diff --git a/.github/actions/check-pr-status/jest.config.js b/.github/actions/check-pr-status/jest.config.js new file mode 100644 index 0000000000..35fb10228e --- /dev/null +++ b/.github/actions/check-pr-status/jest.config.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = { + preset: '../../../jest-preset.unit.js', + displayName: 'Github action check-pr-status', +}; diff --git a/.github/actions/check-pr-status/package.json b/.github/actions/check-pr-status/package.json index f83426e776..dbbac76254 100644 --- a/.github/actions/check-pr-status/package.json +++ b/.github/actions/check-pr-status/package.json @@ -1,6 +1,6 @@ { "name": "check-pr-status", - "version": "4.9.2", + "version": "4.10.1", "main": "dist/index.js", "license": "MIT", "private": true, diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9f5988f1bc..82cb946a0e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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' diff --git a/.github/filters.yaml b/.github/filters.yaml new file mode 100644 index 0000000000..bfcd89e9ea --- /dev/null +++ b/.github/filters.yaml @@ -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/**' diff --git a/.github/jest.config.js b/.github/jest.config.js deleted file mode 100644 index 39009ed0a8..0000000000 --- a/.github/jest.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - displayName: '.github', -}; diff --git a/.github/workflows/skipped_tests.yml b/.github/workflows/skipped_tests.yml index ba007631bc..f6e81dd284 100644 --- a/.github/workflows/skipped_tests.yml +++ b/.github/workflows/skipped_tests.yml @@ -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] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ff42dc63c1..df0c237ec8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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: diff --git a/.husky/pre-commit b/.husky/pre-commit index 70b64877d2..5a182ef106 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,4 +2,3 @@ . "$(dirname -- "$0")/_/husky.sh" yarn lint-staged -yarn nx affected:lint --uncommitted --nx-ignore-cycles diff --git a/api-tests/core/admin/admin-permission.test.api.js b/api-tests/core/admin/admin-permission.test.api.js index 6e08d6c257..e1df822ade 100644 --- a/api-tests/core/admin/admin-permission.test.api.js +++ b/api-tests/core/admin/admin-permission.test.api.js @@ -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", diff --git a/api-tests/core/admin/ee/provider-login.test.api.js b/api-tests/core/admin/ee/provider-login.test.api.js index b722cfe3dc..f4c120cc01 100644 --- a/api-tests/core/admin/ee/provider-login.test.api.js +++ b/api-tests/core/admin/ee/provider-login.test.api.js @@ -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 () => { diff --git a/api-tests/core/admin/ee/review-workflows.test.api.js b/api-tests/core/admin/ee/review-workflows.test.api.js new file mode 100644 index 0000000000..4246c9aec9 --- /dev/null +++ b/api-tests/core/admin/ee/review-workflows.test.api.js @@ -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); + } + }); + }); +}); diff --git a/docs/docs/core/admin/ee/intro.md b/docs/docs/core/admin/ee/intro.md new file mode 100644 index 0000000000..12a3ff99bb --- /dev/null +++ b/docs/docs/core/admin/ee/intro.md @@ -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'; + + +``` diff --git a/docs/docs/core/admin/ee/review-workflows.md b/docs/docs/core/admin/ee/review-workflows.md new file mode 100644 index 0000000000..fdd7fdd91f --- /dev/null +++ b/docs/docs/core/admin/ee/review-workflows.md @@ -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) diff --git a/docs/docs/core/admin/intro.md b/docs/docs/core/admin/intro.md new file mode 100644 index 0000000000..123c42af8d --- /dev/null +++ b/docs/docs/core/admin/intro.md @@ -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'; + + +``` diff --git a/docs/docs/core/content-manager/review-workflows.mdx b/docs/docs/core/content-manager/review-workflows.mdx new file mode 100644 index 0000000000..f762cac565 --- /dev/null +++ b/docs/docs/core/content-manager/review-workflows.mdx @@ -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 +} +``` diff --git a/docs/docs/core/content-type-builder/review-workflows.mdx b/docs/docs/core/content-type-builder/review-workflows.mdx new file mode 100644 index 0000000000..22f6547657 --- /dev/null +++ b/docs/docs/core/content-type-builder/review-workflows.mdx @@ -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 + } +} +``` diff --git a/docs/docs/core/content-manager/hooks/use-callback-ref.mdx b/docs/docs/core/helper-plugin/hooks/use-callback-ref.mdx similarity index 90% rename from docs/docs/core/content-manager/hooks/use-callback-ref.mdx rename to docs/docs/core/helper-plugin/hooks/use-callback-ref.mdx index e31fe1ce10..a468cbe06d 100644 --- a/docs/docs/core/content-manager/hooks/use-callback-ref.mdx +++ b/docs/docs/core/helper-plugin/hooks/use-callback-ref.mdx @@ -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 diff --git a/docs/docs/core/settings/intro.mdx b/docs/docs/core/settings/intro.mdx new file mode 100644 index 0000000000..32806bb2d9 --- /dev/null +++ b/docs/docs/core/settings/intro.mdx @@ -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'; + + +``` diff --git a/docs/docs/core/settings/review-workflows.mdx b/docs/docs/core/settings/review-workflows.mdx new file mode 100644 index 0000000000..5c0b9acf6e --- /dev/null +++ b/docs/docs/core/settings/review-workflows.mdx @@ -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. diff --git a/docs/sidebars.js b/docs/sidebars.js index c0f0ba767a..8a7c391a5f 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -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', diff --git a/examples/getstarted/config/plugins.js b/examples/getstarted/config/plugins.js index a6bb88b407..48ef1edb79 100644 --- a/examples/getstarted/config/plugins.js +++ b/examples/getstarted/config/plugins.js @@ -17,7 +17,7 @@ module.exports = () => ({ documentation: { config: { info: { - version: '2.0.0', + version: '1.0.0', }, }, }, diff --git a/examples/getstarted/package.json b/examples/getstarted/package.json index f1543d185d..a15f3674d3 100644 --- a/examples/getstarted/package.json +++ b/examples/getstarted/package.json @@ -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": { diff --git a/examples/getstarted/src/api/address/content-types/address/schema.json b/examples/getstarted/src/api/address/content-types/address/schema.json index 90bfbaa50f..b6534f5759 100755 --- a/examples/getstarted/src/api/address/content-types/address/schema.json +++ b/examples/getstarted/src/api/address/content-types/address/schema.json @@ -9,8 +9,8 @@ "name": "Address" }, "options": { - "draftAndPublish": false, - "comment": "" + "reviewWorkflows": true, + "draftAndPublish": false }, "pluginOptions": {}, "attributes": { diff --git a/examples/getstarted/src/api/category/content-types/category/schema.json b/examples/getstarted/src/api/category/content-types/category/schema.json index c13d00bcdb..48e29ba004 100755 --- a/examples/getstarted/src/api/category/content-types/category/schema.json +++ b/examples/getstarted/src/api/category/content-types/category/schema.json @@ -9,8 +9,8 @@ "name": "Category" }, "options": { - "draftAndPublish": true, - "comment": "" + "reviewWorkflows": true, + "draftAndPublish": true }, "pluginOptions": { "i18n": { diff --git a/examples/kitchensink-ts/package.json b/examples/kitchensink-ts/package.json index dcd0f4feb9..22ddb7afc2 100644 --- a/examples/kitchensink-ts/package.json +++ b/examples/kitchensink-ts/package.json @@ -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" diff --git a/examples/kitchensink/package.json b/examples/kitchensink/package.json index 1919a7533b..14ff7c338c 100644 --- a/examples/kitchensink/package.json +++ b/examples/kitchensink/package.json @@ -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", diff --git a/jest-preset.front.js b/jest-preset.front.js index 438f6bfccb..37dac4fc8d 100644 --- a/jest-preset.front.js +++ b/jest-preset.front.js @@ -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$': [ diff --git a/jest.config.front.js b/jest.config.front.js new file mode 100644 index 0000000000..be0d1bce5f --- /dev/null +++ b/jest.config.front.js @@ -0,0 +1,12 @@ +'use strict'; + +/** @type {import('jest').Config} */ +const config = { + projects: [ + '/packages/plugins/*/jest.config.front.js', + '/packages/core/*/jest.config.front.js', + '/scripts/*/jest.config.front.js', + ], +}; + +module.exports = config; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000000..6ef1408f68 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,15 @@ +'use strict'; + +/** @type {import('jest').Config} */ +const config = { + projects: [ + '/packages/plugins/*/jest.config.js', + '/packages/utils/*/jest.config.js', + '/packages/generators/*/jest.config.js', + '/packages/core/*/jest.config.js', + '/packages/providers/*/jest.config.js', + '/.github/actions/*/jest.config.js', + ], +}; + +module.exports = config; diff --git a/lerna.json b/lerna.json index 35deedccd2..4efa55cc09 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "4.9.2", + "version": "4.10.1", "packages": ["packages/*", "examples/*"], "npmClient": "yarn", "useWorkspaces": true, diff --git a/lint-staged.config.js b/lint-staged.config.js new file mode 100644 index 0000000000..4469ce4c24 --- /dev/null +++ b/lint-staged.config.js @@ -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'], +}; diff --git a/nx.json b/nx.json index 9e217a281e..d100fd87c7 100644 --- a/nx.json +++ b/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": { diff --git a/package.json b/package.json index 4542d36995..a4a24b6da3 100644 --- a/package.json +++ b/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", diff --git a/packages/admin-test-utils/.eslintignore b/packages/admin-test-utils/.eslintignore index 1723d82cf5..0ea0ae631d 100644 --- a/packages/admin-test-utils/.eslintignore +++ b/packages/admin-test-utils/.eslintignore @@ -1,2 +1,3 @@ node_modules/ +dist/ .eslintrc.js diff --git a/packages/admin-test-utils/.eslintrc.js b/packages/admin-test-utils/.eslintrc.js index 530684a539..a1dca8d1af 100644 --- a/packages/admin-test-utils/.eslintrc.js +++ b/packages/admin-test-utils/.eslintrc.js @@ -1,7 +1,4 @@ module.exports = { root: true, - extends: ['custom/back'], - rules: { - 'import/no-extraneous-dependencies': 'off', - }, + extends: ['custom/typescript'], }; diff --git a/packages/admin-test-utils/.gitignore b/packages/admin-test-utils/.gitignore new file mode 100755 index 0000000000..9585e32ced --- /dev/null +++ b/packages/admin-test-utils/.gitignore @@ -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/ diff --git a/packages/admin-test-utils/LICENSE b/packages/admin-test-utils/LICENSE new file mode 100644 index 0000000000..db018546b5 --- /dev/null +++ b/packages/admin-test-utils/LICENSE @@ -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. diff --git a/packages/generators/app/lib/resources/files/js/database/migrations/.gitkeep b/packages/admin-test-utils/README.md similarity index 100% rename from packages/generators/app/lib/resources/files/js/database/migrations/.gitkeep rename to packages/admin-test-utils/README.md diff --git a/packages/admin-test-utils/custom.d.ts b/packages/admin-test-utils/custom.d.ts new file mode 100644 index 0000000000..ac37bd91b2 --- /dev/null +++ b/packages/admin-test-utils/custom.d.ts @@ -0,0 +1,15 @@ +export {}; + +declare global { + interface Window { + strapi: { + backendURL: string; + isEE: boolean; + features: { + SSO: 'sso'; + isEnabled: (featureName?: string) => boolean; + }; + projectType: string; + }; + } +} diff --git a/packages/admin-test-utils/lib/fixtures/index.js b/packages/admin-test-utils/lib/fixtures/index.js deleted file mode 100644 index 9a1e1b040f..0000000000 --- a/packages/admin-test-utils/lib/fixtures/index.js +++ /dev/null @@ -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, -}; diff --git a/packages/admin-test-utils/lib/index.js b/packages/admin-test-utils/lib/index.js deleted file mode 100644 index 6fb0644136..0000000000 --- a/packages/admin-test-utils/lib/index.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const fixtures = require('./fixtures'); - -module.exports = { - fixtures, -}; diff --git a/packages/admin-test-utils/lib/mocks/IntersectionObserver.js b/packages/admin-test-utils/lib/mocks/IntersectionObserver.js deleted file mode 100644 index aef6fb7c47..0000000000 --- a/packages/admin-test-utils/lib/mocks/IntersectionObserver.js +++ /dev/null @@ -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; diff --git a/packages/admin-test-utils/lib/mocks/LocalStorageMock.js b/packages/admin-test-utils/lib/mocks/LocalStorageMock.js deleted file mode 100644 index 701520ac77..0000000000 --- a/packages/admin-test-utils/lib/mocks/LocalStorageMock.js +++ /dev/null @@ -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(), -}); diff --git a/packages/admin-test-utils/lib/mocks/ResizeObserver.js b/packages/admin-test-utils/lib/mocks/ResizeObserver.js deleted file mode 100644 index da84baab08..0000000000 --- a/packages/admin-test-utils/lib/mocks/ResizeObserver.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -class ResizeObserverMock { - constructor() { - this.disconnect = () => null; - this.observe = () => null; - this.unobserve = () => null; - } -} - -global.ResizeObserver = ResizeObserverMock; diff --git a/packages/admin-test-utils/lib/mocks/cssModule.js b/packages/admin-test-utils/lib/mocks/cssModule.js deleted file mode 100644 index 83bf2e2338..0000000000 --- a/packages/admin-test-utils/lib/mocks/cssModule.js +++ /dev/null @@ -1 +0,0 @@ -// module.exports = 'CSS_MODULE'; diff --git a/packages/admin-test-utils/lib/mocks/fetch.js b/packages/admin-test-utils/lib/mocks/fetch.js deleted file mode 100644 index 41c2e892f8..0000000000 --- a/packages/admin-test-utils/lib/mocks/fetch.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -// Required as long as we are running tests on node@14 and node@16 -require('whatwg-fetch'); diff --git a/packages/admin-test-utils/lib/mocks/image.js b/packages/admin-test-utils/lib/mocks/image.js deleted file mode 100644 index ffc07df9a5..0000000000 --- a/packages/admin-test-utils/lib/mocks/image.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -module.exports = 'IMAGE_MOCK'; diff --git a/packages/admin-test-utils/lib/mocks/mockRangeApi.js b/packages/admin-test-utils/lib/mocks/mockRangeApi.js deleted file mode 100644 index d222d778fe..0000000000 --- a/packages/admin-test-utils/lib/mocks/mockRangeApi.js +++ /dev/null @@ -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; -}; diff --git a/packages/admin-test-utils/lib/mocks/windowMatchMedia.js b/packages/admin-test-utils/lib/mocks/windowMatchMedia.js deleted file mode 100644 index 2d384f79ca..0000000000 --- a/packages/admin-test-utils/lib/mocks/windowMatchMedia.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -global.window.matchMedia = jest.fn(() => false); diff --git a/packages/admin-test-utils/lib/setup/prop-types.js b/packages/admin-test-utils/lib/setup/prop-types.js deleted file mode 100644 index 17928263b9..0000000000 --- a/packages/admin-test-utils/lib/setup/prop-types.js +++ /dev/null @@ -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; -}); diff --git a/packages/admin-test-utils/lib/setup/strapi.js b/packages/admin-test-utils/lib/setup/strapi.js deleted file mode 100644 index ae36c8d04d..0000000000 --- a/packages/admin-test-utils/lib/setup/strapi.js +++ /dev/null @@ -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}`; diff --git a/packages/admin-test-utils/lib/setup/styled-components.js b/packages/admin-test-utils/lib/setup/styled-components.js deleted file mode 100644 index 5a13a5c611..0000000000 --- a/packages/admin-test-utils/lib/setup/styled-components.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -require('jest-styled-components'); diff --git a/packages/admin-test-utils/lib/setup/test-bundler.js b/packages/admin-test-utils/lib/setup/test-bundler.js deleted file mode 100644 index 6470e5a794..0000000000 --- a/packages/admin-test-utils/lib/setup/test-bundler.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -const noop = () => {}; -// eslint-disable-next-line no-undef -Object.defineProperty(window, 'scrollTo', { value: noop, writable: true }); diff --git a/packages/admin-test-utils/package.json b/packages/admin-test-utils/package.json index 24ee4b5af5..586af47ba8 100644 --- a/packages/admin-test-utils/package.json +++ b/packages/admin-test-utils/package.json @@ -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": { diff --git a/packages/admin-test-utils/src/after-env.ts b/packages/admin-test-utils/src/after-env.ts new file mode 100644 index 0000000000..c488bddf0d --- /dev/null +++ b/packages/admin-test-utils/src/after-env.ts @@ -0,0 +1,2 @@ +import '@testing-library/jest-dom'; +import 'jest-styled-components'; diff --git a/packages/admin-test-utils/src/environment.ts b/packages/admin-test-utils/src/environment.ts new file mode 100644 index 0000000000..1fe0082818 --- /dev/null +++ b/packages/admin-test-utils/src/environment.ts @@ -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; + + 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(); diff --git a/packages/admin-test-utils/src/file-mock.ts b/packages/admin-test-utils/src/file-mock.ts new file mode 100644 index 0000000000..112fb2e104 --- /dev/null +++ b/packages/admin-test-utils/src/file-mock.ts @@ -0,0 +1 @@ +export default 'IMAGE_MOCK'; diff --git a/packages/admin-test-utils/lib/fixtures/collectionTypes/address.js b/packages/admin-test-utils/src/fixtures/collection-types/address.ts similarity index 99% rename from packages/admin-test-utils/lib/fixtures/collectionTypes/address.js rename to packages/admin-test-utils/src/fixtures/collection-types/address.ts index 72b6f75710..82a1977def 100644 --- a/packages/admin-test-utils/lib/fixtures/collectionTypes/address.js +++ b/packages/admin-test-utils/src/fixtures/collection-types/address.ts @@ -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 }; diff --git a/packages/admin-test-utils/src/fixtures/collection-types/index.ts b/packages/admin-test-utils/src/fixtures/collection-types/index.ts new file mode 100644 index 0000000000..d19aefed5f --- /dev/null +++ b/packages/admin-test-utils/src/fixtures/collection-types/index.ts @@ -0,0 +1,3 @@ +import { address, Address } from './address'; + +export { Address, address }; diff --git a/packages/admin-test-utils/src/fixtures/index.ts b/packages/admin-test-utils/src/fixtures/index.ts new file mode 100644 index 0000000000..8876e21430 --- /dev/null +++ b/packages/admin-test-utils/src/fixtures/index.ts @@ -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 }; diff --git a/packages/admin-test-utils/lib/fixtures/metaData/address.js b/packages/admin-test-utils/src/fixtures/meta-data/address.ts similarity index 97% rename from packages/admin-test-utils/lib/fixtures/metaData/address.js rename to packages/admin-test-utils/src/fixtures/meta-data/address.ts index c1006244bb..31ee936f77 100644 --- a/packages/admin-test-utils/lib/fixtures/metaData/address.js +++ b/packages/admin-test-utils/src/fixtures/meta-data/address.ts @@ -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 }; diff --git a/packages/admin-test-utils/src/fixtures/meta-data/index.ts b/packages/admin-test-utils/src/fixtures/meta-data/index.ts new file mode 100644 index 0000000000..d19aefed5f --- /dev/null +++ b/packages/admin-test-utils/src/fixtures/meta-data/index.ts @@ -0,0 +1,3 @@ +import { address, Address } from './address'; + +export { Address, address }; diff --git a/packages/admin-test-utils/lib/fixtures/permissions/admin-permissions.js b/packages/admin-test-utils/src/fixtures/permissions/admin-permissions.ts similarity index 95% rename from packages/admin-test-utils/lib/fixtures/permissions/admin-permissions.js rename to packages/admin-test-utils/src/fixtures/permissions/admin-permissions.ts index 939f028a6d..2d080420ec 100644 --- a/packages/admin-test-utils/lib/fixtures/permissions/admin-permissions.js +++ b/packages/admin-test-utils/src/fixtures/permissions/admin-permissions.ts @@ -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 }; diff --git a/packages/admin-test-utils/lib/fixtures/permissions/content-manager-permissions.js b/packages/admin-test-utils/src/fixtures/permissions/content-manager-permissions.ts similarity index 90% rename from packages/admin-test-utils/lib/fixtures/permissions/content-manager-permissions.js rename to packages/admin-test-utils/src/fixtures/permissions/content-manager-permissions.ts index 11e0d97926..f1cbd35284 100644 --- a/packages/admin-test-utils/lib/fixtures/permissions/content-manager-permissions.js +++ b/packages/admin-test-utils/src/fixtures/permissions/content-manager-permissions.ts @@ -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 }; diff --git a/packages/admin-test-utils/lib/fixtures/permissions/content-type-builder-permissions.js b/packages/admin-test-utils/src/fixtures/permissions/content-type-builder-permissions.ts similarity index 84% rename from packages/admin-test-utils/lib/fixtures/permissions/content-type-builder-permissions.js rename to packages/admin-test-utils/src/fixtures/permissions/content-type-builder-permissions.ts index 84b5450eb0..76752b40b5 100644 --- a/packages/admin-test-utils/lib/fixtures/permissions/content-type-builder-permissions.js +++ b/packages/admin-test-utils/src/fixtures/permissions/content-type-builder-permissions.ts @@ -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 }; diff --git a/packages/admin-test-utils/src/fixtures/permissions/index.ts b/packages/admin-test-utils/src/fixtures/permissions/index.ts new file mode 100644 index 0000000000..7eda9a85b8 --- /dev/null +++ b/packages/admin-test-utils/src/fixtures/permissions/index.ts @@ -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, +}; diff --git a/packages/admin-test-utils/lib/fixtures/store/index.js b/packages/admin-test-utils/src/fixtures/store/index.ts similarity index 91% rename from packages/admin-test-utils/lib/fixtures/store/index.js rename to packages/admin-test-utils/src/fixtures/store/index.ts index 3d304fd3ee..0a2e0d5a45 100644 --- a/packages/admin-test-utils/lib/fixtures/store/index.js +++ b/packages/admin-test-utils/src/fixtures/store/index.ts @@ -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(), }; diff --git a/packages/admin-test-utils/src/global-setup.ts b/packages/admin-test-utils/src/global-setup.ts new file mode 100644 index 0000000000..94261d1fe3 --- /dev/null +++ b/packages/admin-test-utils/src/global-setup.ts @@ -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; diff --git a/packages/admin-test-utils/src/index.ts b/packages/admin-test-utils/src/index.ts new file mode 100644 index 0000000000..c3d1f372dd --- /dev/null +++ b/packages/admin-test-utils/src/index.ts @@ -0,0 +1 @@ +export * as fixtures from './fixtures'; diff --git a/packages/admin-test-utils/tsconfig.eslint.json b/packages/admin-test-utils/tsconfig.eslint.json new file mode 100644 index 0000000000..a8e84f79ac --- /dev/null +++ b/packages/admin-test-utils/tsconfig.eslint.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + }, +} diff --git a/packages/admin-test-utils/tsconfig.json b/packages/admin-test-utils/tsconfig.json new file mode 100644 index 0000000000..18b66b12e7 --- /dev/null +++ b/packages/admin-test-utils/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "tsconfig/base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src", "./custom.d.ts"], + "exclude": ["node_modules", "**/__tests__/**"] +} diff --git a/packages/cli/create-strapi-app/package.json b/packages/cli/create-strapi-app/package.json index 4e4d05f703..a1f0dac20d 100644 --- a/packages/cli/create-strapi-app/package.json +++ b/packages/cli/create-strapi-app/package.json @@ -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", diff --git a/packages/cli/create-strapi-starter/package.json b/packages/cli/create-strapi-starter/package.json index 2e4a4d527f..9e899d78f5 100644 --- a/packages/cli/create-strapi-starter/package.json +++ b/packages/cli/create-strapi-starter/package.json @@ -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", diff --git a/packages/cli/create-strapi-starter/src/index.d.ts b/packages/cli/create-strapi-starter/src/index.d.ts deleted file mode 100644 index 00494af403..0000000000 --- a/packages/cli/create-strapi-starter/src/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '@strapi/generate-new'; diff --git a/packages/cli/create-strapi-starter/src/utils/build-starter.ts b/packages/cli/create-strapi-starter/src/utils/build-starter.ts index 09a9a3a605..4200fde750 100644 --- a/packages/cli/create-strapi-starter/src/utils/build-starter.ts +++ b/packages/cli/create-strapi-starter/src/utils/build-starter.ts @@ -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 }); diff --git a/packages/cli/create-strapi-starter/src/utils/has-yarn.ts b/packages/cli/create-strapi-starter/src/utils/has-yarn.ts index 8a86a2ad0f..cf3f49bf92 100644 --- a/packages/cli/create-strapi-starter/src/utils/has-yarn.ts +++ b/packages/cli/create-strapi-starter/src/utils/has-yarn.ts @@ -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) { diff --git a/packages/core/admin/admin/src/components/AuthenticatedApp/index.js b/packages/core/admin/admin/src/components/AuthenticatedApp/index.js index a848486928..f8465ffcd4 100644 --- a/packages/core/admin/admin/src/components/AuthenticatedApp/index.js +++ b/packages/core/admin/admin/src/components/AuthenticatedApp/index.js @@ -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 ; } - // TODO add error state + // TODO: add error state if (status === 'error') { return
error...
; } return ( - + - + ); }; diff --git a/packages/core/admin/admin/src/components/AutoReloadOverlayBlockerProvider/Blocker.js b/packages/core/admin/admin/src/components/AutoReloadOverlayBlockerProvider/Blocker.js deleted file mode 100644 index 3ebcdefa0e..0000000000 --- a/packages/core/admin/admin/src/components/AutoReloadOverlayBlockerProvider/Blocker.js +++ /dev/null @@ -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( - - - - - - {formatMessage(title)} - - - - - {formatMessage(description)} - - - - - {displayedIcon === 'reload' && ( - - - - )} - - {displayedIcon === 'time' && ( - - - - )} - - - - - {formatMessage({ - id: 'global.documentation', - defaultMessage: 'Read the documentation', - })} - - - - - , - 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; diff --git a/packages/core/admin/admin/src/components/AutoReloadOverlayBlockerProvider/Overlay.js b/packages/core/admin/admin/src/components/AutoReloadOverlayBlockerProvider/Overlay.js deleted file mode 100644 index 36454a9fc8..0000000000 --- a/packages/core/admin/admin/src/components/AutoReloadOverlayBlockerProvider/Overlay.js +++ /dev/null @@ -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 }; diff --git a/packages/core/admin/admin/src/components/AutoReloadOverlayBlockerProvider/index.js b/packages/core/admin/admin/src/components/AutoReloadOverlayBlockerProvider/index.js deleted file mode 100644 index 089f949d37..0000000000 --- a/packages/core/admin/admin/src/components/AutoReloadOverlayBlockerProvider/index.js +++ /dev/null @@ -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 ( - - - {children} - - ); -}; - -AutoReloadOverlayBlockerProvider.propTypes = { - children: PropTypes.element.isRequired, -}; - -export default AutoReloadOverlayBlockerProvider; diff --git a/packages/core/admin/admin/src/components/DragLayer/DragLayer.js b/packages/core/admin/admin/src/components/DragLayer/DragLayer.js new file mode 100644 index 0000000000..6a4e1ea60e --- /dev/null +++ b/packages/core/admin/admin/src/components/DragLayer/DragLayer.js @@ -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 ( + + + {renderItem({ type: itemType, item })} + + + ); +} + +DragLayer.propTypes = { + renderItem: PropTypes.func.isRequired, +}; diff --git a/packages/core/admin/admin/src/components/DragLayer/index.js b/packages/core/admin/admin/src/components/DragLayer/index.js new file mode 100644 index 0000000000..c3e27fec39 --- /dev/null +++ b/packages/core/admin/admin/src/components/DragLayer/index.js @@ -0,0 +1 @@ +export * from './DragLayer'; diff --git a/packages/core/admin/admin/src/components/GuidedTour/Homepage/components/tests/Stepper.test.js b/packages/core/admin/admin/src/components/GuidedTour/Homepage/components/tests/Stepper.test.js index a0ea246d51..c7b54e63ca 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Homepage/components/tests/Stepper.test.js +++ b/packages/core/admin/admin/src/components/GuidedTour/Homepage/components/tests/Stepper.test.js @@ -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; - } -
@@ -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; } diff --git a/packages/core/admin/admin/src/components/GuidedTour/Homepage/tests/index.test.js b/packages/core/admin/admin/src/components/GuidedTour/Homepage/tests/index.test.js index bba15e3506..05a8a71b6b 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Homepage/tests/index.test.js +++ b/packages/core/admin/admin/src/components/GuidedTour/Homepage/tests/index.test.js @@ -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; } diff --git a/packages/core/admin/admin/src/components/GuidedTour/Modal/tests/index.test.js b/packages/core/admin/admin/src/components/GuidedTour/Modal/tests/index.test.js index 650feb42c7..7d5074ad41 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Modal/tests/index.test.js +++ b/packages/core/admin/admin/src/components/GuidedTour/Modal/tests/index.test.js @@ -117,6 +117,42 @@ describe('', () => { 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('', () => { 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('', () => {
{ 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(); diff --git a/packages/core/admin/admin/src/components/Notifications/Notification/index.js b/packages/core/admin/admin/src/components/Notifications/Notification/index.js deleted file mode 100644 index 02e1291527..0000000000 --- a/packages/core/admin/admin/src/components/Notifications/Notification/index.js +++ /dev/null @@ -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 ( - - {formatMessage({ - id: link.label?.id || link.label, - defaultMessage: link.label?.defaultMessage || link.label?.id || link.label, - })} - - ) : undefined - } - onClose={handleClose} - closeLabel="Close" - title={alertTitle} - variant={variant} - > - {formattedMessage({ - id: message?.id || message, - defaultMessage: message?.defaultMessage || message?.id || message, - values: message?.values, - })} - - ); -}; - -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; diff --git a/packages/core/admin/admin/src/components/Notifications/index.js b/packages/core/admin/admin/src/components/Notifications/index.js deleted file mode 100644 index 58cd832610..0000000000 --- a/packages/core/admin/admin/src/components/Notifications/index.js +++ /dev/null @@ -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 ( - - - {notifications.map((notification) => { - return ( - - ); - })} - - {children} - - ); -}; - -Notifications.propTypes = { - children: PropTypes.element.isRequired, -}; - -export default Notifications; diff --git a/packages/core/admin/admin/src/components/Notifications/reducer.js b/packages/core/admin/admin/src/components/Notifications/reducer.js deleted file mode 100644 index 318aaf0ea3..0000000000 --- a/packages/core/admin/admin/src/components/Notifications/reducer.js +++ /dev/null @@ -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 }; diff --git a/packages/core/admin/admin/src/components/Notifications/tests/index.test.js b/packages/core/admin/admin/src/components/Notifications/tests/index.test.js deleted file mode 100644 index 949ef53c5b..0000000000 --- a/packages/core/admin/admin/src/components/Notifications/tests/index.test.js +++ /dev/null @@ -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('', () => { - it('renders and matches the snapshot', () => { - const { - container: { firstChild }, - } = render( - - - - -
- - - - - ); - - 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; - } - -
- `); - }); - - it('should display a notification correctly', async () => { - const Button = () => { - const toggleNotification = useNotification(); - - const handleClick = () => { - toggleNotification({ type: 'success', message: 'simple notif' }); - }; - - return ( - - ); - }; - - render( - - - - - - ); - }; - - render( - - - - - - )} - - - ); - } - - return ( - - - - @@ -127,22 +71,10 @@ const WysiwygNav = ({ + } /> + } /> onActionClick('Bold', editorRef)} - id="Bold" - label="Bold" - name="Bold" - icon={} - /> - onActionClick('Italic', editorRef)} - id="Italic" - label="Italic" - name="Italic" - icon={} - /> - onActionClick('Underline', editorRef)} + disabled id="Underline" label="Underline" name="Underline" @@ -150,90 +82,145 @@ const WysiwygNav = ({ /> - } - /> - {visiblePopover && ( - - - - - onActionClick('Strikethrough', editorRef, handleTogglePopover)} - id="Strikethrough" - label="Strikethrough" - name="Strikethrough" - icon={} - /> - onActionClick('BulletList', editorRef, handleTogglePopover)} - id="BulletList" - label="BulletList" - name="BulletList" - icon={} - /> - onActionClick('NumberList', editorRef, handleTogglePopover)} - id="NumberList" - label="NumberList" - name="NumberList" - icon={} - /> - - - onActionClick('Code', editorRef, handleTogglePopover)} - id="Code" - label="Code" - name="Code" - icon={} - /> - { - handleTogglePopover(); - onToggleMediaLib(); - }} - id="Image" - label="Image" - name="Image" - icon={} - /> - onActionClick('Link', editorRef, handleTogglePopover)} - id="Link" - label="Link" - name="Link" - // eslint-disable-next-line jsx-a11y/anchor-is-valid - icon={} - /> - onActionClick('Quote', editorRef, handleTogglePopover)} - id="Quote" - label="Quote" - name="Quote" - icon={} - /> - - - - - )} - + } /> + - {onTogglePreviewMode && ( + {!isExpandMode && ( )} - + ); + } + + return ( + + + + + + onActionClick('Bold', editorRef)} + id="Bold" + label="Bold" + name="Bold" + icon={} + /> + onActionClick('Italic', editorRef)} + id="Italic" + label="Italic" + name="Italic" + icon={} + /> + onActionClick('Underline', editorRef)} + id="Underline" + label="Underline" + name="Underline" + icon={} + /> + + + } + /> + {visiblePopover && ( + + + + + onActionClick('Strikethrough', editorRef, handleTogglePopover)} + id="Strikethrough" + label="Strikethrough" + name="Strikethrough" + icon={} + /> + onActionClick('BulletList', editorRef, handleTogglePopover)} + id="BulletList" + label="BulletList" + name="BulletList" + icon={} + /> + onActionClick('NumberList', editorRef, handleTogglePopover)} + id="NumberList" + label="NumberList" + name="NumberList" + icon={} + /> + + + onActionClick('Code', editorRef, handleTogglePopover)} + id="Code" + label="Code" + name="Code" + icon={} + /> + { + handleTogglePopover(); + onToggleMediaLib(); + }} + id="Image" + label="Image" + name="Image" + icon={} + /> + onActionClick('Link', editorRef, handleTogglePopover)} + id="Link" + label="Link" + name="Link" + // eslint-disable-next-line jsx-a11y/anchor-is-valid + icon={} + /> + onActionClick('Quote', editorRef, handleTogglePopover)} + id="Quote" + label="Quote" + name="Quote" + icon={} + /> + + + + + )} + + + {onTogglePreviewMode && ( + + )} + ); }; @@ -255,3 +242,17 @@ WysiwygNav.propTypes = { }; export default WysiwygNav; + +const StyledFlex = styled(Flex)` + /* Hide the label, every input needs a label. */ + label { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + } +`; diff --git a/packages/core/admin/admin/src/content-manager/components/Wysiwyg/tests/__snapshots__/index.test.js.snap b/packages/core/admin/admin/src/content-manager/components/Wysiwyg/tests/__snapshots__/index.test.js.snap deleted file mode 100644 index a028baa1e5..0000000000 --- a/packages/core/admin/admin/src/content-manager/components/Wysiwyg/tests/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,1196 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Wysiwyg render and actions buttons should render the Wysiwyg 1`] = ` -.c4 { - border-radius: 4px; - border-style: solid; - border-width: 1px; - border-color: #dcdce4; -} - -.c5 { - background: #f6f6f9; - padding: 8px; -} - -.c12 { - padding-right: 16px; - padding-left: 16px; -} - -.c14 { - padding-left: 12px; -} - -.c19 { - background: #ffffff; - padding: 8px; - border-radius: 4px; - border-color: #dcdce4; - border: 1px solid #dcdce4; - width: 2rem; - height: 2rem; - cursor: pointer; -} - -.c27 { - background: #4945ff; - padding: 8px; - padding-right: 16px; - padding-left: 16px; - border-radius: 4px; - border-color: #4945ff; - border: 1px solid #4945ff; - cursor: pointer; -} - -.c33 { - background: #f6f6f9; - padding: 8px; - border-radius: 4px; -} - -.c35 { - background: #ffffff; - padding: 8px; - border-radius: 4px; - border-color: #dcdce4; - border: 1px solid #dcdce4; - cursor: pointer; -} - -.c0 { - -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: 4px; -} - -.c1 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - gap: 4px; -} - -.c6 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-pack: justify; - -webkit-justify-content: space-between; - -ms-flex-pack: justify; - justify-content: space-between; -} - -.c7 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; -} - -.c8 { - -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; -} - -.c20 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; -} - -.c28 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - gap: 8px; -} - -.c34 { - -webkit-align-items: flex-end; - -webkit-box-align: flex-end; - -ms-flex-align: flex-end; - align-items: flex-end; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; -} - -.c3 { - font-size: 0.75rem; - line-height: 1.33; - font-weight: 600; - color: #32324d; -} - -.c13 { - font-size: 0.875rem; - line-height: 1.43; - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: #666687; -} - -.c30 { - font-size: 0.75rem; - line-height: 1.33; - font-weight: 600; - color: #ffffff; -} - -.c37 { - font-size: 0.875rem; - line-height: 1.43; - color: #32324d; -} - -.c21 { - position: relative; - outline: none; -} - -.c21 svg { - height: 12px; - width: 12px; -} - -.c21 svg > g, -.c21 svg path { - fill: #ffffff; -} - -.c21[aria-disabled='true'] { - pointer-events: none; -} - -.c21:after { - -webkit-transition-property: all; - transition-property: all; - -webkit-transition-duration: 0.2s; - transition-duration: 0.2s; - border-radius: 8px; - content: ''; - position: absolute; - top: -4px; - bottom: -4px; - left: -4px; - right: -4px; - border: 2px solid transparent; -} - -.c21:focus-visible { - outline: none; -} - -.c21:focus-visible:after { - border-radius: 8px; - content: ''; - position: absolute; - top: -5px; - bottom: -5px; - left: -5px; - right: -5px; - border: 2px solid #4945ff; -} - -.c25 { - border: 0; - -webkit-clip: rect(0 0 0 0); - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} - -.c29 { - height: 2rem; - border: 1px solid #dcdce4; - background: #ffffff; -} - -.c29[aria-disabled='true'] { - border: 1px solid #dcdce4; - background: #eaeaef; -} - -.c29[aria-disabled='true'] .c2 { - color: #666687; -} - -.c29[aria-disabled='true'] svg > g,.c29[aria-disabled='true'] svg path { - fill: #666687; -} - -.c29[aria-disabled='true']:active { - border: 1px solid #dcdce4; - background: #eaeaef; -} - -.c29[aria-disabled='true']:active .c2 { - color: #666687; -} - -.c29[aria-disabled='true']:active svg > g,.c29[aria-disabled='true']:active svg path { - fill: #666687; -} - -.c29:hover { - background-color: #f6f6f9; -} - -.c29:active { - background-color: #eaeaef; -} - -.c29 .c2 { - color: #32324d; -} - -.c29 svg > g, -.c29 svg path { - fill: #32324d; -} - -.c9 { - position: relative; - border: 1px solid #dcdce4; - padding-right: 12px; - border-radius: 4px; - background: #ffffff; - overflow: hidden; - min-height: 2rem; - outline: none; - box-shadow: 0; - -webkit-transition-property: border-color,box-shadow,fill; - transition-property: border-color,box-shadow,fill; - -webkit-transition-duration: 0.2s; - transition-duration: 0.2s; -} - -.c9:focus-within { - border: 1px solid #4945ff; - box-shadow: #4945ff 0px 0px 0px 2px; -} - -.c15 { - background: transparent; - border: none; - position: relative; - z-index: 1; -} - -.c15 svg { - height: 0.6875rem; - width: 0.6875rem; -} - -.c15 svg path { - fill: #666687; -} - -.c16 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - background: none; - border: none; -} - -.c16 svg { - width: 0.375rem; -} - -.c10 { - position: absolute; - left: 0; - right: 0; - bottom: 0; - top: 0; - width: 100%; - background: transparent; - border: none; -} - -.c10:focus { - outline: none; -} - -.c10[data-disabled] { - cursor: not-allowed; -} - -.c11 { - width: 100%; -} - -.c23 svg > g, -.c23 svg path { - fill: #8e8ea9; -} - -.c23:hover svg > g, -.c23:hover svg path { - fill: #666687; -} - -.c23:active svg > g, -.c23:active svg path { - fill: #a5a5ba; -} - -.c23[aria-disabled='true'] svg path { - fill: #666687; -} - -.c17 span:first-child button { - border-left: 1px solid #dcdce4; - border-radius: 4px 0 0 4px; -} - -.c17 span:last-child button { - border-radius: 0 4px 4px 0; -} - -.c17 .c22 { - border-radius: 0; - border-left: none; -} - -.c17 .c22 svg path { - fill: #4a4a6a; -} - -.c17 .c22:hover { - background-color: #f6f6f9; -} - -.c17 .c22:hover svg path { - fill: #32324d; -} - -.c17 .c22:active { - background-color: #eaeaef; -} - -.c17 .c22:active svg path { - fill: #212134; -} - -.c17 .c22[aria-disabled='true'] svg path { - fill: #666687; -} - -.c32 { - cursor: auto; - height: 100%; -} - -.c32 .CodeMirror-placeholder { - color: #666687 !important; -} - -.c32 .CodeMirror { - font-size: 0.875rem; - height: 290px; - color: #32324d; - direction: ltr; - font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell, 'Open Sans','Helvetica Neue',sans-serif; -} - -.c32 .CodeMirror-lines { - padding: 12px 16px; -} - -.c32 .CodeMirror-scrollbar-filler, -.c32 .CodeMirror-gutter-filler { - background-color: #ffffff; -} - -.c32 .CodeMirror-gutters { - border-right: 1px solid #ddd; - background-color: #f7f7f7; - white-space: nowrap; -} - -.c32 .CodeMirror-linenumber { - padding: 0 3px 0 5px; - min-width: 20px; - text-align: right; - color: #999; - white-space: nowrap; -} - -.c32 .CodeMirror-guttermarker { - color: black; -} - -.c32 .CodeMirror-guttermarker-subtle { - color: #999; -} - -.c32 .CodeMirror-cursor { - border-left: 1px solid black; - border-right: none; - width: 0; -} - -.c32 .CodeMirror div.CodeMirror-secondarycursor { - border-left: 1px solid silver; -} - -.c32 .cm-fat-cursor .CodeMirror-cursor { - width: auto; - border: 0 !important; - background: #7e7; -} - -.c32 .cm-fat-cursor-mark { - background-color: rgba(20,255,20,0.5); - -webkit-animation: blink 1.06s steps(1) infinite; - -moz-animation: blink 1.06s steps(1) infinite; - -webkit-animation: blink 1.06s steps(1) infinite; - animation: blink 1.06s steps(1) infinite; -} - -.c32 .cm-animate-fat-cursor { - width: auto; - border: 0; - -webkit-animation: blink 1.06s steps(1) infinite; - -moz-animation: blink 1.06s steps(1) infinite; - -webkit-animation: blink 1.06s steps(1) infinite; - animation: blink 1.06s steps(1) infinite; - background-color: #7e7; -} - -.c32 .cm-tab { - display: inline-block; - -webkit-text-decoration: inherit; - text-decoration: inherit; -} - -.c32 .CodeMirror-rulers { - position: absolute; - left: 0; - right: 0; - top: -50px; - bottom: 0; - overflow: hidden; -} - -.c32 .CodeMirror-ruler { - border-left: 1px solid #ccc; - top: 0; - bottom: 0; - position: absolute; -} - -.c32 .cm-header, -.c32 .cm-strong { - font-weight: bold; -} - -.c32 .cm-em { - font-style: italic; -} - -.c32 .cm-link { - -webkit-text-decoration: underline; - text-decoration: underline; -} - -.c32 .cm-strikethrough { - -webkit-text-decoration: line-through; - text-decoration: line-through; -} - -.c32 .CodeMirror-composing { - border-bottom: 2px solid; -} - -.c32 div.CodeMirror span.CodeMirror-matchingbracket { - color: #0b0; -} - -.c32 div.CodeMirror span.CodeMirror-nonmatchingbracket { - color: #a22; -} - -.c32 .CodeMirror-matchingtag { - background: rgba(255,150,0,0.3); -} - -.c32 .CodeMirror-activeline-background { - background: #e8f2ff; -} - -.c32 .CodeMirror { - position: relative; - overflow: hidden; - background: #ffffff; -} - -.c32 .CodeMirror-scroll { - overflow: scroll !important; - margin-bottom: -50px; - margin-right: -50px; - padding-bottom: 50px; - height: 100%; - outline: none; - position: relative; -} - -.c32 .CodeMirror-sizer { - position: relative; - border-right: 50px solid transparent; -} - -.c32 .CodeMirror-vscrollbar, -.c32 .CodeMirror-hscrollbar, -.c32 .CodeMirror-scrollbar-filler, -.c32 .CodeMirror-gutter-filler { - position: absolute; - z-index: 1; - display: none; - outline: none; -} - -.c32 .CodeMirror-vscrollbar { - right: 0; - top: 0; - overflow-x: hidden; - overflow-y: scroll; -} - -.c32 .CodeMirror-hscrollbar { - bottom: 0; - left: 0; - overflow-y: hidden; - overflow-x: scroll; -} - -.c32 .CodeMirror-scrollbar-filler { - right: 0; - bottom: 0; -} - -.c32 .CodeMirror-lines { - cursor: text; - min-height: 1px; -} - -.c32 .CodeMirror pre.CodeMirror-line, -.c32 .CodeMirror pre.CodeMirror-line-like { - -moz-border-radius: 0; - -webkit-border-radius: 0; - border-radius: 0; - border-width: 0; - background: transparent; - font-family: inherit; - font-size: inherit; - margin: 0; - white-space: pre; - word-wrap: normal; - line-height: 1.5; - color: inherit; - position: relative; - overflow: visible; - -webkit-tap-highlight-color: transparent; - -webkit-font-variant-ligatures: contextual; - font-variant-ligatures: contextual; -} - -.c32 .CodeMirror pre.CodeMirror-line-like { - z-index: 2; -} - -.c32 .CodeMirror-wrap pre.CodeMirror-line, -.c32 .CodeMirror-wrap pre.CodeMirror-line-like { - word-wrap: break-word; - white-space: pre-wrap; - word-break: normal; -} - -.c32 .CodeMirror-linebackground { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - z-index: 0; -} - -.c32 .CodeMirror-linewidget { - position: relative; - padding: 0.1px; -} - -.c32 .CodeMirror-rtl pre { - direction: rtl; -} - -.c32 .CodeMirror-code { - outline: none; -} - -.c32 .CodeMirror-scroll, -.c32 .CodeMirror-sizer, -.c32 .CodeMirror-gutter, -.c32 .CodeMirror-gutters, -.c32 .CodeMirror-linenumber { - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -.c32 .CodeMirror-measure { - position: absolute; - width: 100%; - height: 0; - overflow: hidden; - visibility: hidden; -} - -.c32 .CodeMirror-cursor { - position: absolute; - pointer-events: none; - border-color: #32324d; -} - -.c32 .CodeMirror-measure pre { - position: static; -} - -.c32 div.CodeMirror-cursors { - visibility: hidden; - position: relative; -} - -.c32 div.CodeMirror-cursors + div { - z-index: 0 !important; -} - -.c32 div.CodeMirror-dragcursors { - visibility: visible; -} - -.c32 .CodeMirror-focused div.CodeMirror-cursors { - visibility: visible; -} - -.c32 .CodeMirror-selected { - background: #dcdce4; -} - -.c32 .CodeMirror-crosshair { - cursor: crosshair; -} - -.c32 .cm-force-border { - padding-right: 0.1px; -} - -.c32 .cm-tab-wrap-hack:after { - content: ''; -} - -.c32 span.CodeMirror-selectedtext { - background: none; -} - -.c32 span { - color: #32324d !important; -} - -.c24 { - padding: 8px; - outline-offset: -2px !important; -} - -.c24 svg { - width: 1.125rem; - height: 1.125rem; -} - -.c18 { - margin-left: 16px; -} - -.c26 { - margin: 0 8px; - padding: 8px; -} - -.c26 svg { - width: 1.125rem; - height: 1.125rem; -} - -.c31 { - position: relative; - height: calc(100% - 48px); -} - -.c36 { - background-color: transparent; - border: none; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; -} - -.c36 svg { - margin-left: 8px; -} - -.c36 svg path { - fill: #4a4a6a; - width: 0.75rem; - height: 0.75rem; -} - -
-
- - hello world - -
-
-
-
-
-
-
-
- -
-
-
-
-
-
- - - - - - - - - -
- - - -
- -
-
-
-
-