mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-03 19:36:20 +00:00 
			
		
		
		
	Merge branch 'main' into chore/docs-api-ref
This commit is contained in:
		
						commit
						fd2f06ee8e
					
				
							
								
								
									
										6
									
								
								.github/actions/check-pr-status/jest.config.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.github/actions/check-pr-status/jest.config.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  preset: '../../../jest-preset.unit.js',
 | 
			
		||||
  displayName: 'Github action check-pr-status',
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/actions/check-pr-status/package.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/actions/check-pr-status/package.json
									
									
									
									
										vendored
									
									
								
							@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "check-pr-status",
 | 
			
		||||
  "version": "4.9.2",
 | 
			
		||||
  "version": "4.10.1",
 | 
			
		||||
  "main": "dist/index.js",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "private": true,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										9
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							@ -3,7 +3,9 @@ updates:
 | 
			
		||||
  - package-ecosystem: npm
 | 
			
		||||
    directory: /
 | 
			
		||||
    schedule:
 | 
			
		||||
      interval: daily
 | 
			
		||||
      interval: weekly
 | 
			
		||||
      day: sunday
 | 
			
		||||
      time: '22:00'
 | 
			
		||||
    versioning-strategy: increase
 | 
			
		||||
    ignore:
 | 
			
		||||
      # Only allow patch as minor babel versions need to be upgraded all together
 | 
			
		||||
@ -14,6 +16,7 @@ updates:
 | 
			
		||||
 | 
			
		||||
      - dependency-name: '*'
 | 
			
		||||
        update-types:
 | 
			
		||||
          - 'version-update:semver-patch'
 | 
			
		||||
          - 'version-update:semver-major'
 | 
			
		||||
 | 
			
		||||
    labels:
 | 
			
		||||
@ -22,7 +25,9 @@ updates:
 | 
			
		||||
  - package-ecosystem: github-actions
 | 
			
		||||
    directory: /
 | 
			
		||||
    schedule:
 | 
			
		||||
      interval: daily
 | 
			
		||||
      interval: weekly
 | 
			
		||||
      day: sunday
 | 
			
		||||
      time: '22:00'
 | 
			
		||||
    labels:
 | 
			
		||||
      - 'source: dependencies'
 | 
			
		||||
      - 'pr: chore'
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								.github/filters.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.github/filters.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
backend:
 | 
			
		||||
  - 'packages/**/package.json'
 | 
			
		||||
  - 'packages/**/server/**/*.(js|ts)'
 | 
			
		||||
  - 'packages/**/strapi-server.js'
 | 
			
		||||
  - 'packages/{utils,generators,cli,providers}/**'
 | 
			
		||||
  - 'packages/core/*/{lib,bin,ee}/**'
 | 
			
		||||
  - 'api-tests/**'
 | 
			
		||||
frontend:
 | 
			
		||||
  - 'packages/**/package.json'
 | 
			
		||||
  - 'packages/**/admin/src/**'
 | 
			
		||||
  - 'packages/**/admin/ee/admin/**'
 | 
			
		||||
  - 'packages/**/strapi-admin.js'
 | 
			
		||||
  - 'packages/core/helper-plugin/**'
 | 
			
		||||
  - 'packages/admin-test-utils/**'
 | 
			
		||||
							
								
								
									
										3
									
								
								.github/jest.config.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/jest.config.js
									
									
									
									
										vendored
									
									
								
							@ -1,3 +0,0 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  displayName: '.github',
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										28
									
								
								.github/workflows/skipped_tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/skipped_tests.yml
									
									
									
									
										vendored
									
									
								
							@ -11,12 +11,28 @@ concurrency:
 | 
			
		||||
  cancel-in-progress: true
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  changes:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    permissions:
 | 
			
		||||
      pull-requests: read
 | 
			
		||||
    outputs:
 | 
			
		||||
      nonDoc: ${{ steps.filter.outputs.nonDoc }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 0
 | 
			
		||||
      # For pull requests it's not necessary to checkout the code
 | 
			
		||||
      - uses: dorny/paths-filter@v2
 | 
			
		||||
        id: filter
 | 
			
		||||
        with:
 | 
			
		||||
          filters: .github/filters.yaml
 | 
			
		||||
 | 
			
		||||
  lint:
 | 
			
		||||
    name: 'lint (node: ${{ matrix.node }})'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        node: [14, 16, 18]
 | 
			
		||||
        node: [18]
 | 
			
		||||
    steps:
 | 
			
		||||
      - run: echo "Skipped"
 | 
			
		||||
 | 
			
		||||
@ -34,6 +50,16 @@ jobs:
 | 
			
		||||
    name: 'unit_front (node: ${{ matrix.node }})'
 | 
			
		||||
    needs: [lint]
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        node: [18]
 | 
			
		||||
    steps:
 | 
			
		||||
      - run: echo "Skipped"
 | 
			
		||||
 | 
			
		||||
  build:
 | 
			
		||||
    name: 'build (node: ${{ matrix.node }})'
 | 
			
		||||
    needs: [changes, lint, unit_front]
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        node: [14, 16, 18]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										49
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										49
									
								
								.github/workflows/tests.yml
									
									
									
									
										vendored
									
									
								
							@ -18,8 +18,25 @@ permissions:
 | 
			
		||||
  actions: read
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  changes:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    permissions:
 | 
			
		||||
      pull-requests: read
 | 
			
		||||
    outputs:
 | 
			
		||||
      backend: ${{ steps.filter.outputs.backend }}
 | 
			
		||||
      frontend: ${{ steps.filter.outputs.frontend }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 0
 | 
			
		||||
      - uses: dorny/paths-filter@v2
 | 
			
		||||
        id: filter
 | 
			
		||||
        with:
 | 
			
		||||
          filters: .github/filters.yaml
 | 
			
		||||
 | 
			
		||||
  lint:
 | 
			
		||||
    name: 'lint (node: ${{ matrix.node }})'
 | 
			
		||||
    needs: [changes]
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
@ -44,7 +61,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
  unit_back:
 | 
			
		||||
    name: 'unit_back (node: ${{ matrix.node }})'
 | 
			
		||||
    needs: [lint]
 | 
			
		||||
    needs: [changes, lint]
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
@ -69,7 +86,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
  unit_front:
 | 
			
		||||
    name: 'unit_front (node: ${{ matrix.node }})'
 | 
			
		||||
    needs: [lint]
 | 
			
		||||
    needs: [changes, lint]
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
@ -87,12 +104,14 @@ jobs:
 | 
			
		||||
          key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
 | 
			
		||||
      - uses: nrwl/nx-set-shas@v3
 | 
			
		||||
      - run: yarn install --immutable
 | 
			
		||||
      - name: Run build:ts for admin-test-utils
 | 
			
		||||
        run: yarn build --projects=@strapi/admin-test-utils,@strapi/helper-plugin --skip-nx-cache
 | 
			
		||||
      - name: Run test
 | 
			
		||||
        run: yarn nx affected --target=test:front --nx-ignore-cycles
 | 
			
		||||
 | 
			
		||||
  build:
 | 
			
		||||
    name: 'build (node: ${{ matrix.node }})'
 | 
			
		||||
    needs: [lint, unit_front]
 | 
			
		||||
    needs: [changes, lint, unit_front]
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
@ -111,8 +130,9 @@ jobs:
 | 
			
		||||
        run: yarn build --projects=@strapi/admin,@strapi/helper-plugin
 | 
			
		||||
 | 
			
		||||
  api_ce_pg:
 | 
			
		||||
    if: needs.changes.outputs.backend == 'true'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [lint, unit_back, unit_front]
 | 
			
		||||
    needs: [changes, lint, unit_back, unit_front]
 | 
			
		||||
    name: '[CE] API Integration (postgres, node: ${{ matrix.node }})'
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
@ -151,8 +171,9 @@ jobs:
 | 
			
		||||
          dbOptions: '--dbclient=postgres --dbhost=localhost --dbport=5432 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
 | 
			
		||||
 | 
			
		||||
  api_ce_mysql:
 | 
			
		||||
    if: needs.changes.outputs.backend == 'true'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [lint, unit_back, unit_front]
 | 
			
		||||
    needs: [changes, lint, unit_back, unit_front]
 | 
			
		||||
    name: '[CE] API Integration (mysql:latest, client: ${{ matrix.db_client }}, node: ${{ matrix.node }})'
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
@ -190,8 +211,9 @@ jobs:
 | 
			
		||||
          dbOptions: '--dbclient=${{ matrix.db_client }} --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
 | 
			
		||||
 | 
			
		||||
  api_ce_mysql_5:
 | 
			
		||||
    if: needs.changes.outputs.backend == 'true'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [lint, unit_back, unit_front]
 | 
			
		||||
    needs: [changes, lint, unit_back, unit_front]
 | 
			
		||||
    name: '[CE] API Integration (mysql:5, client: ${{ matrix.db_client }} , node: ${{ matrix.node }})'
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
@ -228,8 +250,9 @@ jobs:
 | 
			
		||||
          dbOptions: '--dbclient=${{ matrix.db_client }} --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
 | 
			
		||||
 | 
			
		||||
  api_ce_sqlite:
 | 
			
		||||
    if: needs.changes.outputs.backend == 'true'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [lint, unit_back, unit_front]
 | 
			
		||||
    needs: [changes, lint, unit_back, unit_front]
 | 
			
		||||
    name: '[CE] API Integration (sqlite, client: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }})'
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
@ -254,9 +277,9 @@ jobs:
 | 
			
		||||
  # EE
 | 
			
		||||
  api_ee_pg:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [lint, unit_back, unit_front]
 | 
			
		||||
    needs: [changes, lint, unit_back, unit_front]
 | 
			
		||||
    name: '[EE] API Integration (postgres, node: ${{ matrix.node }})'
 | 
			
		||||
    if: github.event.pull_request.head.repo.full_name == github.repository && !(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
 | 
			
		||||
    if: needs.changes.outputs.backend == 'true' && github.event.pull_request.head.repo.full_name == github.repository && !(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
 | 
			
		||||
    env:
 | 
			
		||||
      STRAPI_LICENSE: ${{ secrets.strapiLicense }}
 | 
			
		||||
    strategy:
 | 
			
		||||
@ -298,9 +321,9 @@ jobs:
 | 
			
		||||
 | 
			
		||||
  api_ee_mysql:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [lint, unit_back, unit_front]
 | 
			
		||||
    needs: [changes, lint, unit_back, unit_front]
 | 
			
		||||
    name: '[EE] API Integration (mysql:latest, client: ${{ matrix.db_client }}, node: ${{ matrix.node }})'
 | 
			
		||||
    if: github.event.pull_request.head.repo.full_name == github.repository && !(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
 | 
			
		||||
    if: needs.changes.outputs.backend == 'true' && github.event.pull_request.head.repo.full_name == github.repository && !(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
 | 
			
		||||
    env:
 | 
			
		||||
      STRAPI_LICENSE: ${{ secrets.strapiLicense }}
 | 
			
		||||
    strategy:
 | 
			
		||||
@ -341,9 +364,9 @@ jobs:
 | 
			
		||||
 | 
			
		||||
  api_ee_sqlite:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [lint, unit_back, unit_front]
 | 
			
		||||
    needs: [changes, lint, unit_back, unit_front]
 | 
			
		||||
    name: '[EE] API Integration (sqlite, client: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }})'
 | 
			
		||||
    if: github.event.pull_request.head.repo.full_name == github.repository && !(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
 | 
			
		||||
    if: needs.changes.outputs.backend == 'true' && github.event.pull_request.head.repo.full_name == github.repository && !(github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]')
 | 
			
		||||
    env:
 | 
			
		||||
      STRAPI_LICENSE: ${{ secrets.strapiLicense }}
 | 
			
		||||
    strategy:
 | 
			
		||||
 | 
			
		||||
@ -2,4 +2,3 @@
 | 
			
		||||
. "$(dirname -- "$0")/_/husky.sh"
 | 
			
		||||
 | 
			
		||||
yarn lint-staged
 | 
			
		||||
yarn nx affected:lint --uncommitted --nx-ignore-cycles
 | 
			
		||||
 | 
			
		||||
@ -373,6 +373,12 @@ describe('Role CRUD End to End', () => {
 | 
			
		||||
                  "displayName": "Update",
 | 
			
		||||
                  "subCategory": "options",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  "action": "admin::review-workflows.read",
 | 
			
		||||
                  "category": "review workflows",
 | 
			
		||||
                  "displayName": "Read",
 | 
			
		||||
                  "subCategory": "options",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  "action": "admin::roles.create",
 | 
			
		||||
                  "category": "users and roles",
 | 
			
		||||
 | 
			
		||||
@ -2,17 +2,10 @@
 | 
			
		||||
 | 
			
		||||
const { createStrapiInstance } = require('api-tests/strapi');
 | 
			
		||||
const { createAuthRequest, createRequest } = require('api-tests/request');
 | 
			
		||||
const { createUtils } = require('api-tests/utils');
 | 
			
		||||
const { createUtils, describeOnCondition } = require('api-tests/utils');
 | 
			
		||||
 | 
			
		||||
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
 | 
			
		||||
 | 
			
		||||
if (edition === 'CE') {
 | 
			
		||||
  test('Provider Login (skipped)', () => {
 | 
			
		||||
    expect(true).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
  return;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let strapi;
 | 
			
		||||
let utils;
 | 
			
		||||
const requests = {
 | 
			
		||||
@ -54,7 +47,7 @@ const deleteFixtures = async () => {
 | 
			
		||||
  await utils.deleteRolesById([localData.restrictedRole.id]);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Provider Login', () => {
 | 
			
		||||
describeOnCondition(edition === 'EE')('Provider Login', () => {
 | 
			
		||||
  let hasSSO;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										527
									
								
								api-tests/core/admin/ee/review-workflows.test.api.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										527
									
								
								api-tests/core/admin/ee/review-workflows.test.api.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,527 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const { mapAsync } = require('@strapi/utils');
 | 
			
		||||
 | 
			
		||||
const { createStrapiInstance } = require('api-tests/strapi');
 | 
			
		||||
const { createAuthRequest, createRequest } = require('api-tests/request');
 | 
			
		||||
const { createTestBuilder } = require('api-tests/builder');
 | 
			
		||||
const { describeOnCondition } = require('api-tests/utils');
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
  STAGE_MODEL_UID,
 | 
			
		||||
  WORKFLOW_MODEL_UID,
 | 
			
		||||
  ENTITY_STAGE_ATTRIBUTE,
 | 
			
		||||
} = require('../../../../packages/core/admin/ee/server/constants/workflows');
 | 
			
		||||
 | 
			
		||||
const defaultStages = require('../../../../packages/core/admin/ee/server/constants/default-stages.json');
 | 
			
		||||
 | 
			
		||||
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
 | 
			
		||||
 | 
			
		||||
const productUID = 'api::product.product';
 | 
			
		||||
const model = {
 | 
			
		||||
  draftAndPublish: true,
 | 
			
		||||
  pluginOptions: {},
 | 
			
		||||
  singularName: 'product',
 | 
			
		||||
  pluralName: 'products',
 | 
			
		||||
  displayName: 'Product',
 | 
			
		||||
  kind: 'collectionType',
 | 
			
		||||
  attributes: {
 | 
			
		||||
    name: {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describeOnCondition(edition === 'EE')('Review workflows', () => {
 | 
			
		||||
  const builder = createTestBuilder();
 | 
			
		||||
 | 
			
		||||
  const requests = {
 | 
			
		||||
    public: null,
 | 
			
		||||
    admin: null,
 | 
			
		||||
  };
 | 
			
		||||
  let strapi;
 | 
			
		||||
  let hasRW;
 | 
			
		||||
  let defaultStage;
 | 
			
		||||
  let secondStage;
 | 
			
		||||
  let testWorkflow;
 | 
			
		||||
 | 
			
		||||
  const createEntry = async (uid, data) => {
 | 
			
		||||
    const { body } = await requests.admin({
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      url: `/content-manager/collection-types/${uid}`,
 | 
			
		||||
      body: data,
 | 
			
		||||
    });
 | 
			
		||||
    return body;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const updateEntry = async (uid, id, data) => {
 | 
			
		||||
    const { body } = await requests.admin({
 | 
			
		||||
      method: 'PUT',
 | 
			
		||||
      url: `/content-manager/collection-types/${uid}/${id}`,
 | 
			
		||||
      body: data,
 | 
			
		||||
    });
 | 
			
		||||
    return body;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const findAll = async (uid) => {
 | 
			
		||||
    const { body } = await requests.admin({
 | 
			
		||||
      method: 'GET',
 | 
			
		||||
      url: `/content-manager/collection-types/${uid}`,
 | 
			
		||||
    });
 | 
			
		||||
    return body;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const updateContentType = async (uid, data) => {
 | 
			
		||||
    const result = await requests.admin({
 | 
			
		||||
      method: 'PUT',
 | 
			
		||||
      url: `/content-type-builder/content-types/${uid}`,
 | 
			
		||||
      body: data,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(result.statusCode).toBe(201);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const restart = async () => {
 | 
			
		||||
    await strapi.destroy();
 | 
			
		||||
    strapi = await createStrapiInstance();
 | 
			
		||||
    requests.admin = await createAuthRequest({ strapi });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    await builder.addContentTypes([model]).build();
 | 
			
		||||
    // eslint-disable-next-line node/no-extraneous-require
 | 
			
		||||
    hasRW = require('@strapi/strapi/lib/utils/ee').features.isEnabled('review-workflows');
 | 
			
		||||
 | 
			
		||||
    strapi = await createStrapiInstance();
 | 
			
		||||
    requests.public = createRequest({ strapi });
 | 
			
		||||
    requests.admin = await createAuthRequest({ strapi });
 | 
			
		||||
 | 
			
		||||
    defaultStage = await strapi.query(STAGE_MODEL_UID).create({
 | 
			
		||||
      data: { name: 'Stage' },
 | 
			
		||||
    });
 | 
			
		||||
    secondStage = await strapi.query(STAGE_MODEL_UID).create({
 | 
			
		||||
      data: { name: 'Stage 2' },
 | 
			
		||||
    });
 | 
			
		||||
    testWorkflow = await strapi.query(WORKFLOW_MODEL_UID).create({
 | 
			
		||||
      data: {
 | 
			
		||||
        uid: 'workflow',
 | 
			
		||||
        stages: [defaultStage.id, secondStage.id],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterAll(async () => {
 | 
			
		||||
    await strapi.destroy();
 | 
			
		||||
    await builder.cleanup();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    testWorkflow = await strapi.query(WORKFLOW_MODEL_UID).update({
 | 
			
		||||
      where: { id: testWorkflow.id },
 | 
			
		||||
      data: {
 | 
			
		||||
        uid: 'workflow',
 | 
			
		||||
        stages: [defaultStage.id, secondStage.id],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await updateContentType(productUID, {
 | 
			
		||||
      components: [],
 | 
			
		||||
      contentType: model,
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('Get workflows', () => {
 | 
			
		||||
    test("It shouldn't be available for public", async () => {
 | 
			
		||||
      const res = await requests.public.get('/admin/review-workflows/workflows');
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(res.status).toBe(401);
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(res.status).toBe(404);
 | 
			
		||||
        expect(Array.isArray(res.body)).toBeFalsy();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    test('It should be available for every connected users (admin)', async () => {
 | 
			
		||||
      const res = await requests.admin.get('/admin/review-workflows/workflows');
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(res.status).toBe(200);
 | 
			
		||||
        expect(Array.isArray(res.body.data)).toBeTruthy();
 | 
			
		||||
        // Why 2 workflows ? One added by the test, the other one should be the default workflow added in bootstrap
 | 
			
		||||
        expect(res.body.data).toHaveLength(2);
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(res.status).toBe(404);
 | 
			
		||||
        expect(Array.isArray(res.body)).toBeFalsy();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('Get one workflow', () => {
 | 
			
		||||
    test("It shouldn't be available for public", async () => {
 | 
			
		||||
      const res = await requests.public.get(`/admin/review-workflows/workflows/${testWorkflow.id}`);
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(res.status).toBe(401);
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(res.status).toBe(404);
 | 
			
		||||
        expect(res.body.data).toBeUndefined();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    test('It should be available for every connected users (admin)', async () => {
 | 
			
		||||
      const res = await requests.admin.get(`/admin/review-workflows/workflows/${testWorkflow.id}`);
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(res.status).toBe(200);
 | 
			
		||||
        expect(res.body.data).toBeInstanceOf(Object);
 | 
			
		||||
        expect(res.body.data).toEqual(testWorkflow);
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(res.status).toBe(404);
 | 
			
		||||
        expect(res.body.data).toBeUndefined();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('Get workflow stages', () => {
 | 
			
		||||
    test("It shouldn't be available for public", async () => {
 | 
			
		||||
      const res = await requests.public.get(
 | 
			
		||||
        `/admin/review-workflows/workflows/${testWorkflow.id}?populate=stages`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(res.status).toBe(401);
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(res.status).toBe(404);
 | 
			
		||||
        expect(res.body.data).toBeUndefined();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    test('It should be available for every connected users (admin)', async () => {
 | 
			
		||||
      const res = await requests.admin.get(
 | 
			
		||||
        `/admin/review-workflows/workflows/${testWorkflow.id}?populate=stages`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(res.status).toBe(200);
 | 
			
		||||
        expect(res.body.data).toBeInstanceOf(Object);
 | 
			
		||||
        expect(res.body.data.stages).toBeInstanceOf(Array);
 | 
			
		||||
        expect(res.body.data.stages).toHaveLength(2);
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(res.status).toBe(404);
 | 
			
		||||
        expect(Array.isArray(res.body)).toBeFalsy();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('Get stages', () => {
 | 
			
		||||
    test("It shouldn't be available for public", async () => {
 | 
			
		||||
      const res = await requests.public.get(
 | 
			
		||||
        `/admin/review-workflows/workflows/${testWorkflow.id}/stages`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(res.status).toBe(401);
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(res.status).toBe(404);
 | 
			
		||||
        expect(res.body.data).toBeUndefined();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    test('It should be available for every connected users (admin)', async () => {
 | 
			
		||||
      const res = await requests.admin.get(
 | 
			
		||||
        `/admin/review-workflows/workflows/${testWorkflow.id}/stages`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(res.status).toBe(200);
 | 
			
		||||
        expect(Array.isArray(res.body.data)).toBeTruthy();
 | 
			
		||||
        expect(res.body.data).toHaveLength(2);
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(res.status).toBe(404);
 | 
			
		||||
        expect(Array.isArray(res.body)).toBeFalsy();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('Get stage by id', () => {
 | 
			
		||||
    test("It shouldn't be available for public", async () => {
 | 
			
		||||
      const res = await requests.public.get(
 | 
			
		||||
        `/admin/review-workflows/workflows/${testWorkflow.id}/stages/${secondStage.id}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(res.status).toBe(401);
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(res.status).toBe(404);
 | 
			
		||||
        expect(res.body.data).toBeUndefined();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    test('It should be available for every connected users (admin)', async () => {
 | 
			
		||||
      const res = await requests.admin.get(
 | 
			
		||||
        `/admin/review-workflows/workflows/${testWorkflow.id}/stages/${secondStage.id}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(res.status).toBe(200);
 | 
			
		||||
        expect(res.body.data).toBeInstanceOf(Object);
 | 
			
		||||
        expect(res.body.data).toEqual(secondStage);
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(res.status).toBe(404);
 | 
			
		||||
        expect(res.body.data).toBeUndefined();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('Replace stages of a workflow', () => {
 | 
			
		||||
    let stagesUpdateData;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
      stagesUpdateData = [
 | 
			
		||||
        defaultStage,
 | 
			
		||||
        { id: secondStage.id, name: 'new_name' },
 | 
			
		||||
        { name: 'new stage' },
 | 
			
		||||
      ];
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test("It shouldn't be available for public", async () => {
 | 
			
		||||
      const stagesRes = await requests.public.put(
 | 
			
		||||
        `/admin/review-workflows/workflows/${testWorkflow.id}/stages`,
 | 
			
		||||
        stagesUpdateData
 | 
			
		||||
      );
 | 
			
		||||
      const workflowRes = await requests.public.get(
 | 
			
		||||
        `/admin/review-workflows/workflows/${testWorkflow.id}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(stagesRes.status).toBe(401);
 | 
			
		||||
        expect(workflowRes.status).toBe(401);
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(stagesRes.status).toBe(404);
 | 
			
		||||
        expect(stagesRes.body.data).toBeUndefined();
 | 
			
		||||
        expect(workflowRes.status).toBe(404);
 | 
			
		||||
        expect(workflowRes.body.data).toBeUndefined();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    test('It should be available for every connected users (admin)', async () => {
 | 
			
		||||
      const stagesRes = await requests.admin.put(
 | 
			
		||||
        `/admin/review-workflows/workflows/${testWorkflow.id}/stages`,
 | 
			
		||||
        { body: { data: stagesUpdateData } }
 | 
			
		||||
      );
 | 
			
		||||
      const workflowRes = await requests.admin.get(
 | 
			
		||||
        `/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(stagesRes.status).toBe(200);
 | 
			
		||||
        expect(stagesRes.body.data).toBeInstanceOf(Object);
 | 
			
		||||
        expect(stagesRes.body.data.id).toEqual(testWorkflow.id);
 | 
			
		||||
        expect(workflowRes.status).toBe(200);
 | 
			
		||||
        expect(workflowRes.body.data).toBeInstanceOf(Object);
 | 
			
		||||
        expect(workflowRes.body.data.stages).toBeInstanceOf(Array);
 | 
			
		||||
        expect(workflowRes.body.data.stages[0]).toMatchObject(stagesUpdateData[0]);
 | 
			
		||||
        expect(workflowRes.body.data.stages[1]).toMatchObject(stagesUpdateData[1]);
 | 
			
		||||
        expect(workflowRes.body.data.stages[2]).toMatchObject({
 | 
			
		||||
          id: expect.any(Number),
 | 
			
		||||
          ...stagesUpdateData[2],
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(stagesRes.status).toBe(404);
 | 
			
		||||
        expect(stagesRes.body.data).toBeUndefined();
 | 
			
		||||
        expect(workflowRes.status).toBe(404);
 | 
			
		||||
        expect(workflowRes.body.data).toBeUndefined();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    test('It should throw an error if trying to delete all stages in a workflow', async () => {
 | 
			
		||||
      const stagesRes = await requests.admin.put(
 | 
			
		||||
        `/admin/review-workflows/workflows/${testWorkflow.id}/stages`,
 | 
			
		||||
        { body: { data: [] } }
 | 
			
		||||
      );
 | 
			
		||||
      const workflowRes = await requests.admin.get(
 | 
			
		||||
        `/admin/review-workflows/workflows/${testWorkflow.id}?populate=*`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (hasRW) {
 | 
			
		||||
        expect(stagesRes.status).toBe(400);
 | 
			
		||||
        expect(stagesRes.body.error).toBeDefined();
 | 
			
		||||
        expect(stagesRes.body.error.name).toEqual('ApplicationError');
 | 
			
		||||
        expect(stagesRes.body.error.message).toBeDefined();
 | 
			
		||||
        expect(workflowRes.status).toBe(200);
 | 
			
		||||
        expect(workflowRes.body.data).toBeInstanceOf(Object);
 | 
			
		||||
        expect(workflowRes.body.data.stages).toBeInstanceOf(Array);
 | 
			
		||||
        expect(workflowRes.body.data.stages[0]).toMatchObject({ id: defaultStage.id });
 | 
			
		||||
        expect(workflowRes.body.data.stages[1]).toMatchObject({ id: secondStage.id });
 | 
			
		||||
      } else {
 | 
			
		||||
        expect(stagesRes.status).toBe(404);
 | 
			
		||||
        expect(stagesRes.body.data).toBeUndefined();
 | 
			
		||||
        expect(workflowRes.status).toBe(404);
 | 
			
		||||
        expect(workflowRes.body.data).toBeUndefined();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('Enabling/Disabling review workflows on a content type', () => {
 | 
			
		||||
    beforeAll(async () => {
 | 
			
		||||
      await createEntry(productUID, { name: 'Product' });
 | 
			
		||||
      await createEntry(productUID, { name: 'Product 1' });
 | 
			
		||||
      await createEntry(productUID, { name: 'Product 2' });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('when enabled on a content type, entries of this type should be added to the first stage of the workflow', async () => {
 | 
			
		||||
      await updateContentType(productUID, {
 | 
			
		||||
        components: [],
 | 
			
		||||
        contentType: { ...model, reviewWorkflows: true },
 | 
			
		||||
      });
 | 
			
		||||
      await restart();
 | 
			
		||||
 | 
			
		||||
      const response = await requests.admin({
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        url: `/content-type-builder/content-types/api::product.product`,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(response.body.data.schema.reviewWorkflows).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
      const {
 | 
			
		||||
        body: { results },
 | 
			
		||||
      } = await requests.admin({
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        url: '/content-manager/collection-types/api::product.product',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(results.length).toEqual(3);
 | 
			
		||||
      for (let i = 0; i < results.length; i += 1) {
 | 
			
		||||
        expect(results[i][ENTITY_STAGE_ATTRIBUTE]).toBeDefined();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('when disabled entries in the content type should be removed from any workflow stage', async () => {
 | 
			
		||||
      await updateContentType(productUID, {
 | 
			
		||||
        components: [],
 | 
			
		||||
        contentType: { ...model, reviewWorkflows: false },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await restart();
 | 
			
		||||
 | 
			
		||||
      const response = await requests.admin({
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        url: `/content-type-builder/content-types/api::product.product`,
 | 
			
		||||
      });
 | 
			
		||||
      expect(response.body.data.schema.reviewWorkflows).toBeFalsy();
 | 
			
		||||
 | 
			
		||||
      const {
 | 
			
		||||
        body: { results },
 | 
			
		||||
      } = await requests.admin({
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        url: '/content-manager/collection-types/api::product.product',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      for (let i = 0; i < results.length; i += 1) {
 | 
			
		||||
        expect(results[i][ENTITY_STAGE_ATTRIBUTE]).toBeUndefined();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('update a stage on an entity', () => {
 | 
			
		||||
    describe('Review Workflow is enabled', () => {
 | 
			
		||||
      beforeAll(async () => {
 | 
			
		||||
        await updateContentType(productUID, {
 | 
			
		||||
          components: [],
 | 
			
		||||
          contentType: { ...model, reviewWorkflows: true },
 | 
			
		||||
        });
 | 
			
		||||
        await restart();
 | 
			
		||||
      });
 | 
			
		||||
      test('Should update the accordingly on an entity', async () => {
 | 
			
		||||
        const entry = await createEntry(productUID, { name: 'Product' });
 | 
			
		||||
 | 
			
		||||
        const response = await requests.admin({
 | 
			
		||||
          method: 'PUT',
 | 
			
		||||
          url: `/admin/content-manager/collection-types/${productUID}/${entry.id}/stage`,
 | 
			
		||||
          body: {
 | 
			
		||||
            data: { id: secondStage.id },
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(response.status).toEqual(200);
 | 
			
		||||
        expect(response.body.data[ENTITY_STAGE_ATTRIBUTE]).toEqual(
 | 
			
		||||
          expect.objectContaining({ id: secondStage.id })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
      test('Should throw an error if stage does not exist', async () => {
 | 
			
		||||
        const entry = await createEntry(productUID, { name: 'Product' });
 | 
			
		||||
 | 
			
		||||
        const response = await requests.admin({
 | 
			
		||||
          method: 'PUT',
 | 
			
		||||
          url: `/admin/content-manager/collection-types/${productUID}/${entry.id}/stage`,
 | 
			
		||||
          body: {
 | 
			
		||||
            data: { id: 1234 },
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(response.status).toEqual(400);
 | 
			
		||||
        expect(response.body.error).toBeDefined();
 | 
			
		||||
        expect(response.body.error.name).toEqual('ApplicationError');
 | 
			
		||||
        expect(response.body.error.message).toEqual('Selected stage does not exist');
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    describe('Review Workflow is disabled', () => {
 | 
			
		||||
      beforeAll(async () => {
 | 
			
		||||
        await updateContentType(productUID, {
 | 
			
		||||
          components: [],
 | 
			
		||||
          contentType: { ...model, reviewWorkflows: false },
 | 
			
		||||
        });
 | 
			
		||||
        await restart();
 | 
			
		||||
      });
 | 
			
		||||
      test('Should not update the entity', async () => {
 | 
			
		||||
        const entry = await createEntry(productUID, { name: 'Product' });
 | 
			
		||||
 | 
			
		||||
        const response = await requests.admin({
 | 
			
		||||
          method: 'PUT',
 | 
			
		||||
          url: `/admin/content-manager/collection-types/${productUID}/${entry.id}/stage`,
 | 
			
		||||
          body: {
 | 
			
		||||
            data: { id: secondStage.id },
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(response.status).toEqual(400);
 | 
			
		||||
        expect(response.body.error).toBeDefined();
 | 
			
		||||
        expect(response.body.error.name).toBe('ApplicationError');
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('Creating an entity in a review workflow content type', () => {
 | 
			
		||||
    beforeAll(async () => {
 | 
			
		||||
      await updateContentType(productUID, {
 | 
			
		||||
        components: [],
 | 
			
		||||
        contentType: { ...model, reviewWorkflows: true },
 | 
			
		||||
      });
 | 
			
		||||
      await restart();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('when review workflows is enabled on a content type, new entries should be added to the first stage of the default workflow', async () => {
 | 
			
		||||
      const adminResponse = await createEntry(productUID, { name: 'Product' });
 | 
			
		||||
      expect(await adminResponse[ENTITY_STAGE_ATTRIBUTE].name).toEqual(defaultStages[0].name);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  //FIXME Flaky test
 | 
			
		||||
  describe.skip('Deleting a stage when content already exists', () => {
 | 
			
		||||
    test('When content exists in a review stage and this stage is deleted, the content should be moved to the nearest available stage', async () => {
 | 
			
		||||
      const products = await findAll(productUID);
 | 
			
		||||
 | 
			
		||||
      // Move half of the entries to the last stage,
 | 
			
		||||
      // and the other half to the first stage
 | 
			
		||||
      await mapAsync(products.results, async (entity) =>
 | 
			
		||||
        updateEntry(productUID, entity.id, {
 | 
			
		||||
          [ENTITY_STAGE_ATTRIBUTE]: entity.id % 2 ? defaultStage.id : secondStage.id,
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Delete last stage stage of the default workflow
 | 
			
		||||
      await requests.admin.put(`/admin/review-workflows/workflows/${testWorkflow.id}/stages`, {
 | 
			
		||||
        body: { data: [defaultStage] },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Expect the content in these stages to be moved to the nearest available stage
 | 
			
		||||
      const productsAfter = await findAll(productUID);
 | 
			
		||||
      for (const entry of productsAfter.results) {
 | 
			
		||||
        expect(entry[ENTITY_STAGE_ATTRIBUTE].name).toEqual(defaultStage.name);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										17
									
								
								docs/docs/core/admin/ee/intro.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								docs/docs/core/admin/ee/intro.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
---
 | 
			
		||||
title: Introduction
 | 
			
		||||
slug: /admin/ee
 | 
			
		||||
tags:
 | 
			
		||||
  - enterprise-edition
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
# Admin Enterprise Edition
 | 
			
		||||
 | 
			
		||||
This section is an overview of all the features related to the Enterprise Edition in Admin:
 | 
			
		||||
 | 
			
		||||
```mdx-code-block
 | 
			
		||||
import DocCardList from '@theme/DocCardList';
 | 
			
		||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
 | 
			
		||||
 | 
			
		||||
<DocCardList items={useCurrentSidebarCategory().items} />
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										140
									
								
								docs/docs/core/admin/ee/review-workflows.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								docs/docs/core/admin/ee/review-workflows.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,140 @@
 | 
			
		||||
---
 | 
			
		||||
title: Review Workflows
 | 
			
		||||
slug: /admin/ee/review-workflows
 | 
			
		||||
description: Review workflow technical design
 | 
			
		||||
tags:
 | 
			
		||||
  - review-workflows
 | 
			
		||||
  - implementation
 | 
			
		||||
  - tech design
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
# Review Workflows
 | 
			
		||||
 | 
			
		||||
## Summary
 | 
			
		||||
 | 
			
		||||
The review workflow feature is only available in the Enterprise Edition.
 | 
			
		||||
That is why, in part, it is completely decoupled from the code of the Community Edition.
 | 
			
		||||
 | 
			
		||||
The purpose of this feature is to allow users to assign a tag to the various entities of their Strapi project. This tag is called a 'stage' and is available within what we will call a workflow.
 | 
			
		||||
 | 
			
		||||
## Detailed backend design
 | 
			
		||||
 | 
			
		||||
The Review Workflow feature have been built with one main consideration, to be decoupled from the Community Edition. As so, the implementation can relate a lot to how a plugin would be built.
 | 
			
		||||
 | 
			
		||||
All the backend code related to Review Workflow can be found in `packages/core/admin/ee`.
 | 
			
		||||
This code is separated into several elements:
 | 
			
		||||
 | 
			
		||||
- Two content-types
 | 
			
		||||
  - _strapi_workflows_: `packages/core/admin/ee/server/content-types/workflow/index.js`
 | 
			
		||||
  - _strapi_workflows_stages_: `packages/core/admin/ee/server/content-types/workflow-stage/index.js`
 | 
			
		||||
- Two controllers
 | 
			
		||||
  - _workflows_: `packages/core/admin/ee/server/controllers/workflows/index.js`
 | 
			
		||||
  - _stages_: `packages/core/admin/ee/server/controllers/workflows/stages/index.js`
 | 
			
		||||
- One middleware
 | 
			
		||||
  - _contentTypeMiddleware_: `packages/core/admin/ee/server/middlewares/review-workflows.js`
 | 
			
		||||
- Routes
 | 
			
		||||
  - `packages/core/admin/ee/server/routes/index.js`
 | 
			
		||||
- Four services
 | 
			
		||||
  - _review-workflows_: `packages/core/admin/ee/server/services/review-workflows/review-workflows.js`
 | 
			
		||||
  - _workflows_: `packages/core/admin/ee/server/services/review-workflows/workflows.js`
 | 
			
		||||
  - _stages_: `packages/core/admin/ee/server/services/review-workflows/stages.js`
 | 
			
		||||
  - _metrics_: `packages/core/admin/ee/server/services/review-workflows/metrics.js`
 | 
			
		||||
- One decorator
 | 
			
		||||
  - _EntityService_ decorator: `packages/core/admin/ee/server/services/review-workflows/entity-service-decorator.js`
 | 
			
		||||
- One utils file
 | 
			
		||||
  - _Review workflows utils_: `packages/core/admin/ee/server/utils/review-workflows.js`
 | 
			
		||||
- A bootstrap and a register part
 | 
			
		||||
  - `packages/core/admin/ee/server/bootstrap.js`
 | 
			
		||||
  - `packages/core/admin/ee/server/register.js`
 | 
			
		||||
 | 
			
		||||
### Content types
 | 
			
		||||
 | 
			
		||||
#### strapi_workflows
 | 
			
		||||
 | 
			
		||||
This content type stores the workflow information and is responsible for holding all the information about stages and their order. In MVP, only one workflow is stored inside the Strapi database.
 | 
			
		||||
 | 
			
		||||
#### strapi_workflows_stages
 | 
			
		||||
 | 
			
		||||
This content type store the stage information such as its name.
 | 
			
		||||
 | 
			
		||||
### Controllers
 | 
			
		||||
 | 
			
		||||
#### workflows
 | 
			
		||||
 | 
			
		||||
Used to interact with the `strapi_workflows` content-type.
 | 
			
		||||
 | 
			
		||||
#### stages
 | 
			
		||||
 | 
			
		||||
Used to interact with the `strapi_workflows_stages` content-type.
 | 
			
		||||
 | 
			
		||||
### Middlewares
 | 
			
		||||
 | 
			
		||||
#### contentTypeMiddleware
 | 
			
		||||
 | 
			
		||||
In order to properly manage the options for content-type in the root level of the object, it is necessary to relocate the `reviewWorkflows` option within the `options` object located inside the content-type data. By doing so, we can ensure that all options are consistently organized and easily accessible within their respective data structures. This will also make it simpler to maintain and update the options as needed, providing a more streamlined and efficient workflow for developers working with the system. Therefore, it is recommended to move the reviewWorkflows option to its appropriate location within the options object inside the content-type data before sending it to the admin API.
 | 
			
		||||
 | 
			
		||||
### Routes
 | 
			
		||||
 | 
			
		||||
The Admin API of the Enterprise Edition includes several routes related to the Review Workflow feature. Here is a list of those routes:
 | 
			
		||||
 | 
			
		||||
#### GET `/review-workflows/workflows`
 | 
			
		||||
 | 
			
		||||
This route returns a list of all workflows.
 | 
			
		||||
 | 
			
		||||
#### GET `/review-workflows/workflows/:id`
 | 
			
		||||
 | 
			
		||||
This route returns the details of a specific workflow identified by the id parameter.
 | 
			
		||||
 | 
			
		||||
#### GET `/review-workflows/workflows/:workflow_id/stages`
 | 
			
		||||
 | 
			
		||||
This route returns a list of all stages associated with a specific workflow identified by the workflow_id parameter.
 | 
			
		||||
 | 
			
		||||
#### GET `/review-workflows/workflows/:workflow_id/stages/:id`
 | 
			
		||||
 | 
			
		||||
This route returns the details of a specific stage identified by the id parameter and associated with the workflow identified by the workflow_id parameter.
 | 
			
		||||
 | 
			
		||||
#### PUT `/review-workflows/workflows/:workflow_id/stages`
 | 
			
		||||
 | 
			
		||||
This route updates the stages associated with a specific workflow identified by the workflow_id parameter. The updated stages are passed in the request body.
 | 
			
		||||
 | 
			
		||||
#### PUT `/content-manager/(collection|single)-types/:model_uid/:id/stage`
 | 
			
		||||
 | 
			
		||||
This route updates the stage of a specific entity identified by the id parameter and belonging to a specific collection identified by the model_uid parameter. The new stage value is passed in the request body.
 | 
			
		||||
 | 
			
		||||
### Services
 | 
			
		||||
 | 
			
		||||
The Review Workflow feature of the Enterprise Edition includes several services to manipulate workflows and stages. Here is a list of those services:
 | 
			
		||||
 | 
			
		||||
#### review-workflows
 | 
			
		||||
 | 
			
		||||
This service is used during the bootstrap and register phases of Strapi. Its primary responsibility is to migrate data on entities as needed and add the stage field to the entity schemas.
 | 
			
		||||
 | 
			
		||||
#### workflows
 | 
			
		||||
 | 
			
		||||
This service is used to manipulate the workflows entities. It provides functionalities to create, retrieve, and update workflows.
 | 
			
		||||
 | 
			
		||||
#### stages
 | 
			
		||||
 | 
			
		||||
This service is used to manipulate the stages entities and to update stages on other entities. It provides functionalities to create, retrieve, update, and delete stages.
 | 
			
		||||
 | 
			
		||||
#### metrics
 | 
			
		||||
 | 
			
		||||
This is the telemetry service used to gather information on the usage of this feature. It provides information on the number of workflows and stages created, as well as the frequency of stage updates on entities.
 | 
			
		||||
 | 
			
		||||
### Decorators
 | 
			
		||||
 | 
			
		||||
#### Entity Service
 | 
			
		||||
 | 
			
		||||
The entity service is decorated so that entities can be linked to a default stage upon creation. This allows the entities to be automatically associated with a specific workflow stage when they are created.
 | 
			
		||||
 | 
			
		||||
## Alternatives
 | 
			
		||||
 | 
			
		||||
The Review Workflow feature is currently included as a core feature within the Strapi repository. However, there has been discussion about potentially moving it to a plugin in the future. While no decision has been made on this subject yet, it is possible that it may happen at some point in the future.
 | 
			
		||||
 | 
			
		||||
## Resources
 | 
			
		||||
 | 
			
		||||
- https://docs.strapi.io/user-docs/settings/review-workflows
 | 
			
		||||
- https://docs.strapi.io/user-docs/content-type-builder/creating-new-content-type#creating-a-new-content-type
 | 
			
		||||
- https://docs.strapi.io/user-docs/users-roles-permissions/configuring-administrator-roles#plugins-and-settings
 | 
			
		||||
- [Content manager](/content-manager/review-workflows)
 | 
			
		||||
- [Content type builder](/content-type-builder/review-workflows)
 | 
			
		||||
							
								
								
									
										17
									
								
								docs/docs/core/admin/intro.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								docs/docs/core/admin/intro.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
---
 | 
			
		||||
title: Introduction
 | 
			
		||||
slug: /admin
 | 
			
		||||
tags:
 | 
			
		||||
  - admin
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
# Admin
 | 
			
		||||
 | 
			
		||||
This section is an overview of all the features related to admin:
 | 
			
		||||
 | 
			
		||||
```mdx-code-block
 | 
			
		||||
import DocCardList from '@theme/DocCardList';
 | 
			
		||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
 | 
			
		||||
 | 
			
		||||
<DocCardList items={useCurrentSidebarCategory().items} />
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										95
									
								
								docs/docs/core/content-manager/review-workflows.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								docs/docs/core/content-manager/review-workflows.mdx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,95 @@
 | 
			
		||||
---
 | 
			
		||||
title: Review Workflows
 | 
			
		||||
slug: /content-manager/review-workflows
 | 
			
		||||
description: Guide for review workflows in the content-manager.
 | 
			
		||||
tags:
 | 
			
		||||
  - content-manager
 | 
			
		||||
  - review-workflows
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Summary
 | 
			
		||||
 | 
			
		||||
Review workflows are disabled for all content-types by default and have to be enabled for each of them. More about how to [enable review-workflows for a content-type](/content-type-builder/review-workflows).
 | 
			
		||||
 | 
			
		||||
The feature is visible in two locations of the content-manager:
 | 
			
		||||
 | 
			
		||||
### List view
 | 
			
		||||
 | 
			
		||||
If the feature is enabled for a content-type a new column will show up, displaying the current stage. If no stage was assigned to an entity,
 | 
			
		||||
the column is displayed as empty.
 | 
			
		||||
 | 
			
		||||
### Edit view
 | 
			
		||||
 | 
			
		||||
If the feature is enabled for a content-type the currently selected stage will show up in the information sidebar next to the edit view. Users
 | 
			
		||||
can select any other stage of the current workflow.
 | 
			
		||||
 | 
			
		||||
Stage assignments are decoupled from entities, meaning that updating an entity won't set the selected stage. Instead the stage select
 | 
			
		||||
component will trigger an atomic update using the admin API to assign/ update a stage to the current entity, when a new value is selected.
 | 
			
		||||
Because of this decoupling stages **can not be assigned on entity creation** and only after the have been created.
 | 
			
		||||
 | 
			
		||||
If no stage was assigned to the current entity the select component displays and error and asks a user to select a stage.
 | 
			
		||||
 | 
			
		||||
## Default stage
 | 
			
		||||
 | 
			
		||||
By default every entity which is part of a content-type with review workflows enabled, will get the first stage of the attached workflow
 | 
			
		||||
assigned upon creation via the **admin API**, **content API** and the **entity Service**.
 | 
			
		||||
 | 
			
		||||
### Stage assignments
 | 
			
		||||
 | 
			
		||||
The default stage is assigned upon entity creation. In the bootstrap phase of Strapi all entities that do not have a stage assigned
 | 
			
		||||
(and are part of a content-type which has the feature enabled) will have the default stage assigned. Initially this was meant as the
 | 
			
		||||
migration when the feature is enabled for the first time to ensure all entities, but became also a safety-net for entities
 | 
			
		||||
that do not have a stage set.
 | 
			
		||||
 | 
			
		||||
### Nullish stages
 | 
			
		||||
 | 
			
		||||
Entities which are not created through the admin API, content API or entity service will not have a stage assigned by default (e.g. lifecycle methods).
 | 
			
		||||
If entities are created through more low-level ways, developers need to take care to assign a stage individually.
 | 
			
		||||
 | 
			
		||||
This means at any place where the UI displays a stage, it has to be prepared to receive `null` and should not crash.
 | 
			
		||||
 | 
			
		||||
## List view
 | 
			
		||||
 | 
			
		||||
The information which stage is current assigned to an entity is send as part of the content-type response payload for each entity in the attribute `strapi_reviewWorkflows_stage`.
 | 
			
		||||
Please see [Data Shapes](/settings/review-workflows#data-shapes) for type definitions.
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
{
 | 
			
		||||
  // ... entity attributes
 | 
			
		||||
  strapi_reviewWorkflows_stage?: Stage | null
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
`http://localhost:1337/content-manager/content-types` returns whether the feature is enabled for the content-type. `options.reviewWorkflows` is either `true`, `false` or undefined.
 | 
			
		||||
 | 
			
		||||
**Note**: Downgrading from EE to CE won't delete the associated review workflow data and `http://localhost:1337/content-manager/content-types` still returns true. The admin app had to
 | 
			
		||||
add an additional check if the feature toggle returned in `http://localhost:1337/admin/project-type` is enabled.
 | 
			
		||||
 | 
			
		||||
## Edit View
 | 
			
		||||
 | 
			
		||||
The information which stage is current assigned to an entity is send as part of the entity response payload in the attribute `strapi_reviewWorkflows_stage`.
 | 
			
		||||
Please see [Data Shapes](/settings/review-workflows#data-shapes) for type definitions.
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
{
 | 
			
		||||
  // ... entity attributes
 | 
			
		||||
  strapi_reviewWorkflows_stage?: Stage | null
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
- `undefined`: the feature is not enabled for this content-type
 | 
			
		||||
- `null`: no stage is assigned to the entity
 | 
			
		||||
 | 
			
		||||
### Endpoints
 | 
			
		||||
 | 
			
		||||
#### `PUT /admin/content-manager/[kind]/[content-type-uid]/[entity-id]/stage`
 | 
			
		||||
 | 
			
		||||
Assigns a stage to an entity.
 | 
			
		||||
 | 
			
		||||
##### Payload
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
data: {
 | 
			
		||||
  id: int // assigned stage id
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										38
									
								
								docs/docs/core/content-type-builder/review-workflows.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								docs/docs/core/content-type-builder/review-workflows.mdx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
---
 | 
			
		||||
title: Review Workflows
 | 
			
		||||
slug: /content-type-builder/review-workflows
 | 
			
		||||
description: Guide for review workflows in the content-type-builder.
 | 
			
		||||
tags:
 | 
			
		||||
  - content-type-builder
 | 
			
		||||
  - review-workflows
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Summary
 | 
			
		||||
 | 
			
		||||
By **default review workflows are disabled on all content-types** and users have to enable one workflow
 | 
			
		||||
per content-type. This can be achieved in the "Advanced Settings" Tab of the edit content-type
 | 
			
		||||
modal.
 | 
			
		||||
 | 
			
		||||
Similar to draft & publish review-workflows registers a new input component type called `toggle-review-workflows`
 | 
			
		||||
which is used to render the checkbox component.
 | 
			
		||||
 | 
			
		||||
**Note**: *Ideally the code should have been placed in the `ee` folder to be
 | 
			
		||||
under the enterprise license, but neither the content-type-builder nor the babel-plugin to transpile the ee code had support for this.*
 | 
			
		||||
 | 
			
		||||
## Endpoints
 | 
			
		||||
 | 
			
		||||
### `PUT /content-type-builder/content-types/[content-type-uid]`
 | 
			
		||||
 | 
			
		||||
Toggle review workflows for the content-type.
 | 
			
		||||
 | 
			
		||||
#### Payload
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
{
 | 
			
		||||
  components: [],
 | 
			
		||||
  contentType: {
 | 
			
		||||
    attributes: {},
 | 
			
		||||
    reviewWorkflows: boolean
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
@ -1,8 +1,7 @@
 | 
			
		||||
---
 | 
			
		||||
title: useCallbackRef
 | 
			
		||||
description: API reference for the useCallbackRef hook in Strapi's Content Manager
 | 
			
		||||
description: API reference for the useCallbackRef hook
 | 
			
		||||
tags:
 | 
			
		||||
  - content-manager
 | 
			
		||||
  - hooks
 | 
			
		||||
  - refs
 | 
			
		||||
  - callbacks
 | 
			
		||||
							
								
								
									
										17
									
								
								docs/docs/core/settings/intro.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								docs/docs/core/settings/intro.mdx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
---
 | 
			
		||||
title: Introduction
 | 
			
		||||
slug: /settings/intro
 | 
			
		||||
tags:
 | 
			
		||||
  - settings
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
# Settings
 | 
			
		||||
 | 
			
		||||
This section is an overview of all the features related to Settings:
 | 
			
		||||
 | 
			
		||||
```mdx-code-block
 | 
			
		||||
import DocCardList from '@theme/DocCardList';
 | 
			
		||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
 | 
			
		||||
 | 
			
		||||
<DocCardList items={useCurrentSidebarCategory().items} />
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										77
									
								
								docs/docs/core/settings/review-workflows.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								docs/docs/core/settings/review-workflows.mdx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,77 @@
 | 
			
		||||
---
 | 
			
		||||
title: Review Workflows
 | 
			
		||||
slug: /settings/review-workflows
 | 
			
		||||
description: Guide for review workflows in settings.
 | 
			
		||||
tags:
 | 
			
		||||
  - settings
 | 
			
		||||
  - review-workflows
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Summary
 | 
			
		||||
 | 
			
		||||
The settings page for review workflows is where users can add and edit stages in any workflow. A stage is a step within
 | 
			
		||||
each workflow. It is only **accessible in enterprise mode** and if the read permission `admin::review-workflows.read` is set to `true`.
 | 
			
		||||
 | 
			
		||||
Upon mount the settings page injects itself into the global redux store under the namespace `settings_review-workflows`. Redux is
 | 
			
		||||
then used for all state management updates on the settings page. `Formik` is used to render and validate the list of stages. It
 | 
			
		||||
is integrated with redux, so that all input components are controlled components.
 | 
			
		||||
 | 
			
		||||
### Form submission
 | 
			
		||||
 | 
			
		||||
The form the wraps workflow stages submits all stages at once, because we expect the number of stages per workflow to be
 | 
			
		||||
rather small. Because of this we can simply re-order stages by sending a different order. Every stage that sends a corresponding `id`
 | 
			
		||||
attribute will be re-ordered and not created. Stages without an `id` property will be created in the database on submission.
 | 
			
		||||
 | 
			
		||||
### Stage deletion
 | 
			
		||||
 | 
			
		||||
In case a stage is deleted, all **entities which are connected to that stage are moved to the previous stage**. Because a stage deletion
 | 
			
		||||
might have big effects on the database, a confirmation is shown when a stage is up for deletion.
 | 
			
		||||
 | 
			
		||||
Changes are only applied if the user hits "Save". It is not possible to remove all stages from a workflow (neither in the UI nor the API).
 | 
			
		||||
 | 
			
		||||
### Hooks
 | 
			
		||||
 | 
			
		||||
#### `useReviewWorkflows(workflowId?: number)`
 | 
			
		||||
 | 
			
		||||
This hook allows to fetch either one (if `workflowId` is passed) or all workflows at once. By default all stages are populated. The
 | 
			
		||||
hooks returns a react-query result. This hook is used to fetch a workflow on the settings page and the content-manager edit view.
 | 
			
		||||
 | 
			
		||||
The API returns an `array` of workflows. In the first iteration only one workflow is supported, but this is subject to change soon.
 | 
			
		||||
 | 
			
		||||
### Data shapes
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
type Stage {
 | 
			
		||||
    id: int
 | 
			
		||||
    name: string // max-length: 255 characters
 | 
			
		||||
    createdAt: Date
 | 
			
		||||
    updatedAt: Date
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Worklow {
 | 
			
		||||
    id: int,
 | 
			
		||||
    stages: Stage[]
 | 
			
		||||
    createdAt: Date
 | 
			
		||||
    updatedAt: Date
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Endpoints
 | 
			
		||||
 | 
			
		||||
#### `GET /admin/review-workflows/workflows/`
 | 
			
		||||
 | 
			
		||||
Returns a list of all workflows. Stages can be populated using `?populate=stages`.
 | 
			
		||||
 | 
			
		||||
#### `PUT /admin/review-workflows/workflows/`
 | 
			
		||||
 | 
			
		||||
Update workflow stages.
 | 
			
		||||
 | 
			
		||||
##### Payload
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
{
 | 
			
		||||
  data: Stage[]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Note**: All stages need to be submitted. Stages without an `id` attribute will be created. The order of stages is persisted in the database.
 | 
			
		||||
@ -16,17 +16,6 @@ const sidebars = {
 | 
			
		||||
  // By default, Docusaurus generates a sidebar from the docs folder structure
 | 
			
		||||
  docs: [
 | 
			
		||||
    'index',
 | 
			
		||||
    {
 | 
			
		||||
      type: 'category',
 | 
			
		||||
      label: 'Admin',
 | 
			
		||||
      items: [
 | 
			
		||||
        {
 | 
			
		||||
          type: 'doc',
 | 
			
		||||
          label: 'Link Strapi Design System',
 | 
			
		||||
          id: 'core/admin/link-strapi-design-system',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'category',
 | 
			
		||||
      label: 'Core',
 | 
			
		||||
@ -38,7 +27,26 @@ const sidebars = {
 | 
			
		||||
        {
 | 
			
		||||
          type: 'category',
 | 
			
		||||
          label: 'Admin',
 | 
			
		||||
          link: {
 | 
			
		||||
            type: 'doc',
 | 
			
		||||
            id: 'core/admin/intro',
 | 
			
		||||
          },
 | 
			
		||||
          items: [
 | 
			
		||||
            {
 | 
			
		||||
              type: 'category',
 | 
			
		||||
              label: 'Enterprise Edition',
 | 
			
		||||
              link: {
 | 
			
		||||
                type: 'doc',
 | 
			
		||||
                id: 'core/admin/ee/intro',
 | 
			
		||||
              },
 | 
			
		||||
              items: [
 | 
			
		||||
                {
 | 
			
		||||
                  type: 'doc',
 | 
			
		||||
                  label: 'Review Workflows',
 | 
			
		||||
                  id: 'core/admin/ee/review-workflows',
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              type: 'doc',
 | 
			
		||||
              label: 'Link Strapi Design System',
 | 
			
		||||
@ -58,11 +66,6 @@ const sidebars = {
 | 
			
		||||
              type: 'category',
 | 
			
		||||
              label: 'Hooks',
 | 
			
		||||
              items: [
 | 
			
		||||
                {
 | 
			
		||||
                  type: 'doc',
 | 
			
		||||
                  label: 'useCallbackRef',
 | 
			
		||||
                  id: 'core/content-manager/hooks/use-callback-ref',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  type: 'doc',
 | 
			
		||||
                  label: 'useDragAndDrop',
 | 
			
		||||
@ -75,6 +78,11 @@ const sidebars = {
 | 
			
		||||
              label: 'Relations',
 | 
			
		||||
              id: 'core/content-manager/relations',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              type: 'doc',
 | 
			
		||||
              label: 'Review Workflows',
 | 
			
		||||
              id: 'core/content-manager/review-workflows',
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
@ -84,7 +92,13 @@ const sidebars = {
 | 
			
		||||
            type: 'doc',
 | 
			
		||||
            id: 'core/content-type-builder/intro',
 | 
			
		||||
          },
 | 
			
		||||
          items: ['example'],
 | 
			
		||||
          items: [
 | 
			
		||||
            {
 | 
			
		||||
              type: 'doc',
 | 
			
		||||
              label: 'Review Workflows',
 | 
			
		||||
              id: 'core/content-type-builder/review-workflows',
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'category',
 | 
			
		||||
@ -120,6 +134,11 @@ const sidebars = {
 | 
			
		||||
                  label: 'useAPIErrorHandler',
 | 
			
		||||
                  id: 'core/helper-plugin/hooks/use-api-error-handler',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  type: 'doc',
 | 
			
		||||
                  label: 'useCallbackRef',
 | 
			
		||||
                  id: 'core/helper-plugin/hooks/use-callback-ref',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  type: 'doc',
 | 
			
		||||
                  label: 'useCollator',
 | 
			
		||||
@ -170,6 +189,21 @@ const sidebars = {
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'category',
 | 
			
		||||
          label: 'Settings',
 | 
			
		||||
          link: {
 | 
			
		||||
            type: 'doc',
 | 
			
		||||
            id: 'core/settings/intro',
 | 
			
		||||
          },
 | 
			
		||||
          items: [
 | 
			
		||||
            {
 | 
			
		||||
              type: 'doc',
 | 
			
		||||
              label: 'Review Workflows',
 | 
			
		||||
              id: 'core/settings/review-workflows',
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'category',
 | 
			
		||||
          label: 'Utils',
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ module.exports = () => ({
 | 
			
		||||
  documentation: {
 | 
			
		||||
    config: {
 | 
			
		||||
      info: {
 | 
			
		||||
        version: '2.0.0',
 | 
			
		||||
        version: '1.0.0',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "getstarted",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "version": "4.9.2",
 | 
			
		||||
  "version": "4.10.1",
 | 
			
		||||
  "description": "A Strapi application.",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "develop": "strapi develop",
 | 
			
		||||
@ -12,26 +12,26 @@
 | 
			
		||||
    "strapi": "strapi"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@strapi/icons": "1.6.6",
 | 
			
		||||
    "@strapi/plugin-color-picker": "4.9.2",
 | 
			
		||||
    "@strapi/plugin-documentation": "4.9.2",
 | 
			
		||||
    "@strapi/plugin-graphql": "4.9.2",
 | 
			
		||||
    "@strapi/plugin-i18n": "4.9.2",
 | 
			
		||||
    "@strapi/plugin-sentry": "4.9.2",
 | 
			
		||||
    "@strapi/plugin-users-permissions": "4.9.2",
 | 
			
		||||
    "@strapi/provider-email-mailgun": "4.9.2",
 | 
			
		||||
    "@strapi/provider-upload-aws-s3": "4.9.2",
 | 
			
		||||
    "@strapi/provider-upload-cloudinary": "4.9.2",
 | 
			
		||||
    "@strapi/strapi": "4.9.2",
 | 
			
		||||
    "@strapi/icons": "1.7.2",
 | 
			
		||||
    "@strapi/plugin-color-picker": "4.10.1",
 | 
			
		||||
    "@strapi/plugin-documentation": "4.10.1",
 | 
			
		||||
    "@strapi/plugin-graphql": "4.10.1",
 | 
			
		||||
    "@strapi/plugin-i18n": "4.10.1",
 | 
			
		||||
    "@strapi/plugin-sentry": "4.10.1",
 | 
			
		||||
    "@strapi/plugin-users-permissions": "4.10.1",
 | 
			
		||||
    "@strapi/provider-email-mailgun": "4.10.1",
 | 
			
		||||
    "@strapi/provider-upload-aws-s3": "4.10.1",
 | 
			
		||||
    "@strapi/provider-upload-cloudinary": "4.10.1",
 | 
			
		||||
    "@strapi/strapi": "4.10.1",
 | 
			
		||||
    "@vscode/sqlite3": "5.1.2",
 | 
			
		||||
    "better-sqlite3": "8.0.1",
 | 
			
		||||
    "better-sqlite3": "8.3.0",
 | 
			
		||||
    "lodash": "4.17.21",
 | 
			
		||||
    "mysql": "2.18.1",
 | 
			
		||||
    "mysql2": "3.2.0",
 | 
			
		||||
    "passport-google-oauth2": "0.2.0",
 | 
			
		||||
    "pg": "8.8.0",
 | 
			
		||||
    "react": "^17.0.2",
 | 
			
		||||
    "react-intl": "6.3.2",
 | 
			
		||||
    "react-intl": "6.4.1",
 | 
			
		||||
    "sqlite3": "5.1.2"
 | 
			
		||||
  },
 | 
			
		||||
  "strapi": {
 | 
			
		||||
 | 
			
		||||
@ -9,8 +9,8 @@
 | 
			
		||||
    "name": "Address"
 | 
			
		||||
  },
 | 
			
		||||
  "options": {
 | 
			
		||||
    "draftAndPublish": false,
 | 
			
		||||
    "comment": ""
 | 
			
		||||
    "reviewWorkflows": true,
 | 
			
		||||
    "draftAndPublish": false
 | 
			
		||||
  },
 | 
			
		||||
  "pluginOptions": {},
 | 
			
		||||
  "attributes": {
 | 
			
		||||
 | 
			
		||||
@ -9,8 +9,8 @@
 | 
			
		||||
    "name": "Category"
 | 
			
		||||
  },
 | 
			
		||||
  "options": {
 | 
			
		||||
    "draftAndPublish": true,
 | 
			
		||||
    "comment": ""
 | 
			
		||||
    "reviewWorkflows": true,
 | 
			
		||||
    "draftAndPublish": true
 | 
			
		||||
  },
 | 
			
		||||
  "pluginOptions": {
 | 
			
		||||
    "i18n": {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "kitchensink-ts",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "version": "4.9.2",
 | 
			
		||||
  "version": "4.10.1",
 | 
			
		||||
  "description": "A Strapi application",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "develop": "strapi develop",
 | 
			
		||||
@ -10,10 +10,10 @@
 | 
			
		||||
    "strapi": "strapi"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@strapi/plugin-i18n": "4.9.2",
 | 
			
		||||
    "@strapi/plugin-users-permissions": "4.9.2",
 | 
			
		||||
    "@strapi/strapi": "4.9.2",
 | 
			
		||||
    "better-sqlite3": "8.0.1"
 | 
			
		||||
    "@strapi/plugin-i18n": "4.10.1",
 | 
			
		||||
    "@strapi/plugin-users-permissions": "4.10.1",
 | 
			
		||||
    "@strapi/strapi": "4.10.1",
 | 
			
		||||
    "better-sqlite3": "8.3.0"
 | 
			
		||||
  },
 | 
			
		||||
  "author": {
 | 
			
		||||
    "name": "A Strapi developer"
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "kitchensink",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "version": "4.9.2",
 | 
			
		||||
  "version": "4.10.1",
 | 
			
		||||
  "description": "A Strapi application.",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "develop": "strapi develop",
 | 
			
		||||
@ -12,10 +12,10 @@
 | 
			
		||||
    "strapi": "strapi"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@strapi/provider-email-mailgun": "4.9.2",
 | 
			
		||||
    "@strapi/provider-upload-aws-s3": "4.9.2",
 | 
			
		||||
    "@strapi/provider-upload-cloudinary": "4.9.2",
 | 
			
		||||
    "@strapi/strapi": "4.9.2",
 | 
			
		||||
    "@strapi/provider-email-mailgun": "4.10.1",
 | 
			
		||||
    "@strapi/provider-upload-aws-s3": "4.10.1",
 | 
			
		||||
    "@strapi/provider-upload-cloudinary": "4.10.1",
 | 
			
		||||
    "@strapi/strapi": "4.10.1",
 | 
			
		||||
    "lodash": "4.17.21",
 | 
			
		||||
    "mysql": "2.18.1",
 | 
			
		||||
    "passport-google-oauth2": "0.2.0",
 | 
			
		||||
 | 
			
		||||
@ -5,12 +5,9 @@ const path = require('path');
 | 
			
		||||
const IS_EE = process.env.IS_EE === 'true';
 | 
			
		||||
 | 
			
		||||
const moduleNameMapper = {
 | 
			
		||||
  '.*\\.(css|less|styl|scss|sass)$': path.join(
 | 
			
		||||
    __dirname,
 | 
			
		||||
    'packages/admin-test-utils/lib/mocks/cssModule.js'
 | 
			
		||||
  ),
 | 
			
		||||
  '.*\\.(css|less|styl|scss|sass)$': '@strapi/admin-test-utils/file-mock',
 | 
			
		||||
  '.*\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|ico)$':
 | 
			
		||||
    path.join(__dirname, 'packages/admin-test-utils/lib/mocks/image.js'),
 | 
			
		||||
    '@strapi/admin-test-utils/file-mock',
 | 
			
		||||
  '^ee_else_ce(/.*)$': IS_EE
 | 
			
		||||
    ? [
 | 
			
		||||
        path.join(__dirname, 'packages/core/admin/ee/admin$1'),
 | 
			
		||||
@ -34,21 +31,9 @@ module.exports = {
 | 
			
		||||
  rootDir: __dirname,
 | 
			
		||||
  moduleNameMapper,
 | 
			
		||||
  testPathIgnorePatterns: ['/node_modules/', '__tests__'],
 | 
			
		||||
  globalSetup: path.join(__dirname, 'test/config/front/global-setup.js'),
 | 
			
		||||
  setupFiles: [
 | 
			
		||||
    path.join(__dirname, 'packages/admin-test-utils/lib/setup/test-bundler.js'),
 | 
			
		||||
    path.join(__dirname, 'packages/admin-test-utils/lib/mocks/fetch.js'),
 | 
			
		||||
    path.join(__dirname, 'packages/admin-test-utils/lib/mocks/LocalStorageMock.js'),
 | 
			
		||||
    path.join(__dirname, 'packages/admin-test-utils/lib/mocks/IntersectionObserver.js'),
 | 
			
		||||
    path.join(__dirname, 'packages/admin-test-utils/lib/mocks/ResizeObserver.js'),
 | 
			
		||||
    path.join(__dirname, 'packages/admin-test-utils/lib/mocks/windowMatchMedia.js'),
 | 
			
		||||
    path.join(__dirname, 'packages/admin-test-utils/lib/mocks/mockRangeApi.js'),
 | 
			
		||||
  ],
 | 
			
		||||
  setupFilesAfterEnv: [
 | 
			
		||||
    path.join(__dirname, '/packages/admin-test-utils/lib/setup/styled-components.js'),
 | 
			
		||||
    path.join(__dirname, '/packages/admin-test-utils/lib/setup/strapi.js'),
 | 
			
		||||
    path.join(__dirname, '/packages/admin-test-utils/lib/setup/prop-types.js'),
 | 
			
		||||
  ],
 | 
			
		||||
  globalSetup: '@strapi/admin-test-utils/global-setup',
 | 
			
		||||
  setupFiles: ['@strapi/admin-test-utils/environment'],
 | 
			
		||||
  setupFilesAfterEnv: ['@strapi/admin-test-utils/after-env'],
 | 
			
		||||
  testEnvironment: 'jsdom',
 | 
			
		||||
  transform: {
 | 
			
		||||
    '^.+\\.js$': [
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								jest.config.front.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								jest.config.front.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
/** @type {import('jest').Config} */
 | 
			
		||||
const config = {
 | 
			
		||||
  projects: [
 | 
			
		||||
    '<rootDir>/packages/plugins/*/jest.config.front.js',
 | 
			
		||||
    '<rootDir>/packages/core/*/jest.config.front.js',
 | 
			
		||||
    '<rootDir>/scripts/*/jest.config.front.js',
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = config;
 | 
			
		||||
							
								
								
									
										15
									
								
								jest.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								jest.config.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
/** @type {import('jest').Config} */
 | 
			
		||||
const config = {
 | 
			
		||||
  projects: [
 | 
			
		||||
    '<rootDir>/packages/plugins/*/jest.config.js',
 | 
			
		||||
    '<rootDir>/packages/utils/*/jest.config.js',
 | 
			
		||||
    '<rootDir>/packages/generators/*/jest.config.js',
 | 
			
		||||
    '<rootDir>/packages/core/*/jest.config.js',
 | 
			
		||||
    '<rootDir>/packages/providers/*/jest.config.js',
 | 
			
		||||
    '<rootDir>/.github/actions/*/jest.config.js',
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = config;
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
  "version": "4.9.2",
 | 
			
		||||
  "version": "4.10.1",
 | 
			
		||||
  "packages": ["packages/*", "examples/*"],
 | 
			
		||||
  "npmClient": "yarn",
 | 
			
		||||
  "useWorkspaces": true,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										50
									
								
								lint-staged.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								lint-staged.config.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const fs = require('fs');
 | 
			
		||||
const findUp = require('find-up');
 | 
			
		||||
 | 
			
		||||
const includes = ['packages', '.github'];
 | 
			
		||||
 | 
			
		||||
const root = path.resolve(__dirname);
 | 
			
		||||
 | 
			
		||||
function extractPackageName(pkgJsonPath) {
 | 
			
		||||
  return JSON.parse(fs.readFileSync(pkgJsonPath).toString()).name;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getLintCommand(files) {
 | 
			
		||||
  const affectedFolders = new Set();
 | 
			
		||||
 | 
			
		||||
  for (const file of files) {
 | 
			
		||||
    const r = findUp.sync('package.json', { cwd: file });
 | 
			
		||||
    const relPath = path.relative(root, r);
 | 
			
		||||
 | 
			
		||||
    if (includes.some((incl) => relPath.startsWith(incl))) {
 | 
			
		||||
      affectedFolders.add(r);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const affectedPackages = [...affectedFolders].map(extractPackageName);
 | 
			
		||||
 | 
			
		||||
  if (affectedPackages.length === 0) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  return `nx run-many -t lint -p ${affectedPackages.join()}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCodeCommands(files) {
 | 
			
		||||
  const lintCmd = getLintCommand(files);
 | 
			
		||||
 | 
			
		||||
  const prettierCmd = `prettier --write ${files.join(' ')}`;
 | 
			
		||||
 | 
			
		||||
  if (lintCmd) {
 | 
			
		||||
    return [lintCmd, prettierCmd];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [prettierCmd];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  '*.{js,ts}': getCodeCommands,
 | 
			
		||||
  '*.{md,css,scss,yaml,yml}': ['prettier --write'],
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										9
									
								
								nx.json
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								nx.json
									
									
									
									
									
								
							@ -29,12 +29,10 @@
 | 
			
		||||
      "dependsOn": ["^build:ts"]
 | 
			
		||||
    },
 | 
			
		||||
    "test:unit": {
 | 
			
		||||
      "inputs": ["default", "{workspaceRoot}/jest-preset.unit.js"],
 | 
			
		||||
      "dependsOn": ["build:ts"]
 | 
			
		||||
      "inputs": ["default", "{workspaceRoot}/jest-preset.unit.js"]
 | 
			
		||||
    },
 | 
			
		||||
    "test:front": {
 | 
			
		||||
      "inputs": ["default", "{workspaceRoot}/jest-preset.front.js"],
 | 
			
		||||
      "dependsOn": ["^build"]
 | 
			
		||||
      "inputs": ["default", "{workspaceRoot}/jest-preset.front.js"]
 | 
			
		||||
    },
 | 
			
		||||
    "lint": {
 | 
			
		||||
      "inputs": [
 | 
			
		||||
@ -44,8 +42,7 @@
 | 
			
		||||
        "{projectRoot}/.eslintignore",
 | 
			
		||||
        "{projectRoot}/tsconfig.eslint.json",
 | 
			
		||||
        "{workspaceRoot}/packages/utils/eslint-config-custom/**/*"
 | 
			
		||||
      ],
 | 
			
		||||
      "dependsOn": ["build:ts"]
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "tasksRunnerOptions": {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										32
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								package.json
									
									
									
									
									
								
							@ -45,37 +45,36 @@
 | 
			
		||||
    "format:other": "yarn prettier:other --write",
 | 
			
		||||
    "prettier:code": "prettier --cache --cache-strategy content \"**/*.{js,ts}\"",
 | 
			
		||||
    "prettier:other": "prettier --cache --cache-strategy content \"**/*.{md,css,scss,yaml,yml}\"",
 | 
			
		||||
    "test:front": "cross-env IS_EE=true nx run-many --target=test:front --nx-ignore-cycles",
 | 
			
		||||
    "test:front:watch": "cross-env IS_EE=true nx run-many --target=test:front:watch --nx-ignore-cycles",
 | 
			
		||||
    "test:front:update": "yarn test:front -u",
 | 
			
		||||
    "test:front:ce": "cross-env IS_EE=false nx run-many --target=test:front --nx-ignore-cycles",
 | 
			
		||||
    "test:front:watch:ce": "cross-env IS_EE=false nx run-many --target=test:front:watch --nx-ignore-cycles",
 | 
			
		||||
    "test:front:all": "cross-env IS_EE=true nx run-many --target=test:front --nx-ignore-cycles",
 | 
			
		||||
    "test:front": "cross-env IS_EE=true jest --config jest.config.front.js",
 | 
			
		||||
    "test:front:watch": "cross-env IS_EE=true run test:front --watch",
 | 
			
		||||
    "test:front:update": "run test:front -u",
 | 
			
		||||
    "test:front:all:ce": "cross-env IS_EE=false nx run-many --target=test:front:ce --nx-ignore-cycles",
 | 
			
		||||
    "test:front:ce": "cross-env IS_EE=false run test:front",
 | 
			
		||||
    "test:front:watch:ce": "cross-env IS_EE=false run test:front --watch",
 | 
			
		||||
    "test:front:update:ce": "yarn test:front:ce -u",
 | 
			
		||||
    "test:unit": "nx run-many --target=test:unit --nx-ignore-cycles",
 | 
			
		||||
    "test:unit:watch": "nx run-many --target=test:unit:watch --nx-ignore-cycles",
 | 
			
		||||
    "test:unit:all": "nx run-many --target=test:unit --nx-ignore-cycles",
 | 
			
		||||
    "test:unit": "jest --config jest.config.js",
 | 
			
		||||
    "test:unit:watch": "run test:unit --watch",
 | 
			
		||||
    "test:api": "node test/api.js",
 | 
			
		||||
    "test:generate-app": "node test/create-test-app.js",
 | 
			
		||||
    "doc:api": "node scripts/open-api/serve.js"
 | 
			
		||||
  },
 | 
			
		||||
  "lint-staged": {
 | 
			
		||||
    "*.{js,ts,md,css,scss,yaml,yml}": [
 | 
			
		||||
      "prettier --write"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/core": "^7.20.12",
 | 
			
		||||
    "@babel/eslint-parser": "^7.19.1",
 | 
			
		||||
    "@babel/preset-react": "7.18.6",
 | 
			
		||||
    "@strapi/admin-test-utils": "workspace:*",
 | 
			
		||||
    "@strapi/eslint-config": "0.1.2",
 | 
			
		||||
    "@swc/cli": "0.1.62",
 | 
			
		||||
    "@swc/core": "1.3.37",
 | 
			
		||||
    "@swc/jest": "0.2.24",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^5.55.0",
 | 
			
		||||
    "@typescript-eslint/parser": "5.43.0",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "5.59.1",
 | 
			
		||||
    "@typescript-eslint/parser": "5.59.1",
 | 
			
		||||
    "babel-eslint": "10.1.0",
 | 
			
		||||
    "chalk": "4.1.2",
 | 
			
		||||
    "chokidar": "3.5.3",
 | 
			
		||||
    "core-js": "3.28.0",
 | 
			
		||||
    "core-js": "3.30.1",
 | 
			
		||||
    "cross-env": "7.0.3",
 | 
			
		||||
    "dotenv": "14.2.0",
 | 
			
		||||
    "eslint": "8.27.0",
 | 
			
		||||
@ -90,6 +89,7 @@
 | 
			
		||||
    "eslint-plugin-react": "^7.32.2",
 | 
			
		||||
    "eslint-plugin-react-hooks": "^4.6.0",
 | 
			
		||||
    "execa": "1.0.0",
 | 
			
		||||
    "find-up": "5.0.0",
 | 
			
		||||
    "fs-extra": "10.1.0",
 | 
			
		||||
    "get-port": "5.1.1",
 | 
			
		||||
    "glob": "7.2.3",
 | 
			
		||||
@ -97,7 +97,7 @@
 | 
			
		||||
    "inquirer": "8.2.5",
 | 
			
		||||
    "jest": "29.0.3",
 | 
			
		||||
    "jest-circus": "29.0.3",
 | 
			
		||||
    "jest-cli": "29.0.3",
 | 
			
		||||
    "jest-cli": "29.5.0",
 | 
			
		||||
    "jest-environment-jsdom": "29.0.3",
 | 
			
		||||
    "jest-watch-typeahead": "2.2.2",
 | 
			
		||||
    "lerna": "6.5.1",
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,3 @@
 | 
			
		||||
node_modules/
 | 
			
		||||
dist/
 | 
			
		||||
.eslintrc.js
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,4 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  root: true,
 | 
			
		||||
  extends: ['custom/back'],
 | 
			
		||||
  rules: {
 | 
			
		||||
    'import/no-extraneous-dependencies': 'off',
 | 
			
		||||
  },
 | 
			
		||||
  extends: ['custom/typescript'],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										99
									
								
								packages/admin-test-utils/.gitignore
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										99
									
								
								packages/admin-test-utils/.gitignore
									
									
									
									
										vendored
									
									
										Executable file
									
								
							@ -0,0 +1,99 @@
 | 
			
		||||
############################
 | 
			
		||||
# OS X
 | 
			
		||||
############################
 | 
			
		||||
 | 
			
		||||
.DS_Store
 | 
			
		||||
.AppleDouble
 | 
			
		||||
.LSOverride
 | 
			
		||||
Icon
 | 
			
		||||
.Spotlight-V100
 | 
			
		||||
.Trashes
 | 
			
		||||
._*
 | 
			
		||||
 | 
			
		||||
############################
 | 
			
		||||
# Linux
 | 
			
		||||
############################
 | 
			
		||||
 | 
			
		||||
*~
 | 
			
		||||
 | 
			
		||||
############################
 | 
			
		||||
# Windows
 | 
			
		||||
############################
 | 
			
		||||
 | 
			
		||||
Thumbs.db
 | 
			
		||||
ehthumbs.db
 | 
			
		||||
Desktop.ini
 | 
			
		||||
$RECYCLE.BIN/
 | 
			
		||||
*.cab
 | 
			
		||||
*.msi
 | 
			
		||||
*.msm
 | 
			
		||||
*.msp
 | 
			
		||||
 | 
			
		||||
############################
 | 
			
		||||
# Packages
 | 
			
		||||
############################
 | 
			
		||||
 | 
			
		||||
*.7z
 | 
			
		||||
*.csv
 | 
			
		||||
*.dat
 | 
			
		||||
*.dmg
 | 
			
		||||
*.gz
 | 
			
		||||
*.iso
 | 
			
		||||
*.jar
 | 
			
		||||
*.rar
 | 
			
		||||
*.tar
 | 
			
		||||
*.zip
 | 
			
		||||
*.com
 | 
			
		||||
*.class
 | 
			
		||||
*.dll
 | 
			
		||||
*.exe
 | 
			
		||||
*.o
 | 
			
		||||
*.seed
 | 
			
		||||
*.so
 | 
			
		||||
*.swo
 | 
			
		||||
*.swp
 | 
			
		||||
*.swn
 | 
			
		||||
*.swm
 | 
			
		||||
*.out
 | 
			
		||||
*.pid
 | 
			
		||||
 | 
			
		||||
############################
 | 
			
		||||
# Logs and databases
 | 
			
		||||
############################
 | 
			
		||||
 | 
			
		||||
.tmp
 | 
			
		||||
*.log
 | 
			
		||||
*.sql
 | 
			
		||||
*.sqlite
 | 
			
		||||
 | 
			
		||||
############################
 | 
			
		||||
# Misc.
 | 
			
		||||
############################
 | 
			
		||||
 | 
			
		||||
*#
 | 
			
		||||
.idea
 | 
			
		||||
nbproject
 | 
			
		||||
 | 
			
		||||
############################
 | 
			
		||||
# Node.js
 | 
			
		||||
############################
 | 
			
		||||
 | 
			
		||||
lib-cov
 | 
			
		||||
lcov.info
 | 
			
		||||
pids
 | 
			
		||||
logs
 | 
			
		||||
results
 | 
			
		||||
build
 | 
			
		||||
node_modules
 | 
			
		||||
.node_history
 | 
			
		||||
package-lock.json
 | 
			
		||||
 | 
			
		||||
############################
 | 
			
		||||
# Tests
 | 
			
		||||
############################
 | 
			
		||||
 | 
			
		||||
testApp
 | 
			
		||||
coverage
 | 
			
		||||
 | 
			
		||||
dist/
 | 
			
		||||
docs/
 | 
			
		||||
							
								
								
									
										22
									
								
								packages/admin-test-utils/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								packages/admin-test-utils/LICENSE
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
Copyright (c) 2015-present Strapi Solutions SAS
 | 
			
		||||
 | 
			
		||||
Portions of the Strapi software are licensed as follows:
 | 
			
		||||
 | 
			
		||||
* All software that resides under an "ee/" directory (the “EE Software”), if that directory exists, is licensed under the license defined in "ee/LICENSE".
 | 
			
		||||
 | 
			
		||||
* All software outside of the above-mentioned directories or restrictions above is available under the "MIT Expat" license as set forth below.
 | 
			
		||||
 | 
			
		||||
MIT Expat License
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
in the Software without restriction, including without limitation the rights
 | 
			
		||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
furnished to do so, subject to the following conditions:
 | 
			
		||||
 | 
			
		||||
The above copyright notice and this permission notice shall be included in all
 | 
			
		||||
copies or substantial portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
			
		||||
SOFTWARE.
 | 
			
		||||
							
								
								
									
										15
									
								
								packages/admin-test-utils/custom.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								packages/admin-test-utils/custom.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
export {};
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface Window {
 | 
			
		||||
    strapi: {
 | 
			
		||||
      backendURL: string;
 | 
			
		||||
      isEE: boolean;
 | 
			
		||||
      features: {
 | 
			
		||||
        SSO: 'sso';
 | 
			
		||||
        isEnabled: (featureName?: string) => boolean;
 | 
			
		||||
      };
 | 
			
		||||
      projectType: string;
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,16 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const adminPermissions = require('./permissions/admin-permissions');
 | 
			
		||||
const cmPermissions = require('./permissions/content-manager-permissions');
 | 
			
		||||
const ctbPermissions = require('./permissions/content-type-builder-permissions');
 | 
			
		||||
const store = require('./store');
 | 
			
		||||
 | 
			
		||||
const permissions = [...adminPermissions, ...cmPermissions, ...ctbPermissions];
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  adminPermissions,
 | 
			
		||||
  cmPermissions,
 | 
			
		||||
  ctbPermissions,
 | 
			
		||||
  permissions,
 | 
			
		||||
  store,
 | 
			
		||||
};
 | 
			
		||||
@ -1,7 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const fixtures = require('./fixtures');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  fixtures,
 | 
			
		||||
};
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class IntersectionObserverMock {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.root = null;
 | 
			
		||||
    this.rootMargin = '';
 | 
			
		||||
    this.thresholds = [];
 | 
			
		||||
    this.disconnect = () => null;
 | 
			
		||||
    this.observe = () => null;
 | 
			
		||||
    this.takeRecords = () => [];
 | 
			
		||||
    this.unobserve = () => null;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
global.IntersectionObserver = IntersectionObserverMock;
 | 
			
		||||
@ -1,37 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class LocalStorageMock {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.store = new Map();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clear() {
 | 
			
		||||
    this.store.clear();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getItem(key) {
 | 
			
		||||
    /**
 | 
			
		||||
     * We return null to avoid returning `undefined`
 | 
			
		||||
     * because `undefined` is not a valid JSON value.
 | 
			
		||||
     */
 | 
			
		||||
    return this.store.get(key) ?? null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setItem(key, value) {
 | 
			
		||||
    this.store.set(key, String(value));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeItem(key) {
 | 
			
		||||
    this.store.delete(key);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get length() {
 | 
			
		||||
    return this.store.size;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-undef
 | 
			
		||||
Object.defineProperty(window, 'localStorage', {
 | 
			
		||||
  writable: true,
 | 
			
		||||
  value: new LocalStorageMock(),
 | 
			
		||||
});
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class ResizeObserverMock {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.disconnect = () => null;
 | 
			
		||||
    this.observe = () => null;
 | 
			
		||||
    this.unobserve = () => null;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
global.ResizeObserver = ResizeObserverMock;
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
// module.exports = 'CSS_MODULE';
 | 
			
		||||
@ -1,4 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
// Required as long as we are running tests on node@14 and node@16
 | 
			
		||||
require('whatwg-fetch');
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
module.exports = 'IMAGE_MOCK';
 | 
			
		||||
@ -1,16 +0,0 @@
 | 
			
		||||
/* eslint-disable no-undef */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
// Codemirror inner dependency, reference: https://github.com/jsdom/jsdom/issues/3002
 | 
			
		||||
// Otherwise it throws: TypeError: range(...).getBoundingClientRect is not a function
 | 
			
		||||
 | 
			
		||||
document.createRange = () => {
 | 
			
		||||
  const range = new Range();
 | 
			
		||||
  range.getClientRects = jest.fn(() => ({
 | 
			
		||||
    item: () => null,
 | 
			
		||||
    length: 0,
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  return range;
 | 
			
		||||
};
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
global.window.matchMedia = jest.fn(() => false);
 | 
			
		||||
@ -1,21 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const { format } = require('util');
 | 
			
		||||
 | 
			
		||||
const originalConsoleError = console.error;
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  console.error = (...args) => {
 | 
			
		||||
    originalConsoleError(...args);
 | 
			
		||||
 | 
			
		||||
    const message = format(...args);
 | 
			
		||||
 | 
			
		||||
    if (/(Invalid prop|Failed prop type)/gi.test(message)) {
 | 
			
		||||
      throw new Error(message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
afterEach(() => {
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
});
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * Strapi
 | 
			
		||||
 * This file allow to mock any key that is in the global strapi variable
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// FIXME create a better jest setup
 | 
			
		||||
require('@testing-library/jest-dom/extend-expect');
 | 
			
		||||
 | 
			
		||||
global.process.env.ADMIN_PATH = '/admin/';
 | 
			
		||||
 | 
			
		||||
global.strapi = {
 | 
			
		||||
  backendURL: 'http://localhost:1337',
 | 
			
		||||
  isEE: false,
 | 
			
		||||
  features: {
 | 
			
		||||
    SSO: 'sso',
 | 
			
		||||
    isEnabled: () => false,
 | 
			
		||||
  },
 | 
			
		||||
  projectType: 'Community',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
global.prompt = jest.fn();
 | 
			
		||||
 | 
			
		||||
global.URL.createObjectURL = (file) => `http://localhost:4000/assets/${file.name}`;
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
require('jest-styled-components');
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const noop = () => {};
 | 
			
		||||
// eslint-disable-next-line no-undef
 | 
			
		||||
Object.defineProperty(window, 'scrollTo', { value: noop, writable: true });
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@strapi/admin-test-utils",
 | 
			
		||||
  "version": "4.9.2",
 | 
			
		||||
  "version": "4.10.1",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "description": "Test utilities for the Strapi administration panel",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
@ -16,21 +16,43 @@
 | 
			
		||||
      "url": "https://strapi.io"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "main": "lib/index.js",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
  "main": "./dist/index.js",
 | 
			
		||||
  "types": "./dist/index.d.ts",
 | 
			
		||||
  "exports": {
 | 
			
		||||
    ".": {
 | 
			
		||||
      "require": "./dist/index.js"
 | 
			
		||||
    },
 | 
			
		||||
    "./after-env": {
 | 
			
		||||
      "require": "./dist/after-env.js"
 | 
			
		||||
    },
 | 
			
		||||
    "./environment": {
 | 
			
		||||
      "require": "./dist/environment.js"
 | 
			
		||||
    },
 | 
			
		||||
    "./file-mock": {
 | 
			
		||||
      "require": "./dist/file-mock.js"
 | 
			
		||||
    },
 | 
			
		||||
    "./global-setup": {
 | 
			
		||||
      "require": "./dist/global-setup.js"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@juggle/resize-observer": "3.4.0",
 | 
			
		||||
    "@testing-library/jest-dom": "5.16.5",
 | 
			
		||||
    "jest-styled-components": "7.1.1",
 | 
			
		||||
    "react": "^17.0.2",
 | 
			
		||||
    "react-dom": "^17.0.2",
 | 
			
		||||
    "react-is": "^17.0.2",
 | 
			
		||||
    "jest-styled-components": "7.1.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "eslint-config-custom": "4.10.1",
 | 
			
		||||
    "redux": "^4.2.1",
 | 
			
		||||
    "styled-components": "5.3.3",
 | 
			
		||||
    "whatwg-fetch": "3.6.2"
 | 
			
		||||
    "tsconfig": "4.10.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "redux": "^4.2.1"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "build": "run -T tsc",
 | 
			
		||||
    "build:ts": "run -T tsc",
 | 
			
		||||
    "watch": "run -T tsc -w --preserveWatchOutput",
 | 
			
		||||
    "clean": "run -T rimraf ./dist",
 | 
			
		||||
    "lint": "run -T eslint ."
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								packages/admin-test-utils/src/after-env.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/admin-test-utils/src/after-env.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
import '@testing-library/jest-dom';
 | 
			
		||||
import 'jest-styled-components';
 | 
			
		||||
							
								
								
									
										202
									
								
								packages/admin-test-utils/src/environment.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								packages/admin-test-utils/src/environment.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,202 @@
 | 
			
		||||
import { ResizeObserver } from '@juggle/resize-observer';
 | 
			
		||||
import { format } from 'util';
 | 
			
		||||
 | 
			
		||||
/* -------------------------------------------------------------------------------------------------
 | 
			
		||||
 * IntersectionObserver
 | 
			
		||||
 * -----------------------------------------------------------------------------------------------*/
 | 
			
		||||
 | 
			
		||||
const mockIntersectionObserver = jest.fn();
 | 
			
		||||
mockIntersectionObserver.mockReturnValue({
 | 
			
		||||
  observe: () => null,
 | 
			
		||||
  unobserve: () => null,
 | 
			
		||||
  disconnect: () => null,
 | 
			
		||||
});
 | 
			
		||||
window.IntersectionObserver = mockIntersectionObserver;
 | 
			
		||||
 | 
			
		||||
/* -------------------------------------------------------------------------------------------------
 | 
			
		||||
 * ResizeObserver
 | 
			
		||||
 * -----------------------------------------------------------------------------------------------*/
 | 
			
		||||
 | 
			
		||||
window.ResizeObserver = ResizeObserver;
 | 
			
		||||
 | 
			
		||||
/* -------------------------------------------------------------------------------------------------
 | 
			
		||||
 * ResizeObserver
 | 
			
		||||
 * -----------------------------------------------------------------------------------------------*/
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * If there's a prop type error then we want to throw an
 | 
			
		||||
 * error so that the test fails.
 | 
			
		||||
 *
 | 
			
		||||
 * NOTE: This can be removed once we move to a typescript
 | 
			
		||||
 * setup & we throw tests on type errors.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const error = console.error;
 | 
			
		||||
window.console = {
 | 
			
		||||
  ...window.console,
 | 
			
		||||
  error(...args: any[]) {
 | 
			
		||||
    error(...args);
 | 
			
		||||
 | 
			
		||||
    const message = format(...args);
 | 
			
		||||
 | 
			
		||||
    if (/(Invalid prop|Failed prop type)/gi.test(message)) {
 | 
			
		||||
      throw new Error(message);
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/* -------------------------------------------------------------------------------------------------
 | 
			
		||||
 * Strapi
 | 
			
		||||
 * -----------------------------------------------------------------------------------------------*/
 | 
			
		||||
 | 
			
		||||
window.strapi = {
 | 
			
		||||
  backendURL: 'http://localhost:1337',
 | 
			
		||||
  isEE: false,
 | 
			
		||||
  features: {
 | 
			
		||||
    SSO: 'sso',
 | 
			
		||||
    isEnabled: () => false,
 | 
			
		||||
  },
 | 
			
		||||
  projectType: 'Community',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/* -------------------------------------------------------------------------------------------------
 | 
			
		||||
 * matchMedia
 | 
			
		||||
 * -----------------------------------------------------------------------------------------------*/
 | 
			
		||||
 | 
			
		||||
Object.defineProperty(window, 'matchMedia', {
 | 
			
		||||
  writable: true,
 | 
			
		||||
  value: jest.fn().mockImplementation((query) => ({
 | 
			
		||||
    matches: false,
 | 
			
		||||
    media: query,
 | 
			
		||||
    onchange: null,
 | 
			
		||||
    /**
 | 
			
		||||
     * @deprecated
 | 
			
		||||
     */
 | 
			
		||||
    addListener: jest.fn(),
 | 
			
		||||
    /**
 | 
			
		||||
     * @deprecated
 | 
			
		||||
     */
 | 
			
		||||
    removeListener: jest.fn(),
 | 
			
		||||
    addEventListener: jest.fn(),
 | 
			
		||||
    removeEventListener: jest.fn(),
 | 
			
		||||
    dispatchEvent: jest.fn(),
 | 
			
		||||
  })),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/* -------------------------------------------------------------------------------------------------
 | 
			
		||||
 * scrollTo
 | 
			
		||||
 * -----------------------------------------------------------------------------------------------*/
 | 
			
		||||
 | 
			
		||||
Object.defineProperty(window, 'scrollTo', {
 | 
			
		||||
  writable: true,
 | 
			
		||||
  value: jest.fn(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/* -------------------------------------------------------------------------------------------------
 | 
			
		||||
 * prompt
 | 
			
		||||
 * -----------------------------------------------------------------------------------------------*/
 | 
			
		||||
 | 
			
		||||
Object.defineProperty(window, 'prompt', {
 | 
			
		||||
  writable: true,
 | 
			
		||||
  value: jest.fn(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/* -------------------------------------------------------------------------------------------------
 | 
			
		||||
 * URL
 | 
			
		||||
 * -----------------------------------------------------------------------------------------------*/
 | 
			
		||||
 | 
			
		||||
window.URL.createObjectURL = jest
 | 
			
		||||
  .fn()
 | 
			
		||||
  .mockImplementation((file) => `http://localhost:4000/assets/${file.name}`);
 | 
			
		||||
 | 
			
		||||
/* -------------------------------------------------------------------------------------------------
 | 
			
		||||
 * createRange
 | 
			
		||||
 * -----------------------------------------------------------------------------------------------*/
 | 
			
		||||
 | 
			
		||||
document.createRange = () => {
 | 
			
		||||
  const range = new Range();
 | 
			
		||||
  range.getClientRects = jest.fn(() => ({
 | 
			
		||||
    item: () => null,
 | 
			
		||||
    length: 0,
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  return range;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/* -------------------------------------------------------------------------------------------------
 | 
			
		||||
 * localStorage
 | 
			
		||||
 * -----------------------------------------------------------------------------------------------*/
 | 
			
		||||
 | 
			
		||||
class LocalStorageMock {
 | 
			
		||||
  store: Map<string, string>;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.store = new Map();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clear() {
 | 
			
		||||
    this.store.clear();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getItem(key: string) {
 | 
			
		||||
    /**
 | 
			
		||||
     * We return null to avoid returning `undefined`
 | 
			
		||||
     * because `undefined` is not a valid JSON value.
 | 
			
		||||
     */
 | 
			
		||||
    return this.store.get(key) ?? null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setItem(key: string, value: unknown) {
 | 
			
		||||
    this.store.set(key, String(value));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeItem(key: string) {
 | 
			
		||||
    this.store.delete(key);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get length() {
 | 
			
		||||
    return this.store.size;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Object.defineProperty(window, 'localStorage', {
 | 
			
		||||
  writable: true,
 | 
			
		||||
  value: new LocalStorageMock(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/* -------------------------------------------------------------------------------------------------
 | 
			
		||||
 * PointerEvents
 | 
			
		||||
 * -----------------------------------------------------------------------------------------------*/
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * JSDOM doesn't implement PointerEvent so we need to mock our own implementation
 | 
			
		||||
 * Default to mouse left click interaction
 | 
			
		||||
 * https://github.com/radix-ui/primitives/issues/1822
 | 
			
		||||
 * https://github.com/jsdom/jsdom/pull/2666
 | 
			
		||||
 */
 | 
			
		||||
class MockPointerEvent extends Event {
 | 
			
		||||
  button: number;
 | 
			
		||||
 | 
			
		||||
  ctrlKey: boolean;
 | 
			
		||||
 | 
			
		||||
  pointerType: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    type: string,
 | 
			
		||||
    props: EventInit & { button?: number; ctrlKey?: boolean; pointerType?: string }
 | 
			
		||||
  ) {
 | 
			
		||||
    super(type, props);
 | 
			
		||||
    this.button = props.button || 0;
 | 
			
		||||
    this.ctrlKey = props.ctrlKey || false;
 | 
			
		||||
    this.pointerType = props.pointerType || 'mouse';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Object.defineProperty(window, 'PointerEvent', {
 | 
			
		||||
  writable: true,
 | 
			
		||||
  value: MockPointerEvent,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
 | 
			
		||||
window.HTMLElement.prototype.releasePointerCapture = jest.fn();
 | 
			
		||||
window.HTMLElement.prototype.hasPointerCapture = jest.fn();
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/admin-test-utils/src/file-mock.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/admin-test-utils/src/file-mock.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
export default 'IMAGE_MOCK';
 | 
			
		||||
@ -1,6 +1,4 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const addressCT = {
 | 
			
		||||
const address = {
 | 
			
		||||
  uid: 'api::address.address',
 | 
			
		||||
  settings: {
 | 
			
		||||
    bulkable: true,
 | 
			
		||||
@ -421,4 +419,6 @@ const addressCT = {
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = addressCT;
 | 
			
		||||
type Address = typeof address;
 | 
			
		||||
 | 
			
		||||
export { address, Address };
 | 
			
		||||
@ -0,0 +1,3 @@
 | 
			
		||||
import { address, Address } from './address';
 | 
			
		||||
 | 
			
		||||
export { Address, address };
 | 
			
		||||
							
								
								
									
										6
									
								
								packages/admin-test-utils/src/fixtures/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								packages/admin-test-utils/src/fixtures/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
import store from './store';
 | 
			
		||||
 | 
			
		||||
export * as collectionTypes from './collection-types';
 | 
			
		||||
export * as metaData from './meta-data';
 | 
			
		||||
export * as permissions from './permissions';
 | 
			
		||||
export { store };
 | 
			
		||||
@ -1,6 +1,4 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const addressMetaData = {
 | 
			
		||||
const address = {
 | 
			
		||||
  id: { edit: {}, list: { label: 'Id', searchable: true, sortable: true } },
 | 
			
		||||
  postal_coder: {
 | 
			
		||||
    edit: {
 | 
			
		||||
@ -98,4 +96,6 @@ const addressMetaData = {
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = addressMetaData;
 | 
			
		||||
type Address = typeof address;
 | 
			
		||||
 | 
			
		||||
export { address, Address };
 | 
			
		||||
@ -0,0 +1,3 @@
 | 
			
		||||
import { address, Address } from './address';
 | 
			
		||||
 | 
			
		||||
export { Address, address };
 | 
			
		||||
@ -1,6 +1,4 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const adminPermissions = [
 | 
			
		||||
const admin = [
 | 
			
		||||
  {
 | 
			
		||||
    id: 169,
 | 
			
		||||
    action: 'admin::provider-login.read',
 | 
			
		||||
@ -108,4 +106,6 @@ const adminPermissions = [
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
module.exports = adminPermissions;
 | 
			
		||||
type Admin = typeof admin;
 | 
			
		||||
 | 
			
		||||
export { admin, Admin };
 | 
			
		||||
@ -1,6 +1,4 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const cmPermissions = [
 | 
			
		||||
const contentManager = [
 | 
			
		||||
  {
 | 
			
		||||
    id: 2817,
 | 
			
		||||
    action: 'plugin::content-manager.single-types.configure-view',
 | 
			
		||||
@ -60,4 +58,6 @@ const cmPermissions = [
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
module.exports = cmPermissions;
 | 
			
		||||
type ContentManager = typeof contentManager;
 | 
			
		||||
 | 
			
		||||
export { contentManager, ContentManager };
 | 
			
		||||
@ -1,6 +1,4 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const ctbPermissions = [
 | 
			
		||||
const contentTypeBuilder = [
 | 
			
		||||
  {
 | 
			
		||||
    id: 2820,
 | 
			
		||||
    action: 'plugin::content-type-builder.read',
 | 
			
		||||
@ -39,4 +37,6 @@ const ctbPermissions = [
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
module.exports = ctbPermissions;
 | 
			
		||||
type ContentTypeBuilder = typeof contentTypeBuilder;
 | 
			
		||||
 | 
			
		||||
export { contentTypeBuilder, ContentTypeBuilder };
 | 
			
		||||
							
								
								
									
										24
									
								
								packages/admin-test-utils/src/fixtures/permissions/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packages/admin-test-utils/src/fixtures/permissions/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
/**
 | 
			
		||||
 * TODO: These types could be done better, since they're mock data
 | 
			
		||||
 * for user-permissions plugin it might be better to extract them
 | 
			
		||||
 * from that package and use them here.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { admin, Admin } from './admin-permissions';
 | 
			
		||||
import { contentManager, ContentManager } from './content-manager-permissions';
 | 
			
		||||
import { contentTypeBuilder, ContentTypeBuilder } from './content-type-builder-permissions';
 | 
			
		||||
 | 
			
		||||
const allPermissions = [...admin, ...contentManager, ...contentTypeBuilder];
 | 
			
		||||
 | 
			
		||||
type AdminPermissions = typeof allPermissions;
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  admin,
 | 
			
		||||
  Admin,
 | 
			
		||||
  contentManager,
 | 
			
		||||
  ContentManager,
 | 
			
		||||
  contentTypeBuilder,
 | 
			
		||||
  ContentTypeBuilder,
 | 
			
		||||
  allPermissions,
 | 
			
		||||
  AdminPermissions,
 | 
			
		||||
};
 | 
			
		||||
@ -1,6 +1,4 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const { combineReducers, createStore } = require('redux');
 | 
			
		||||
import { combineReducers, createStore } from 'redux';
 | 
			
		||||
 | 
			
		||||
const reducers = {
 | 
			
		||||
  admin_app: jest.fn(() => ({ status: 'init' })),
 | 
			
		||||
@ -37,7 +35,7 @@ const reducers = {
 | 
			
		||||
 | 
			
		||||
const store = createStore(combineReducers(reducers));
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
export default {
 | 
			
		||||
  store,
 | 
			
		||||
  state: store.getState(),
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										7
									
								
								packages/admin-test-utils/src/global-setup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								packages/admin-test-utils/src/global-setup.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
const globalSetup = async () => {
 | 
			
		||||
  process.env.TZ = 'UTC';
 | 
			
		||||
  process.env.LANG = 'en_US.UTF-8';
 | 
			
		||||
  process.env.ADMIN_PATH = '/admin/';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default globalSetup;
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/admin-test-utils/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/admin-test-utils/src/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
export * as fixtures from './fixtures';
 | 
			
		||||
							
								
								
									
										6
									
								
								packages/admin-test-utils/tsconfig.eslint.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								packages/admin-test-utils/tsconfig.eslint.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "./tsconfig.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								packages/admin-test-utils/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								packages/admin-test-utils/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "tsconfig/base.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "outDir": "dist"
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src", "./custom.d.ts"],
 | 
			
		||||
  "exclude": ["node_modules", "**/__tests__/**"]
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "create-strapi-app",
 | 
			
		||||
  "version": "4.9.2",
 | 
			
		||||
  "version": "4.10.1",
 | 
			
		||||
  "description": "Generate a new Strapi application.",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@strapi/generate-new": "4.9.2",
 | 
			
		||||
    "@strapi/generate-new": "4.10.1",
 | 
			
		||||
    "commander": "8.3.0",
 | 
			
		||||
    "inquirer": "8.2.5"
 | 
			
		||||
  },
 | 
			
		||||
@ -49,8 +49,8 @@
 | 
			
		||||
    "lint": "run -T eslint ."
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "eslint-config-custom": "4.9.2",
 | 
			
		||||
    "tsconfig": "4.9.2"
 | 
			
		||||
    "eslint-config-custom": "4.10.1",
 | 
			
		||||
    "tsconfig": "4.10.1"
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">=14.19.1 <=18.x.x",
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "create-strapi-starter",
 | 
			
		||||
  "version": "4.9.2",
 | 
			
		||||
  "version": "4.10.1",
 | 
			
		||||
  "description": "Generate a new Strapi application.",
 | 
			
		||||
  "keywords": [
 | 
			
		||||
    "create-strapi-starter",
 | 
			
		||||
@ -44,7 +44,7 @@
 | 
			
		||||
    "lint": "run -T eslint ."
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@strapi/generate-new": "4.9.2",
 | 
			
		||||
    "@strapi/generate-new": "4.10.1",
 | 
			
		||||
    "chalk": "4.1.2",
 | 
			
		||||
    "ci-info": "3.8.0",
 | 
			
		||||
    "commander": "8.3.0",
 | 
			
		||||
@ -54,8 +54,8 @@
 | 
			
		||||
    "ora": "5.4.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "eslint-config-custom": "4.9.2",
 | 
			
		||||
    "tsconfig": "4.9.2"
 | 
			
		||||
    "eslint-config-custom": "4.10.1",
 | 
			
		||||
    "tsconfig": "4.10.1"
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">=14.19.1 <=18.x.x",
 | 
			
		||||
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
declare module '@strapi/generate-new';
 | 
			
		||||
@ -104,7 +104,7 @@ export default async function buildStarter(
 | 
			
		||||
  { projectName, starter }: { projectName: string; starter: string },
 | 
			
		||||
  program: Program
 | 
			
		||||
) {
 | 
			
		||||
  const hasYarnInstalled = await hasYarn();
 | 
			
		||||
  const hasYarnInstalled = hasYarn();
 | 
			
		||||
  const { isLocalStarter, starterPath, starterParentPath, starterPackageInfo } =
 | 
			
		||||
    await getStarterInfo(starter, { useYarn: hasYarnInstalled });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import execa from 'execa';
 | 
			
		||||
 | 
			
		||||
export default async function hasYarn() {
 | 
			
		||||
export default function hasYarn() {
 | 
			
		||||
  try {
 | 
			
		||||
    const { exitCode } = await execa.commandSync('yarn --version', { shell: true });
 | 
			
		||||
    const { exitCode } = execa.commandSync('yarn --version', { shell: true });
 | 
			
		||||
 | 
			
		||||
    if (exitCode === 0) return true;
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
import React, { useMemo, useState, useEffect, useRef } from 'react';
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
//  TODO: DS add loader
 | 
			
		||||
import {
 | 
			
		||||
  auth,
 | 
			
		||||
  LoadingIndicatorPage,
 | 
			
		||||
  AppInfosContext,
 | 
			
		||||
  AppInfoProvider,
 | 
			
		||||
  useGuidedTour,
 | 
			
		||||
  useNotification,
 | 
			
		||||
} from '@strapi/helper-plugin';
 | 
			
		||||
@ -27,7 +27,6 @@ const strapiVersion = packageJSON.version;
 | 
			
		||||
const AuthenticatedApp = () => {
 | 
			
		||||
  const { setGuidedTourVisibility } = useGuidedTour();
 | 
			
		||||
  const toggleNotification = useNotification();
 | 
			
		||||
  const setGuidedTourVisibilityRef = useRef(setGuidedTourVisibility);
 | 
			
		||||
  const userInfo = auth.getUserInfo();
 | 
			
		||||
  const userName = get(userInfo, 'username') || getFullName(userInfo.firstname, userInfo.lastname);
 | 
			
		||||
  const [userDisplayName, setUserDisplayName] = useState(userName);
 | 
			
		||||
@ -35,7 +34,7 @@ const AuthenticatedApp = () => {
 | 
			
		||||
  const { showReleaseNotification } = useConfigurations();
 | 
			
		||||
  const [
 | 
			
		||||
    { data: appInfos, status },
 | 
			
		||||
    { data: tag_name, isLoading },
 | 
			
		||||
    { data: tagName, isLoading },
 | 
			
		||||
    { data: permissions, status: fetchPermissionsStatus, refetch, isFetched, isFetching },
 | 
			
		||||
    { data: userRoles },
 | 
			
		||||
  ] = useQueries([
 | 
			
		||||
@ -57,20 +56,20 @@ const AuthenticatedApp = () => {
 | 
			
		||||
    },
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  const shouldUpdateStrapi = useMemo(
 | 
			
		||||
    () => checkLatestStrapiVersion(strapiVersion, tag_name),
 | 
			
		||||
    [tag_name]
 | 
			
		||||
  );
 | 
			
		||||
  const shouldUpdateStrapi = checkLatestStrapiVersion(strapiVersion, tagName);
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * TODO: does this actually need to be an effect?
 | 
			
		||||
   */
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (userRoles) {
 | 
			
		||||
      const isUserSuperAdmin = userRoles.find(({ code }) => code === 'strapi-super-admin');
 | 
			
		||||
 | 
			
		||||
      if (isUserSuperAdmin && appInfos?.autoReload) {
 | 
			
		||||
        setGuidedTourVisibilityRef.current(true);
 | 
			
		||||
        setGuidedTourVisibility(true);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [userRoles, appInfos]);
 | 
			
		||||
  }, [userRoles, appInfos, setGuidedTourVisibility]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const getUserId = async () => {
 | 
			
		||||
@ -88,32 +87,28 @@ const AuthenticatedApp = () => {
 | 
			
		||||
 | 
			
		||||
  const shouldShowLoader = isLoading || shouldShowNotDependentQueriesLoader;
 | 
			
		||||
 | 
			
		||||
  const appInfosValue = useMemo(() => {
 | 
			
		||||
    return {
 | 
			
		||||
      ...appInfos,
 | 
			
		||||
      userId,
 | 
			
		||||
      latestStrapiReleaseTag: tag_name,
 | 
			
		||||
      setUserDisplayName,
 | 
			
		||||
      shouldUpdateStrapi,
 | 
			
		||||
      userDisplayName,
 | 
			
		||||
    };
 | 
			
		||||
  }, [appInfos, tag_name, shouldUpdateStrapi, userDisplayName, userId]);
 | 
			
		||||
 | 
			
		||||
  if (shouldShowLoader) {
 | 
			
		||||
    return <LoadingIndicatorPage />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO add error state
 | 
			
		||||
  // TODO: add error state
 | 
			
		||||
  if (status === 'error') {
 | 
			
		||||
    return <div>error...</div>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AppInfosContext.Provider value={appInfosValue}>
 | 
			
		||||
    <AppInfoProvider
 | 
			
		||||
      {...appInfos}
 | 
			
		||||
      userId={userId}
 | 
			
		||||
      latestStrapiReleaseTag={tagName}
 | 
			
		||||
      setUserDisplayName={setUserDisplayName}
 | 
			
		||||
      shouldUpdateStrapi={shouldUpdateStrapi}
 | 
			
		||||
      userDisplayName={userDisplayName}
 | 
			
		||||
    >
 | 
			
		||||
      <RBACProvider permissions={permissions} refetchPermissions={refetch}>
 | 
			
		||||
        <PluginsInitializer />
 | 
			
		||||
      </RBACProvider>
 | 
			
		||||
    </AppInfosContext.Provider>
 | 
			
		||||
    </AppInfoProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,95 +0,0 @@
 | 
			
		||||
import React, { useEffect } from 'react';
 | 
			
		||||
import ReactDOM from 'react-dom';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useIntl } from 'react-intl';
 | 
			
		||||
import styled, { keyframes } from 'styled-components';
 | 
			
		||||
import { pxToRem } from '@strapi/helper-plugin';
 | 
			
		||||
import { Clock, Refresh } from '@strapi/icons';
 | 
			
		||||
import { Link } from '@strapi/design-system/v2';
 | 
			
		||||
import { Box, Flex, Typography } from '@strapi/design-system';
 | 
			
		||||
import { Content, IconBox, Overlay } from './Overlay';
 | 
			
		||||
 | 
			
		||||
const overlayContainer = document.createElement('div');
 | 
			
		||||
const ID = 'autoReloadOverlayBlocker';
 | 
			
		||||
overlayContainer.setAttribute('id', ID);
 | 
			
		||||
 | 
			
		||||
const rotation = keyframes`
 | 
			
		||||
  from {
 | 
			
		||||
    transform: rotate(0deg);
 | 
			
		||||
  }
 | 
			
		||||
  to {
 | 
			
		||||
    transform: rotate(359deg);
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const LoaderReload = styled(Refresh)`
 | 
			
		||||
  animation: ${rotation} 1s infinite linear;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const Blocker = ({ displayedIcon, description, title, isOpen }) => {
 | 
			
		||||
  const { formatMessage } = useIntl();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.body.appendChild(overlayContainer);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      document.body.removeChild(overlayContainer);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  if (isOpen) {
 | 
			
		||||
    return ReactDOM.createPortal(
 | 
			
		||||
      <Overlay>
 | 
			
		||||
        <Content direction="column" alignItems="stretch" gap={6}>
 | 
			
		||||
          <Flex direction="column" alignItems="stretch" gap={2}>
 | 
			
		||||
            <Flex justifyContent="center">
 | 
			
		||||
              <Typography as="h1" variant="alpha">
 | 
			
		||||
                {formatMessage(title)}
 | 
			
		||||
              </Typography>
 | 
			
		||||
            </Flex>
 | 
			
		||||
            <Flex justifyContent="center">
 | 
			
		||||
              <Typography as="h2" textColor="neutral600" fontSize={4} fontWeight="regular">
 | 
			
		||||
                {formatMessage(description)}
 | 
			
		||||
              </Typography>
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Flex>
 | 
			
		||||
          <Flex justifyContent="center">
 | 
			
		||||
            {displayedIcon === 'reload' && (
 | 
			
		||||
              <IconBox padding={6} background="primary100" borderColor="primary200">
 | 
			
		||||
                <LoaderReload width={pxToRem(36)} height={pxToRem(36)} />
 | 
			
		||||
              </IconBox>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            {displayedIcon === 'time' && (
 | 
			
		||||
              <IconBox padding={6} background="primary100" borderColor="primary200">
 | 
			
		||||
                <Clock width={pxToRem(40)} height={pxToRem(40)} />
 | 
			
		||||
              </IconBox>
 | 
			
		||||
            )}
 | 
			
		||||
          </Flex>
 | 
			
		||||
          <Flex justifyContent="center">
 | 
			
		||||
            <Box paddingTop={2}>
 | 
			
		||||
              <Link href="https://docs.strapi.io" isExternal>
 | 
			
		||||
                {formatMessage({
 | 
			
		||||
                  id: 'global.documentation',
 | 
			
		||||
                  defaultMessage: 'Read the documentation',
 | 
			
		||||
                })}
 | 
			
		||||
              </Link>
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Flex>
 | 
			
		||||
        </Content>
 | 
			
		||||
      </Overlay>,
 | 
			
		||||
      overlayContainer
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Blocker.propTypes = {
 | 
			
		||||
  displayedIcon: PropTypes.oneOfType([PropTypes.string, PropTypes.array]).isRequired,
 | 
			
		||||
  description: PropTypes.object.isRequired,
 | 
			
		||||
  isOpen: PropTypes.bool.isRequired,
 | 
			
		||||
  title: PropTypes.object.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Blocker;
 | 
			
		||||
@ -1,42 +0,0 @@
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import { Box, Flex } from '@strapi/design-system';
 | 
			
		||||
import { pxToRem } from '@strapi/helper-plugin';
 | 
			
		||||
 | 
			
		||||
const Overlay = styled(Box)`
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  z-index: 1140;
 | 
			
		||||
  &:before {
 | 
			
		||||
    content: '';
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    background: ${({ theme }) => theme.colors.neutral0};
 | 
			
		||||
    opacity: 0.9;
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const Content = styled(Flex)`
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  padding-top: ${pxToRem(160)};
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const IconBox = styled(Box)`
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  svg {
 | 
			
		||||
    > path {
 | 
			
		||||
      fill: ${({ theme }) => theme.colors.primary600} !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export { Content, IconBox, Overlay };
 | 
			
		||||
@ -1,99 +0,0 @@
 | 
			
		||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { AutoReloadOverlayBockerContext } from '@strapi/helper-plugin';
 | 
			
		||||
import Blocker from './Blocker';
 | 
			
		||||
 | 
			
		||||
const ELAPSED = 30;
 | 
			
		||||
 | 
			
		||||
const AutoReloadOverlayBlockerProvider = ({ children }) => {
 | 
			
		||||
  const [isOpen, setIsOpen] = useState(false);
 | 
			
		||||
  const [{ elapsed }, setState] = useState({ elapsed: 0, start: 0 });
 | 
			
		||||
  const [config, setConfig] = useState(undefined);
 | 
			
		||||
 | 
			
		||||
  const lockAppWithAutoreload = (config = undefined) => {
 | 
			
		||||
    setIsOpen(true);
 | 
			
		||||
    setConfig(config);
 | 
			
		||||
    setState((prev) => ({ ...prev, start: Date.now() }));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const unlockAppWithAutoreload = () => {
 | 
			
		||||
    setIsOpen(false);
 | 
			
		||||
    setState({ start: 0, elapsed: 0 });
 | 
			
		||||
    setConfig(undefined);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const lockApp = useRef(lockAppWithAutoreload);
 | 
			
		||||
  const unlockApp = useRef(unlockAppWithAutoreload);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let timer = null;
 | 
			
		||||
 | 
			
		||||
    if (isOpen) {
 | 
			
		||||
      timer = setInterval(() => {
 | 
			
		||||
        if (elapsed > ELAPSED) {
 | 
			
		||||
          clearInterval(timer);
 | 
			
		||||
 | 
			
		||||
          return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setState((prev) => ({ ...prev, elapsed: Math.round(Date.now() - prev.start) / 1000 }));
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
      }, 1000);
 | 
			
		||||
    } else {
 | 
			
		||||
      clearInterval(timer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      clearInterval(timer);
 | 
			
		||||
    };
 | 
			
		||||
  }, [isOpen, elapsed]);
 | 
			
		||||
 | 
			
		||||
  let displayedIcon = config?.icon || 'reload';
 | 
			
		||||
 | 
			
		||||
  let description = {
 | 
			
		||||
    id: config?.description || 'components.OverlayBlocker.description',
 | 
			
		||||
    defaultMessage:
 | 
			
		||||
      "You're using a feature that needs the server to restart. Please wait until the server is up.",
 | 
			
		||||
  };
 | 
			
		||||
  let title = {
 | 
			
		||||
    id: config?.title || 'components.OverlayBlocker.title',
 | 
			
		||||
    defaultMessage: 'Waiting for restart',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (elapsed > ELAPSED) {
 | 
			
		||||
    displayedIcon = 'time';
 | 
			
		||||
 | 
			
		||||
    description = {
 | 
			
		||||
      id: 'components.OverlayBlocker.description.serverError',
 | 
			
		||||
      defaultMessage: 'The server should have restarted, please check your logs in the terminal.',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    title = {
 | 
			
		||||
      id: 'components.OverlayBlocker.title.serverError',
 | 
			
		||||
      defaultMessage: 'The restart is taking longer than expected',
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const autoReloadValue = useMemo(() => {
 | 
			
		||||
    return { lockApp: lockApp.current, unlockApp: unlockApp.current };
 | 
			
		||||
  }, [lockApp, unlockApp]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AutoReloadOverlayBockerContext.Provider value={autoReloadValue}>
 | 
			
		||||
      <Blocker
 | 
			
		||||
        displayedIcon={displayedIcon}
 | 
			
		||||
        isOpen={isOpen}
 | 
			
		||||
        description={description}
 | 
			
		||||
        title={title}
 | 
			
		||||
      />
 | 
			
		||||
      {children}
 | 
			
		||||
    </AutoReloadOverlayBockerContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
AutoReloadOverlayBlockerProvider.propTypes = {
 | 
			
		||||
  children: PropTypes.element.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AutoReloadOverlayBlockerProvider;
 | 
			
		||||
@ -0,0 +1,53 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useDragLayer } from 'react-dnd';
 | 
			
		||||
import { Box } from '@strapi/design-system';
 | 
			
		||||
 | 
			
		||||
function getStyle(initialOffset, currentOffset, mouseOffset) {
 | 
			
		||||
  if (!initialOffset || !currentOffset) {
 | 
			
		||||
    return { display: 'none' };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { x, y } = mouseOffset;
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    transform: `translate(${x}px, ${y}px)`,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function DragLayer({ renderItem }) {
 | 
			
		||||
  const { itemType, isDragging, item, initialOffset, currentOffset, mouseOffset } = useDragLayer(
 | 
			
		||||
    (monitor) => ({
 | 
			
		||||
      item: monitor.getItem(),
 | 
			
		||||
      itemType: monitor.getItemType(),
 | 
			
		||||
      initialOffset: monitor.getInitialSourceClientOffset(),
 | 
			
		||||
      currentOffset: monitor.getSourceClientOffset(),
 | 
			
		||||
      isDragging: monitor.isDragging(),
 | 
			
		||||
      mouseOffset: monitor.getClientOffset(),
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (!isDragging) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      height="100%"
 | 
			
		||||
      left={0}
 | 
			
		||||
      position="fixed"
 | 
			
		||||
      pointerEvents="none"
 | 
			
		||||
      top={0}
 | 
			
		||||
      zIndex={100}
 | 
			
		||||
      width="100%"
 | 
			
		||||
    >
 | 
			
		||||
      <Box style={getStyle(initialOffset, currentOffset, mouseOffset)}>
 | 
			
		||||
        {renderItem({ type: itemType, item })}
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DragLayer.propTypes = {
 | 
			
		||||
  renderItem: PropTypes.func.isRequired,
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
export * from './DragLayer';
 | 
			
		||||
@ -96,6 +96,27 @@ describe('GuidedTour Stepper', () => {
 | 
			
		||||
        min-height: 4.0625rem;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c4 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c5 {
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        line-height: 1.25;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c11 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #666687;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c0 {
 | 
			
		||||
        -webkit-align-items: center;
 | 
			
		||||
        -webkit-box-align: center;
 | 
			
		||||
@ -142,27 +163,6 @@ describe('GuidedTour Stepper', () => {
 | 
			
		||||
        flex-direction: row;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c4 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c5 {
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        line-height: 1.25;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c11 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #666687;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        class=""
 | 
			
		||||
      >
 | 
			
		||||
@ -351,6 +351,27 @@ describe('GuidedTour Stepper', () => {
 | 
			
		||||
        height: 1.875rem;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c6 {
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        line-height: 1.25;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c11 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c14 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #666687;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c0 {
 | 
			
		||||
        -webkit-align-items: center;
 | 
			
		||||
        -webkit-box-align: center;
 | 
			
		||||
@ -397,27 +418,6 @@ describe('GuidedTour Stepper', () => {
 | 
			
		||||
        flex-direction: row;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c6 {
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        line-height: 1.25;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c11 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c14 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #666687;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c5 path {
 | 
			
		||||
        fill: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
@ -601,6 +601,13 @@ describe('GuidedTour Stepper', () => {
 | 
			
		||||
        margin-top: 8px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c6 {
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        line-height: 1.25;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c0 {
 | 
			
		||||
        -webkit-align-items: center;
 | 
			
		||||
        -webkit-box-align: center;
 | 
			
		||||
@ -647,13 +654,6 @@ describe('GuidedTour Stepper', () => {
 | 
			
		||||
        flex-direction: row;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c6 {
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        line-height: 1.25;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c5 path {
 | 
			
		||||
        fill: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -120,6 +120,41 @@ describe('GuidedTour Homepage', () => {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c3 {
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        font-size: 1.125rem;
 | 
			
		||||
        line-height: 1.22;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c8 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c9 {
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        line-height: 1.25;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c22 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #666687;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c29 {
 | 
			
		||||
        font-size: 0.75rem;
 | 
			
		||||
        line-height: 1.33;
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c1 {
 | 
			
		||||
        -webkit-align-items: stretch;
 | 
			
		||||
        -webkit-box-align: stretch;
 | 
			
		||||
@ -214,53 +249,18 @@ describe('GuidedTour Homepage', () => {
 | 
			
		||||
        gap: 8px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c3 {
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        font-size: 1.125rem;
 | 
			
		||||
        line-height: 1.22;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c8 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c9 {
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        line-height: 1.25;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c22 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #666687;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c29 {
 | 
			
		||||
        font-size: 0.75rem;
 | 
			
		||||
        line-height: 1.33;
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c27 {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        outline: none;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c27 svg {
 | 
			
		||||
      .c27 > svg {
 | 
			
		||||
        height: 12px;
 | 
			
		||||
        width: 12px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c27 svg > g,
 | 
			
		||||
      .c27 svg path {
 | 
			
		||||
      .c27 > svg > g,
 | 
			
		||||
      .c27 > svg path {
 | 
			
		||||
        fill: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -399,13 +399,13 @@ describe('GuidedTour Homepage', () => {
 | 
			
		||||
        outline: none;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c16 svg {
 | 
			
		||||
      .c16 > svg {
 | 
			
		||||
        height: 12px;
 | 
			
		||||
        width: 12px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c16 svg > g,
 | 
			
		||||
      .c16 svg path {
 | 
			
		||||
      .c16 > svg > g,
 | 
			
		||||
      .c16 > svg path {
 | 
			
		||||
        fill: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -117,6 +117,42 @@ describe('<GuidedTourModal />', () => {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c15 {
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        font-size: 0.6875rem;
 | 
			
		||||
        line-height: 1.45;
 | 
			
		||||
        text-transform: uppercase;
 | 
			
		||||
        color: #4945ff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c19 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c20 {
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        font-size: 2rem;
 | 
			
		||||
        line-height: 1.25;
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c24 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c28 {
 | 
			
		||||
        font-size: 0.75rem;
 | 
			
		||||
        line-height: 1.33;
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c2 {
 | 
			
		||||
        -webkit-align-items: center;
 | 
			
		||||
        -webkit-box-align: center;
 | 
			
		||||
@ -244,54 +280,18 @@ describe('<GuidedTourModal />', () => {
 | 
			
		||||
        gap: 8px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c15 {
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        font-size: 0.6875rem;
 | 
			
		||||
        line-height: 1.45;
 | 
			
		||||
        text-transform: uppercase;
 | 
			
		||||
        color: #4945ff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c19 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c20 {
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        font-size: 2rem;
 | 
			
		||||
        line-height: 1.25;
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c24 {
 | 
			
		||||
        font-size: 0.875rem;
 | 
			
		||||
        line-height: 1.43;
 | 
			
		||||
        color: #32324d;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c28 {
 | 
			
		||||
        font-size: 0.75rem;
 | 
			
		||||
        line-height: 1.33;
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        color: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c8 {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        outline: none;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c8 svg {
 | 
			
		||||
      .c8 > svg {
 | 
			
		||||
        height: 12px;
 | 
			
		||||
        width: 12px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c8 svg > g,
 | 
			
		||||
      .c8 svg path {
 | 
			
		||||
      .c8 > svg > g,
 | 
			
		||||
      .c8 > svg path {
 | 
			
		||||
        fill: #ffffff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -487,7 +487,7 @@ describe('<GuidedTourModal />', () => {
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div
 | 
			
		||||
          data-react-portal="true"
 | 
			
		||||
          class=""
 | 
			
		||||
        >
 | 
			
		||||
          <div
 | 
			
		||||
            class="c1 c2 c3"
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ import { Write, Exit } from '@strapi/icons';
 | 
			
		||||
import {
 | 
			
		||||
  auth,
 | 
			
		||||
  usePersistentState,
 | 
			
		||||
  useAppInfos,
 | 
			
		||||
  useAppInfo,
 | 
			
		||||
  useTracking,
 | 
			
		||||
  getFetchClient,
 | 
			
		||||
} from '@strapi/helper-plugin';
 | 
			
		||||
@ -59,7 +59,7 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => {
 | 
			
		||||
    logos: { menu },
 | 
			
		||||
  } = useConfigurations();
 | 
			
		||||
  const [condensed, setCondensed] = usePersistentState('navbar-condensed', false);
 | 
			
		||||
  const { userDisplayName } = useAppInfos();
 | 
			
		||||
  const { userDisplayName } = useAppInfo();
 | 
			
		||||
  const { formatMessage } = useIntl();
 | 
			
		||||
  const { trackUsage } = useTracking();
 | 
			
		||||
  const { pathname } = useLocation();
 | 
			
		||||
 | 
			
		||||
@ -1,159 +0,0 @@
 | 
			
		||||
import React, { useEffect, useCallback } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useIntl } from 'react-intl';
 | 
			
		||||
import { Alert } from '@strapi/design-system';
 | 
			
		||||
import { Link } from '@strapi/design-system/v2';
 | 
			
		||||
 | 
			
		||||
const Notification = ({ dispatch, notification }) => {
 | 
			
		||||
  const { formatMessage } = useIntl();
 | 
			
		||||
  const { message, link, type, id, onClose, timeout, blockTransition, title } = notification;
 | 
			
		||||
 | 
			
		||||
  const formattedMessage = (msg) =>
 | 
			
		||||
    typeof msg === 'string' ? msg : formatMessage(msg, msg.values);
 | 
			
		||||
  const handleClose = useCallback(() => {
 | 
			
		||||
    if (onClose) {
 | 
			
		||||
      onClose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dispatch({
 | 
			
		||||
      type: 'HIDE_NOTIFICATION',
 | 
			
		||||
      id,
 | 
			
		||||
    });
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [id]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let timeoutToClear;
 | 
			
		||||
 | 
			
		||||
    if (!blockTransition) {
 | 
			
		||||
      timeoutToClear = setTimeout(() => {
 | 
			
		||||
        handleClose();
 | 
			
		||||
      }, timeout || 2500);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => clearTimeout(timeoutToClear);
 | 
			
		||||
  }, [blockTransition, handleClose, timeout]);
 | 
			
		||||
 | 
			
		||||
  let variant;
 | 
			
		||||
  let alertTitle;
 | 
			
		||||
 | 
			
		||||
  // TODO break out this logic into separate file
 | 
			
		||||
  if (type === 'info') {
 | 
			
		||||
    variant = 'default';
 | 
			
		||||
    alertTitle = formatMessage({
 | 
			
		||||
      id: 'notification.default.title',
 | 
			
		||||
      defaultMessage: 'Information:',
 | 
			
		||||
    });
 | 
			
		||||
  } else if (type === 'warning') {
 | 
			
		||||
    // type should be renamed to danger in the future, but it might introduce changes if done now
 | 
			
		||||
    variant = 'danger';
 | 
			
		||||
    alertTitle = formatMessage({
 | 
			
		||||
      id: 'notification.warning.title',
 | 
			
		||||
      defaultMessage: 'Warning:',
 | 
			
		||||
    });
 | 
			
		||||
  } else if (type === 'softWarning') {
 | 
			
		||||
    // type should be renamed to just warning in the future
 | 
			
		||||
    variant = 'warning';
 | 
			
		||||
    alertTitle = formatMessage({
 | 
			
		||||
      id: 'notification.warning.title',
 | 
			
		||||
      defaultMessage: 'Warning:',
 | 
			
		||||
    });
 | 
			
		||||
  } else {
 | 
			
		||||
    variant = 'success';
 | 
			
		||||
    alertTitle = formatMessage({
 | 
			
		||||
      id: 'notification.success.title',
 | 
			
		||||
      defaultMessage: 'Success:',
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (title) {
 | 
			
		||||
    alertTitle =
 | 
			
		||||
      typeof title === 'string'
 | 
			
		||||
        ? title
 | 
			
		||||
        : formattedMessage({
 | 
			
		||||
            id: title?.id || title,
 | 
			
		||||
            defaultMessage: title?.defaultMessage || title?.id || title,
 | 
			
		||||
            values: title?.values,
 | 
			
		||||
          });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Alert
 | 
			
		||||
      action={
 | 
			
		||||
        link ? (
 | 
			
		||||
          <Link href={link.url} isExternal>
 | 
			
		||||
            {formatMessage({
 | 
			
		||||
              id: link.label?.id || link.label,
 | 
			
		||||
              defaultMessage: link.label?.defaultMessage || link.label?.id || link.label,
 | 
			
		||||
            })}
 | 
			
		||||
          </Link>
 | 
			
		||||
        ) : undefined
 | 
			
		||||
      }
 | 
			
		||||
      onClose={handleClose}
 | 
			
		||||
      closeLabel="Close"
 | 
			
		||||
      title={alertTitle}
 | 
			
		||||
      variant={variant}
 | 
			
		||||
    >
 | 
			
		||||
      {formattedMessage({
 | 
			
		||||
        id: message?.id || message,
 | 
			
		||||
        defaultMessage: message?.defaultMessage || message?.id || message,
 | 
			
		||||
        values: message?.values,
 | 
			
		||||
      })}
 | 
			
		||||
    </Alert>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Notification.defaultProps = {
 | 
			
		||||
  notification: {
 | 
			
		||||
    id: 1,
 | 
			
		||||
    type: 'success',
 | 
			
		||||
    message: {
 | 
			
		||||
      id: 'notification.success.saved',
 | 
			
		||||
      defaultMessage: 'Saved',
 | 
			
		||||
    },
 | 
			
		||||
    onClose: () => null,
 | 
			
		||||
    timeout: 2500,
 | 
			
		||||
    blockTransition: false,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Notification.propTypes = {
 | 
			
		||||
  dispatch: PropTypes.func.isRequired,
 | 
			
		||||
  notification: PropTypes.shape({
 | 
			
		||||
    id: PropTypes.number,
 | 
			
		||||
    message: PropTypes.oneOfType([
 | 
			
		||||
      PropTypes.string,
 | 
			
		||||
      PropTypes.shape({
 | 
			
		||||
        id: PropTypes.string.isRequired,
 | 
			
		||||
        defaultMessage: PropTypes.string,
 | 
			
		||||
        values: PropTypes.object,
 | 
			
		||||
      }),
 | 
			
		||||
    ]),
 | 
			
		||||
    link: PropTypes.shape({
 | 
			
		||||
      target: PropTypes.string,
 | 
			
		||||
      url: PropTypes.string.isRequired,
 | 
			
		||||
      label: PropTypes.oneOfType([
 | 
			
		||||
        PropTypes.string,
 | 
			
		||||
        PropTypes.shape({
 | 
			
		||||
          id: PropTypes.string.isRequired,
 | 
			
		||||
          defaultMessage: PropTypes.string,
 | 
			
		||||
          values: PropTypes.object,
 | 
			
		||||
        }),
 | 
			
		||||
      ]).isRequired,
 | 
			
		||||
    }),
 | 
			
		||||
    type: PropTypes.string,
 | 
			
		||||
    onClose: PropTypes.func,
 | 
			
		||||
    timeout: PropTypes.number,
 | 
			
		||||
    blockTransition: PropTypes.bool,
 | 
			
		||||
    title: PropTypes.oneOfType([
 | 
			
		||||
      PropTypes.string,
 | 
			
		||||
      PropTypes.shape({
 | 
			
		||||
        id: PropTypes.string.isRequired,
 | 
			
		||||
        defaultMessage: PropTypes.string,
 | 
			
		||||
        values: PropTypes.object,
 | 
			
		||||
      }),
 | 
			
		||||
    ]),
 | 
			
		||||
  }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Notification;
 | 
			
		||||
@ -1,46 +0,0 @@
 | 
			
		||||
import { NotificationsProvider } from '@strapi/helper-plugin';
 | 
			
		||||
import React, { useReducer } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { Flex } from '@strapi/design-system';
 | 
			
		||||
import Notification from './Notification';
 | 
			
		||||
import reducer, { initialState } from './reducer';
 | 
			
		||||
 | 
			
		||||
const Notifications = ({ children }) => {
 | 
			
		||||
  const [{ notifications }, dispatch] = useReducer(reducer, initialState);
 | 
			
		||||
 | 
			
		||||
  const displayNotification = (config) => {
 | 
			
		||||
    dispatch({
 | 
			
		||||
      type: 'SHOW_NOTIFICATION',
 | 
			
		||||
      config,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <NotificationsProvider toggleNotification={displayNotification}>
 | 
			
		||||
      <Flex
 | 
			
		||||
        left="50%"
 | 
			
		||||
        marginLeft="-250px"
 | 
			
		||||
        position="fixed"
 | 
			
		||||
        direction="column"
 | 
			
		||||
        alignItems="stretch"
 | 
			
		||||
        gap={2}
 | 
			
		||||
        top={`${46 / 16}rem`}
 | 
			
		||||
        width={`${500 / 16}rem`}
 | 
			
		||||
        zIndex={10}
 | 
			
		||||
      >
 | 
			
		||||
        {notifications.map((notification) => {
 | 
			
		||||
          return (
 | 
			
		||||
            <Notification key={notification.id} dispatch={dispatch} notification={notification} />
 | 
			
		||||
          );
 | 
			
		||||
        })}
 | 
			
		||||
      </Flex>
 | 
			
		||||
      {children}
 | 
			
		||||
    </NotificationsProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Notifications.propTypes = {
 | 
			
		||||
  children: PropTypes.element.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Notifications;
 | 
			
		||||
@ -1,47 +0,0 @@
 | 
			
		||||
import produce from 'immer';
 | 
			
		||||
import get from 'lodash/get';
 | 
			
		||||
 | 
			
		||||
const initialState = {
 | 
			
		||||
  notifId: 0,
 | 
			
		||||
  notifications: [],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const notificationReducer = (state = initialState, action) =>
 | 
			
		||||
  // eslint-disable-next-line consistent-return
 | 
			
		||||
  produce(state, (draftState) => {
 | 
			
		||||
    switch (action.type) {
 | 
			
		||||
      case 'SHOW_NOTIFICATION': {
 | 
			
		||||
        draftState.notifications.push({
 | 
			
		||||
          // No action.config spread to limit the notification API and avoid customization
 | 
			
		||||
          id: state.notifId,
 | 
			
		||||
          type: get(action, ['config', 'type'], 'success'),
 | 
			
		||||
          message: get(action, ['config', 'message'], {
 | 
			
		||||
            id: 'notification.success.saved',
 | 
			
		||||
            defaultMessage: 'Saved',
 | 
			
		||||
          }),
 | 
			
		||||
          link: get(action, ['config', 'link'], null),
 | 
			
		||||
          timeout: get(action, ['config', 'timeout'], 2500),
 | 
			
		||||
          blockTransition: get(action, ['config', 'blockTransition'], false),
 | 
			
		||||
          onClose: get(action, ['config', 'onClose'], null),
 | 
			
		||||
          title: get(action, ['config', 'title'], null),
 | 
			
		||||
        });
 | 
			
		||||
        draftState.notifId = state.notifId + 1;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case 'HIDE_NOTIFICATION': {
 | 
			
		||||
        const indexToRemove = state.notifications.findIndex((notif) => notif.id === action.id);
 | 
			
		||||
 | 
			
		||||
        if (indexToRemove !== -1) {
 | 
			
		||||
          draftState.notifications.splice(indexToRemove, 1);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      default: {
 | 
			
		||||
        return draftState;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export default notificationReducer;
 | 
			
		||||
export { initialState };
 | 
			
		||||
@ -1,161 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * Tests for Notifications
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { render, fireEvent, screen } from '@testing-library/react';
 | 
			
		||||
import { IntlProvider } from 'react-intl';
 | 
			
		||||
import { useNotification } from '@strapi/helper-plugin';
 | 
			
		||||
import { act } from 'react-dom/test-utils';
 | 
			
		||||
import { lightTheme, darkTheme } from '@strapi/design-system';
 | 
			
		||||
import Theme from '../../Theme';
 | 
			
		||||
import ThemeToggleProvider from '../../ThemeToggleProvider';
 | 
			
		||||
import Notifications from '../index';
 | 
			
		||||
 | 
			
		||||
const messages = {};
 | 
			
		||||
 | 
			
		||||
describe('<Notifications />', () => {
 | 
			
		||||
  it('renders and matches the snapshot', () => {
 | 
			
		||||
    const {
 | 
			
		||||
      container: { firstChild },
 | 
			
		||||
    } = render(
 | 
			
		||||
      <ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
 | 
			
		||||
        <Theme>
 | 
			
		||||
          <IntlProvider locale="en" messages={messages} defaultLocale="en" textComponent="span">
 | 
			
		||||
            <Notifications>
 | 
			
		||||
              <div />
 | 
			
		||||
            </Notifications>
 | 
			
		||||
          </IntlProvider>
 | 
			
		||||
        </Theme>
 | 
			
		||||
      </ThemeToggleProvider>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(firstChild).toMatchInlineSnapshot(`
 | 
			
		||||
      .c0 {
 | 
			
		||||
        margin-left: -250px;
 | 
			
		||||
        position: fixed;
 | 
			
		||||
        left: 50%;
 | 
			
		||||
        top: 2.875rem;
 | 
			
		||||
        z-index: 10;
 | 
			
		||||
        width: 31.25rem;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .c1 {
 | 
			
		||||
        -webkit-align-items: stretch;
 | 
			
		||||
        -webkit-box-align: stretch;
 | 
			
		||||
        -ms-flex-align: stretch;
 | 
			
		||||
        align-items: stretch;
 | 
			
		||||
        display: -webkit-box;
 | 
			
		||||
        display: -webkit-flex;
 | 
			
		||||
        display: -ms-flexbox;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        -webkit-flex-direction: column;
 | 
			
		||||
        -ms-flex-direction: column;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        gap: 8px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        class="c0 c1"
 | 
			
		||||
      />
 | 
			
		||||
    `);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should display a notification correctly', async () => {
 | 
			
		||||
    const Button = () => {
 | 
			
		||||
      const toggleNotification = useNotification();
 | 
			
		||||
 | 
			
		||||
      const handleClick = () => {
 | 
			
		||||
        toggleNotification({ type: 'success', message: 'simple notif' });
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      return (
 | 
			
		||||
        <button onClick={handleClick} type="button">
 | 
			
		||||
          display notif
 | 
			
		||||
        </button>
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(
 | 
			
		||||
      <ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
 | 
			
		||||
        <Theme>
 | 
			
		||||
          <IntlProvider locale="en" defaultLocale="en" messages={messages} textComponent="span">
 | 
			
		||||
            <Notifications>
 | 
			
		||||
              <Button />
 | 
			
		||||
            </Notifications>
 | 
			
		||||
          </IntlProvider>
 | 
			
		||||
        </Theme>
 | 
			
		||||
      </ThemeToggleProvider>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Click button
 | 
			
		||||
    fireEvent.click(screen.getByText('display notif'));
 | 
			
		||||
 | 
			
		||||
    const items = await screen.findAllByText(/simple notif/);
 | 
			
		||||
 | 
			
		||||
    expect(items).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
    await act(async () => {
 | 
			
		||||
      await new Promise((resolve) => {
 | 
			
		||||
        setTimeout(resolve, 2500);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const foundItems = screen.queryAllByText(/simple notif/);
 | 
			
		||||
 | 
			
		||||
    expect(foundItems).toHaveLength(0);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should display a notification correctly and not toggle it', async () => {
 | 
			
		||||
    const Button = () => {
 | 
			
		||||
      const toggleNotification = useNotification();
 | 
			
		||||
 | 
			
		||||
      const handleClick = () => {
 | 
			
		||||
        toggleNotification({ type: 'success', message: 'simple notif', blockTransition: true });
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      return (
 | 
			
		||||
        <button onClick={handleClick} type="button">
 | 
			
		||||
          display notif
 | 
			
		||||
        </button>
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(
 | 
			
		||||
      <ThemeToggleProvider themes={{ light: lightTheme, dark: darkTheme }}>
 | 
			
		||||
        <Theme>
 | 
			
		||||
          <IntlProvider locale="en" defaultLocale="en" messages={messages} textComponent="span">
 | 
			
		||||
            <Notifications>
 | 
			
		||||
              <Button />
 | 
			
		||||
            </Notifications>
 | 
			
		||||
          </IntlProvider>
 | 
			
		||||
        </Theme>
 | 
			
		||||
      </ThemeToggleProvider>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Click button
 | 
			
		||||
    fireEvent.click(screen.getByText('display notif'));
 | 
			
		||||
 | 
			
		||||
    const items = await screen.findAllByText(/simple notif/);
 | 
			
		||||
 | 
			
		||||
    expect(items).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
    await act(async () => {
 | 
			
		||||
      await new Promise((resolve) => {
 | 
			
		||||
        setTimeout(resolve, 2500);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const foundItems = screen.queryAllByText(/simple notif/);
 | 
			
		||||
 | 
			
		||||
    expect(foundItems).toHaveLength(1);
 | 
			
		||||
 | 
			
		||||
    fireEvent.click(screen.getByLabelText('Close'));
 | 
			
		||||
 | 
			
		||||
    const displayedItems = screen.queryAllByText(/simple notif/);
 | 
			
		||||
 | 
			
		||||
    expect(displayedItems).toHaveLength(0);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -1,80 +0,0 @@
 | 
			
		||||
import reducer from '../reducer';
 | 
			
		||||
 | 
			
		||||
describe('ADMIN | COMPONENTS | NOTIFICATIONS | reducer', () => {
 | 
			
		||||
  describe('DEFAULT_ACTION', () => {
 | 
			
		||||
    it('should return the initialState', () => {
 | 
			
		||||
      const state = {
 | 
			
		||||
        test: true,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      expect(reducer(state, {})).toEqual(state);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('SHOW_NOTIFICATION', () => {
 | 
			
		||||
    it('should add a notification', () => {
 | 
			
		||||
      const action = {
 | 
			
		||||
        type: 'SHOW_NOTIFICATION',
 | 
			
		||||
        config: {
 | 
			
		||||
          type: 'success',
 | 
			
		||||
          message: {
 | 
			
		||||
            id: 'notification.message',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
      const initialState = {
 | 
			
		||||
        notifications: [],
 | 
			
		||||
        notifId: 0,
 | 
			
		||||
      };
 | 
			
		||||
      const expected = {
 | 
			
		||||
        notifications: [
 | 
			
		||||
          {
 | 
			
		||||
            id: 0,
 | 
			
		||||
            type: 'success',
 | 
			
		||||
            message: { id: 'notification.message' },
 | 
			
		||||
            link: null,
 | 
			
		||||
            timeout: 2500,
 | 
			
		||||
            blockTransition: false,
 | 
			
		||||
            onClose: null,
 | 
			
		||||
            title: null,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        notifId: 1,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      expect(reducer(initialState, action)).toEqual(expected);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('HIDE_NOTIFICATION', () => {
 | 
			
		||||
    it('should remove a notification if the notification exist', () => {
 | 
			
		||||
      const action = {
 | 
			
		||||
        type: 'HIDE_NOTIFICATION',
 | 
			
		||||
        id: 1,
 | 
			
		||||
      };
 | 
			
		||||
      const initialState = {
 | 
			
		||||
        notifications: [{ id: 1, message: { id: 'notification.message' }, type: 'success' }],
 | 
			
		||||
      };
 | 
			
		||||
      const expected = {
 | 
			
		||||
        notifications: [],
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      expect(reducer(initialState, action)).toEqual(expected);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not remove the notification if the notification does not exist', () => {
 | 
			
		||||
      const action = {
 | 
			
		||||
        type: 'HIDE_NOTIFICATION',
 | 
			
		||||
        id: 3,
 | 
			
		||||
      };
 | 
			
		||||
      const initialState = {
 | 
			
		||||
        notifications: [{ id: 1, message: { id: 'notification.message' }, type: 'success' }],
 | 
			
		||||
      };
 | 
			
		||||
      const expected = {
 | 
			
		||||
        notifications: [{ id: 1, message: { id: 'notification.message' }, type: 'success' }],
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      expect(reducer(initialState, action)).toEqual(expected);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -1,67 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * OverlayBlockerProvider
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import ReactDOM from 'react-dom';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import { OverlayBlockerContext } from '@strapi/helper-plugin';
 | 
			
		||||
 | 
			
		||||
const overlayContainer = document.createElement('div');
 | 
			
		||||
overlayContainer.setAttribute('id', 'overlayBlocker');
 | 
			
		||||
 | 
			
		||||
const Overlay = styled.div`
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  z-index: 1140;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const Portal = ({ isOpen }) => {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.body.appendChild(overlayContainer);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      document.body.removeChild(overlayContainer);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  if (isOpen) {
 | 
			
		||||
    return ReactDOM.createPortal(<Overlay />, overlayContainer);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Portal.propTypes = {
 | 
			
		||||
  isOpen: PropTypes.bool.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const OverlayBlockerProvider = ({ children }) => {
 | 
			
		||||
  const [isOpen, setIsOpen] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const lockApp = () => {
 | 
			
		||||
    setIsOpen(true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const unlockApp = () => {
 | 
			
		||||
    setIsOpen(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <OverlayBlockerContext.Provider value={{ lockApp, unlockApp }}>
 | 
			
		||||
      {children}
 | 
			
		||||
      <Portal isOpen={isOpen} />
 | 
			
		||||
    </OverlayBlockerContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
OverlayBlockerProvider.propTypes = {
 | 
			
		||||
  children: PropTypes.node.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default OverlayBlockerProvider;
 | 
			
		||||
@ -1,15 +1,19 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { QueryClientProvider, QueryClient } from 'react-query';
 | 
			
		||||
import { LibraryProvider, CustomFieldsProvider, StrapiAppProvider } from '@strapi/helper-plugin';
 | 
			
		||||
import {
 | 
			
		||||
  LibraryProvider,
 | 
			
		||||
  CustomFieldsProvider,
 | 
			
		||||
  StrapiAppProvider,
 | 
			
		||||
  AutoReloadOverlayBlockerProvider,
 | 
			
		||||
  OverlayBlockerProvider,
 | 
			
		||||
  NotificationsProvider,
 | 
			
		||||
} from '@strapi/helper-plugin';
 | 
			
		||||
import { Provider } from 'react-redux';
 | 
			
		||||
import { AdminContext } from '../../contexts';
 | 
			
		||||
import ConfigurationsProvider from '../ConfigurationsProvider';
 | 
			
		||||
import LanguageProvider from '../LanguageProvider';
 | 
			
		||||
import GuidedTour from '../GuidedTour';
 | 
			
		||||
import AutoReloadOverlayBlockerProvider from '../AutoReloadOverlayBlockerProvider';
 | 
			
		||||
import Notifications from '../Notifications';
 | 
			
		||||
import OverlayBlocker from '../OverlayBlocker';
 | 
			
		||||
import ThemeToggleProvider from '../ThemeToggleProvider';
 | 
			
		||||
import Theme from '../Theme';
 | 
			
		||||
 | 
			
		||||
@ -68,11 +72,11 @@ const Providers = ({
 | 
			
		||||
                    <CustomFieldsProvider customFields={customFields}>
 | 
			
		||||
                      <LanguageProvider messages={messages} localeNames={localeNames}>
 | 
			
		||||
                        <AutoReloadOverlayBlockerProvider>
 | 
			
		||||
                          <OverlayBlocker>
 | 
			
		||||
                          <OverlayBlockerProvider>
 | 
			
		||||
                            <GuidedTour>
 | 
			
		||||
                              <Notifications>{children}</Notifications>
 | 
			
		||||
                              <NotificationsProvider>{children}</NotificationsProvider>
 | 
			
		||||
                            </GuidedTour>
 | 
			
		||||
                          </OverlayBlocker>
 | 
			
		||||
                          </OverlayBlockerProvider>
 | 
			
		||||
                        </AutoReloadOverlayBlockerProvider>
 | 
			
		||||
                      </LanguageProvider>
 | 
			
		||||
                    </CustomFieldsProvider>
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { permissions } from '@strapi/admin-test-utils/lib/fixtures';
 | 
			
		||||
import { fixtures } from '@strapi/admin-test-utils';
 | 
			
		||||
 | 
			
		||||
import { setPermissions, resetStore } from '../actions';
 | 
			
		||||
import rbacProviderReducer, { initialState } from '../reducer';
 | 
			
		||||
@ -89,7 +89,8 @@ describe('rbacProviderReducer', () => {
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        rbacProviderReducer(state, setPermissions(permissions)).collectionTypesRelatedPermissions
 | 
			
		||||
        rbacProviderReducer(state, setPermissions(fixtures.permissions.allPermissions))
 | 
			
		||||
          .collectionTypesRelatedPermissions
 | 
			
		||||
      ).toEqual(expected);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -1,83 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import { pxToRem } from '@strapi/helper-plugin';
 | 
			
		||||
import { Box, Flex, Typography, IconButton } from '@strapi/design-system';
 | 
			
		||||
import { Trash, Drag, CarretDown } from '@strapi/icons';
 | 
			
		||||
 | 
			
		||||
const DragPreviewBox = styled(Box)`
 | 
			
		||||
  border: 1px solid ${({ theme }) => theme.colors.neutral200};
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const DropdownIconWrapper = styled(Box)`
 | 
			
		||||
  height: ${32 / 16}rem;
 | 
			
		||||
  width: ${32 / 16}rem;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
 | 
			
		||||
  svg {
 | 
			
		||||
    height: ${6 / 16}rem;
 | 
			
		||||
    width: ${11 / 16}rem;
 | 
			
		||||
    > path {
 | 
			
		||||
      fill: ${({ theme }) => theme.colors.neutral600};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const ToggleButton = styled.button`
 | 
			
		||||
  border: none;
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  display: block;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  text-align: unset;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const DragPreview = ({ displayedValue }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <DragPreviewBox
 | 
			
		||||
      paddingLeft={3}
 | 
			
		||||
      paddingRight={3}
 | 
			
		||||
      paddingTop={3}
 | 
			
		||||
      paddingBottom={3}
 | 
			
		||||
      hasRadius
 | 
			
		||||
      background="neutral0"
 | 
			
		||||
      width={pxToRem(300)}
 | 
			
		||||
    >
 | 
			
		||||
      <Flex justifyContent="space-between">
 | 
			
		||||
        <ToggleButton type="button">
 | 
			
		||||
          <Flex>
 | 
			
		||||
            <DropdownIconWrapper background="neutral200">
 | 
			
		||||
              <CarretDown />
 | 
			
		||||
            </DropdownIconWrapper>
 | 
			
		||||
            <Flex gap={2} paddingLeft={6} maxWidth={pxToRem(150)}>
 | 
			
		||||
              <Typography textColor="neutral700" ellipsis>
 | 
			
		||||
                {displayedValue}
 | 
			
		||||
              </Typography>
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Flex>
 | 
			
		||||
        </ToggleButton>
 | 
			
		||||
        <Box paddingLeft={3}>
 | 
			
		||||
          <Flex>
 | 
			
		||||
            <IconButton noBorder>
 | 
			
		||||
              <Trash />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
            <Box paddingLeft={2}>
 | 
			
		||||
              <IconButton noBorder>
 | 
			
		||||
                <Drag />
 | 
			
		||||
              </IconButton>
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Flex>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Flex>
 | 
			
		||||
    </DragPreviewBox>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
DragPreview.propTypes = {
 | 
			
		||||
  displayedValue: PropTypes.string.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DragPreview;
 | 
			
		||||
@ -1,85 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { useDragLayer } from 'react-dnd';
 | 
			
		||||
import LayoutDndProvider from '../LayoutDndProvider';
 | 
			
		||||
 | 
			
		||||
import ItemTypes from '../../utils/ItemTypes';
 | 
			
		||||
import CardPreview from '../../pages/ListSettingsView/components/CardPreview';
 | 
			
		||||
 | 
			
		||||
import ComponentPreview from './ComponentDragPreview';
 | 
			
		||||
import { RelationDragPreview } from './RelationDragPreview';
 | 
			
		||||
 | 
			
		||||
const layerStyles = {
 | 
			
		||||
  position: 'fixed',
 | 
			
		||||
  pointerEvents: 'none',
 | 
			
		||||
  zIndex: 100,
 | 
			
		||||
  left: 0,
 | 
			
		||||
  top: 0,
 | 
			
		||||
  width: '100%',
 | 
			
		||||
  height: '100%',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function getItemStyles(initialOffset, currentOffset, mouseOffset) {
 | 
			
		||||
  if (!initialOffset || !currentOffset) {
 | 
			
		||||
    return { display: 'none' };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { x, y } = mouseOffset;
 | 
			
		||||
  // TODO adjust
 | 
			
		||||
  const transform = `translate(${x}px, ${y}px)`;
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    transform,
 | 
			
		||||
    WebkitTransform: transform,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CustomDragLayer = () => {
 | 
			
		||||
  const { itemType, isDragging, item, initialOffset, currentOffset, mouseOffset } = useDragLayer(
 | 
			
		||||
    (monitor) => ({
 | 
			
		||||
      item: monitor.getItem(),
 | 
			
		||||
      itemType: monitor.getItemType(),
 | 
			
		||||
      initialOffset: monitor.getInitialSourceClientOffset(),
 | 
			
		||||
      currentOffset: monitor.getSourceClientOffset(),
 | 
			
		||||
      isDragging: monitor.isDragging(),
 | 
			
		||||
      mouseOffset: monitor.getClientOffset(),
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (!isDragging) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Because a user may have multiple relations / dynamic zones / repeable fields in the same content type,
 | 
			
		||||
   * we append the fieldName for the item type to make them unique, however, we then want to extract that
 | 
			
		||||
   * first type to apply the correct preview.
 | 
			
		||||
   */
 | 
			
		||||
  const [actualType] = itemType.split('_');
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <LayoutDndProvider>
 | 
			
		||||
      <div style={layerStyles}>
 | 
			
		||||
        <div style={getItemStyles(initialOffset, currentOffset, mouseOffset)} className="col-md-2">
 | 
			
		||||
          {[ItemTypes.EDIT_FIELD, ItemTypes.FIELD].includes(itemType) && (
 | 
			
		||||
            <CardPreview labelField={item.labelField} />
 | 
			
		||||
          )}
 | 
			
		||||
          {actualType === ItemTypes.COMPONENT && (
 | 
			
		||||
            <ComponentPreview displayedValue={item.displayedValue} />
 | 
			
		||||
          )}
 | 
			
		||||
          {actualType === ItemTypes.DYNAMIC_ZONE && (
 | 
			
		||||
            <ComponentPreview displayedValue={item.displayedValue} />
 | 
			
		||||
          )}
 | 
			
		||||
          {actualType === ItemTypes.RELATION && (
 | 
			
		||||
            <RelationDragPreview
 | 
			
		||||
              displayedValue={item.displayedValue}
 | 
			
		||||
              status={item.status}
 | 
			
		||||
              width={item.width}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </LayoutDndProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default CustomDragLayer;
 | 
			
		||||
@ -0,0 +1,26 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useIntl } from 'react-intl';
 | 
			
		||||
import { Status, Typography } from '@strapi/design-system';
 | 
			
		||||
 | 
			
		||||
import { getTrad } from '../../../../utils';
 | 
			
		||||
 | 
			
		||||
export function PublicationState({ isPublished }) {
 | 
			
		||||
  const { formatMessage } = useIntl();
 | 
			
		||||
  const variant = isPublished ? 'success' : 'secondary';
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Status showBullet={false} variant={variant} size="S" width="min-content">
 | 
			
		||||
      <Typography fontWeight="bold" textColor={`${variant}700`}>
 | 
			
		||||
        {formatMessage({
 | 
			
		||||
          id: getTrad(`containers.List.${isPublished ? 'published' : 'draft'}`),
 | 
			
		||||
          defaultMessage: isPublished ? 'Published' : 'Draft',
 | 
			
		||||
        })}
 | 
			
		||||
      </Typography>
 | 
			
		||||
    </Status>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PublicationState.propTypes = {
 | 
			
		||||
  isPublished: PropTypes.bool.isRequired,
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
export * from './PublicationState';
 | 
			
		||||
@ -0,0 +1,42 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { render } from '@testing-library/react';
 | 
			
		||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
 | 
			
		||||
import { IntlProvider } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import { PublicationState } from '..';
 | 
			
		||||
 | 
			
		||||
const ComponentFixture = (props) => (
 | 
			
		||||
  <ThemeProvider theme={lightTheme}>
 | 
			
		||||
    <IntlProvider locale="en" messages={{}}>
 | 
			
		||||
      <PublicationState {...props} />
 | 
			
		||||
    </IntlProvider>
 | 
			
		||||
  </ThemeProvider>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const setup = (props) => render(<ComponentFixture {...props} />);
 | 
			
		||||
 | 
			
		||||
describe('DynamicTable | PublicationState', () => {
 | 
			
		||||
  test('render draft state', () => {
 | 
			
		||||
    const { container, getByText } = setup({ isPublished: false });
 | 
			
		||||
 | 
			
		||||
    // retreive styles of rendered component, rather than the container
 | 
			
		||||
    const statusNodeStyle = window.getComputedStyle(container.firstChild);
 | 
			
		||||
    const textNode = getByText('Draft');
 | 
			
		||||
 | 
			
		||||
    expect(textNode).toBeInTheDocument();
 | 
			
		||||
    expect(statusNodeStyle).toHaveProperty('background-color', 'rgb(234, 245, 255)');
 | 
			
		||||
    expect(window.getComputedStyle(textNode)).toHaveProperty('color', 'rgb(12, 117, 175)');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('render published state', () => {
 | 
			
		||||
    const { container, getByText } = setup({ isPublished: true });
 | 
			
		||||
 | 
			
		||||
    // retreive styles of rendered component, rather than the container
 | 
			
		||||
    const statusNodeStyle = window.getComputedStyle(container.firstChild);
 | 
			
		||||
    const textNode = getByText('Published');
 | 
			
		||||
 | 
			
		||||
    expect(textNode).toBeInTheDocument();
 | 
			
		||||
    expect(statusNodeStyle).toHaveProperty('background-color', 'rgb(234, 251, 231)');
 | 
			
		||||
    expect(window.getComputedStyle(textNode)).toHaveProperty('color', 'rgb(50, 128, 72)');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -19,6 +19,21 @@ exports[`DynamicTable / Cellcontent / RelationMultiple renders and matches the s
 | 
			
		||||
  min-width: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.c6 {
 | 
			
		||||
  font-size: 0.75rem;
 | 
			
		||||
  line-height: 1.33;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  color: #ffffff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.c11 {
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  font-size: 0.6875rem;
 | 
			
		||||
  line-height: 1.45;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  color: #666687;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.c1 {
 | 
			
		||||
  -webkit-align-items: center;
 | 
			
		||||
  -webkit-box-align: center;
 | 
			
		||||
@ -84,21 +99,6 @@ exports[`DynamicTable / Cellcontent / RelationMultiple renders and matches the s
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.c6 {
 | 
			
		||||
  font-size: 0.75rem;
 | 
			
		||||
  line-height: 1.33;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  color: #ffffff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.c11 {
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  font-size: 0.6875rem;
 | 
			
		||||
  line-height: 1.45;
 | 
			
		||||
  text-transform: uppercase;
 | 
			
		||||
  color: #666687;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.c10 {
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  height: 1.5rem;
 | 
			
		||||
@ -109,13 +109,13 @@ exports[`DynamicTable / Cellcontent / RelationMultiple renders and matches the s
 | 
			
		||||
  outline: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.c2 svg {
 | 
			
		||||
.c2 > svg {
 | 
			
		||||
  height: 12px;
 | 
			
		||||
  width: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.c2 svg > g,
 | 
			
		||||
.c2 svg path {
 | 
			
		||||
.c2 > svg > g,
 | 
			
		||||
.c2 > svg path {
 | 
			
		||||
  fill: #ffffff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user