Merge branch 'main' into v5/main

This commit is contained in:
Alexandre Bodin 2023-10-18 12:38:06 +02:00
commit 010308b0cd
122 changed files with 1101 additions and 1726 deletions

View File

@ -20,7 +20,7 @@ https://github.com/strapi/strapi/blob/main/CONTRIBUTING.md#reporting-an-issue
### Required System information
<!-- Please ensure you are using the Node LTS version (v16 or v18 or v20) -->
<!-- Please ensure you are using the Node LTS version (v18 or v20) -->
<!-- Strapi v3 is no longer supported, please update to Strapi v4 -->
<!-- If you are reporting a frontend bug please provide error logs after setting STRAPI_ENFORCE_SOURCEMAPS=true in your .env -->
<!-- This environment variable makes frontend errors easier to read and trace -->

View File

@ -19,7 +19,7 @@ This action checks a PR labels, milestone and status to validate it is ready for
### Requirements
- The code is compatible with Node 16, 18, and 20
- The code is compatible with Node 18, and 20
### Dependencies

View File

@ -1,5 +1,5 @@
name: 'PR Checker'
description: 'Check PR status for mergeability'
runs:
using: 'node16'
using: 'node20'
main: 'dist/index.js'

View File

@ -11,6 +11,5 @@ export JWT_SECRET="aSecret"
opts=($DB_OPTIONS)
jestOptions=($JEST_OPTIONS)
yarn nx run-many --target=build --nx-ignore-cycles --skip-nx-cache
yarn run test:generate-app --appPath=test-apps/api "${opts[@]}"
yarn run test:generate-app:no-build --appPath=test-apps/api "${opts[@]}"
yarn run test:api --no-generate-app "${jestOptions[@]}"

19
.github/actions/run-build/action.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: 'Monorepo build (yarn)'
description: 'Run yarn build with cache enabled'
runs:
using: 'composite'
steps:
- name: ♻️ Restore build cache
uses: actions/cache@v3
id: yarn-build-cache
with:
path: packages/**/dist
key: yarn-build-cache-${{ github.sha }}
- if: ${{ steps.yarn-build-cache.outputs.cache-hit != 'true' }}
name: 📥 Run build
shell: bash
run: yarn nx run-many --target=build --nx-ignore-cycles --skip-nx-cache

View File

@ -18,7 +18,7 @@ jobs:
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
- run: yarn
- run: ./scripts/pre-publish.sh --yes
env:

View File

@ -21,7 +21,7 @@ jobs:
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
- run: yarn
- run: ./scripts/pre-publish.sh --yes
env:

View File

@ -21,7 +21,7 @@ jobs:
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
- run: yarn
- run: ./scripts/remove-dist-tag.sh
env:

View File

@ -42,7 +42,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16, 18, 20]
node: [18, 20]
steps:
- run: echo "Skipped"
@ -62,7 +62,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16, 18, 20]
node: [18, 20]
steps:
- run: echo "Skipped"
@ -72,7 +72,7 @@ jobs:
name: '[CE] API Integration (postgres, node: ${{ matrix.node }})'
strategy:
matrix:
node: [16, 18, 20]
node: [18, 20]
steps:
- run: echo "Skipped"
@ -82,7 +82,7 @@ jobs:
name: '[CE] API Integration (mysql, node: ${{ matrix.node }})'
strategy:
matrix:
node: [16, 18, 20]
node: [18, 20]
steps:
- run: echo "Skipped"
@ -92,7 +92,7 @@ jobs:
name: '[CE] API Integration (mysql:5 , node: ${{ matrix.node }})'
strategy:
matrix:
node: [16, 18, 20]
node: [18, 20]
steps:
- run: echo "Skipped"

View File

@ -35,30 +35,39 @@ jobs:
filters: .github/filters.yaml
lint:
name: 'lint (node: ${{ matrix.node }})'
needs: [changes]
name: 'lint (node: 20)'
runs-on: ubuntu-latest
strategy:
matrix:
node: [18]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
node-version: 20
- uses: nrwl/nx-set-shas@v3
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Run build
run: yarn nx run-many --target=build --nx-ignore-cycles --skip-nx-cache
- name: Monorepo build
uses: ./.github/actions/run-build
- name: Run lint
run: yarn nx affected --target=lint --parallel --nx-ignore-cycles
build:
name: 'build (node: 20)'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: 20
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Monorepo build
uses: ./.github/actions/run-build
typescript:
name: 'typescript'
needs: [changes]
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -70,8 +79,8 @@ jobs:
- uses: nrwl/nx-set-shas@v3
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Run build
run: yarn nx run-many --target=build --nx-ignore-cycles --skip-nx-cache
- name: Monorepo build
uses: ./.github/actions/run-build
- name: TSC for packages
run: yarn nx affected --target=test:ts --nx-ignore-cycles
- name: TSC for back
@ -81,11 +90,11 @@ jobs:
unit_back:
name: 'unit_back (node: ${{ matrix.node }})'
needs: [changes, lint, typescript]
needs: [changes, build]
runs-on: ubuntu-latest
strategy:
matrix:
node: [16, 18, 20]
node: [18, 20]
steps:
- uses: actions/checkout@v4
with:
@ -96,14 +105,14 @@ jobs:
- uses: nrwl/nx-set-shas@v3
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Run build
run: yarn build --skip-nx-cache
- name: Monorepo build
uses: ./.github/actions/run-build
- name: Run tests
run: yarn nx affected --target=test:unit --nx-ignore-cycles
unit_front:
name: 'unit_front (node: ${{ matrix.node }})'
needs: [changes, lint, typescript]
needs: [changes, build]
runs-on: ubuntu-latest
strategy:
matrix:
@ -118,31 +127,14 @@ jobs:
- uses: nrwl/nx-set-shas@v3
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Run build:ts for admin-test-utils & helper-plugin
run: yarn build --projects=@strapi/admin-test-utils,@strapi/helper-plugin --skip-nx-cache
- name: Monorepo build
uses: ./.github/actions/run-build
- name: Run test
run: yarn nx affected --target=test:front --nx-ignore-cycles
build:
name: 'build (node: ${{ matrix.node }})'
needs: [changes, lint, typescript, unit_front]
runs-on: ubuntu-latest
strategy:
matrix:
node: [16, 18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Build
run: yarn build --projects=@strapi/admin,@strapi/helper-plugin
e2e:
timeout-minutes: 60
needs: [changes, lint, typescript, unit_front, build]
needs: [changes, build, typescript, unit_front]
name: 'e2e (browser: ${{ matrix.project }})'
runs-on: ubuntu-latest
strategy:
@ -164,8 +156,8 @@ jobs:
- name: Install Playwright Browsers
run: npx playwright@1.38.1 install --with-deps
- name: Run build
run: yarn nx run-many --target=build --nx-ignore-cycles --skip-nx-cache
- name: Monorepo build
uses: ./.github/actions/run-build
- name: Run E2E tests
run: yarn test:e2e --setup --concurrency=1 --project=${{ matrix.project }}
@ -180,12 +172,12 @@ jobs:
api_ce_pg:
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
needs: [changes, lint, typescript, unit_back, unit_front]
needs: [changes, build, typescript, unit_back, unit_front]
name: '[CE] API Integration (postgres, node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
strategy:
matrix:
node: [16, 18, 20]
shard: [1/2, 2/2]
node: [18, 20]
shard: [1/5, 2/5, 3/5, 4/5, 5/5]
services:
postgres:
# Docker Hub image
@ -212,6 +204,8 @@ jobs:
node-version: ${{ matrix.node }}
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Monorepo build
uses: ./.github/actions/run-build
- uses: ./.github/actions/run-api-tests
with:
dbOptions: '--dbclient=postgres --dbhost=localhost --dbport=5432 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
@ -220,13 +214,13 @@ jobs:
api_ce_mysql:
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
needs: [changes, lint, typescript, unit_back, unit_front]
needs: [changes, build, typescript, unit_back, unit_front]
name: '[CE] API Integration (mysql:latest, client: ${{ matrix.db_client }}, node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
strategy:
matrix:
node: [16, 18, 20]
node: [18, 20]
db_client: ['mysql', 'mysql2']
shard: [1/2, 2/2]
shard: [1/5, 2/5, 3/5, 4/5, 5/5]
services:
mysql:
image: bitnami/mysql:latest
@ -251,6 +245,8 @@ jobs:
node-version: ${{ matrix.node }}
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Monorepo build
uses: ./.github/actions/run-build
- uses: ./.github/actions/run-api-tests
with:
dbOptions: '--dbclient=${{ matrix.db_client }} --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
@ -259,13 +255,13 @@ jobs:
api_ce_mysql_5:
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
needs: [changes, lint, typescript, unit_back, unit_front]
needs: [changes, build, typescript, unit_back, unit_front]
name: '[CE] API Integration (mysql:5, client: ${{ matrix.db_client }} , node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
strategy:
matrix:
node: [16, 18, 20]
node: [18, 20]
db_client: ['mysql', 'mysql2']
shard: [1/2, 2/2]
shard: [1/5, 2/5, 3/5, 4/5, 5/5]
services:
mysql:
image: bitnami/mysql:5.7
@ -289,6 +285,8 @@ jobs:
node-version: ${{ matrix.node }}
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Monorepo build
uses: ./.github/actions/run-build
- uses: ./.github/actions/run-api-tests
with:
dbOptions: '--dbclient=${{ matrix.db_client }} --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
@ -297,13 +295,13 @@ jobs:
api_ce_sqlite:
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
needs: [changes, lint, typescript, unit_back, unit_front]
needs: [changes, build, typescript, unit_back, unit_front]
name: '[CE] API Integration (sqlite, client: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
strategy:
matrix:
node: [16, 18, 20]
node: [18, 20]
sqlite_pkg: ['better-sqlite3', 'sqlite3']
shard: [1/2, 2/2]
shard: [1/5, 2/5, 3/5, 4/5, 5/5]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
@ -311,6 +309,8 @@ jobs:
node-version: ${{ matrix.node }}
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Monorepo build
uses: ./.github/actions/run-build
- uses: ./.github/actions/run-api-tests
env:
SQLITE_PKG: ${{ matrix.sqlite_pkg }}
@ -321,15 +321,15 @@ jobs:
# EE
api_ee_pg:
runs-on: ubuntu-latest
needs: [changes, lint, typescript, unit_back, unit_front]
needs: [changes, build, typescript, unit_back, unit_front]
name: '[EE] API Integration (postgres, node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
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:
matrix:
node: [16, 18, 20]
shard: [1/2, 2/2]
node: [18, 20]
shard: [1/5, 2/5, 3/5, 4/5, 5/5]
services:
postgres:
# Docker Hub image
@ -356,6 +356,8 @@ jobs:
node-version: ${{ matrix.node }}
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Monorepo build
uses: ./.github/actions/run-build
- uses: ./.github/actions/run-api-tests
with:
dbOptions: '--dbclient=postgres --dbhost=localhost --dbport=5432 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
@ -364,16 +366,16 @@ jobs:
api_ee_mysql:
runs-on: ubuntu-latest
needs: [changes, lint, typescript, unit_back, unit_front]
needs: [changes, build, typescript, unit_back, unit_front]
name: '[EE] API Integration (mysql:latest, client: ${{ matrix.db_client }}, node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
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:
matrix:
node: [16, 18, 20]
node: [18, 20]
db_client: ['mysql', 'mysql2']
shard: [1/2, 2/2]
shard: [1/5, 2/5, 3/5, 4/5, 5/5]
services:
mysql:
image: bitnami/mysql:latest
@ -398,6 +400,8 @@ jobs:
node-version: ${{ matrix.node }}
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Monorepo build
uses: ./.github/actions/run-build
- uses: ./.github/actions/run-api-tests
with:
dbOptions: '--dbclient=${{ matrix.db_client }} --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
@ -406,16 +410,16 @@ jobs:
api_ee_sqlite:
runs-on: ubuntu-latest
needs: [changes, lint, typescript, unit_back, unit_front]
needs: [changes, build, typescript, unit_back, unit_front]
name: '[EE] API Integration (sqlite, client: ${{ matrix.sqlite_pkg }}, node: ${{ matrix.node }}, shard: ${{ matrix.shard }})'
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:
matrix:
node: [16, 18, 20]
node: [18, 20]
sqlite_pkg: ['better-sqlite3', 'sqlite3']
shard: [1/2, 2/2]
shard: [1/5, 2/5, 3/5, 4/5, 5/5]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
@ -423,6 +427,8 @@ jobs:
node-version: ${{ matrix.node }}
- name: Monorepo install
uses: ./.github/actions/yarn-nm-install
- name: Monorepo build
uses: ./.github/actions/run-build
- uses: ./.github/actions/run-api-tests
env:
SQLITE_PKG: ${{ matrix.sqlite_pkg }}

2
.nvmrc
View File

@ -1 +1 @@
16
20

View File

@ -46,7 +46,7 @@ The Strapi core team will review your pull request and either merge it, request
## Contribution Prerequisites
- You have [Node.js](https://nodejs.org/en/) at version >= v16 and <= v20 and [Yarn](https://yarnpkg.com/en/) at v1.2.0+ installed.
- You have [Node.js](https://nodejs.org/en/) at version >= v18 and <= v20 and [Yarn](https://yarnpkg.com/en/) at v1.2.0+ installed.
- You are familiar with [Git](https://git-scm.com).
**Before submitting your pull request** make sure the following requirements are fulfilled:

View File

@ -94,6 +94,7 @@ Strapi only supports maintenance and LTS versions of Node.js. Please refer to th
| Strapi Version | Recommended | Minimum |
| --------------- | ----------- | ------- |
| 4.14.5 and up | 20.x | 18.x |
| 4.11.0 and up | 18.x | 16.x |
| 4.3.9 to 4.10.x | 18.x | 14.x |
| 4.0.x to 4.3.8 | 16.x | 14.x |

View File

@ -14,3 +14,41 @@ This is an experimental API that is subject to change at any moment, hence why i
## Available Commands
- [plugin:build](build) - Build a plugin for publishing
- [plugin:watch](watch) - Watch & compile a plugin in local development
## Setting up your package
In order to build/watch/check a plugin you need to have a `package.json` that must contain the following fields:
- `name`
- `version`
In regards to the export keys of your package.json because a plugin _typically_ has both a server and client
side output we recommend doing the following:
```json
{
"name": "@strapi/plugin",
"version": "1.0.0",
"exports": {
"./strapi-admin": {
"types": "./dist/admin/index.d.ts",
"source": "./admin/src/index.ts",
"import": "./dist/admin/index.mjs",
"require": "./dist/admin/index.js",
"default": "./dist/admin/index.js"
},
"./strapi-server": {
"types": "./dist/server/index.d.ts",
"source": "./server/src/index.ts",
"import": "./dist/server/index.mjs",
"require": "./dist/server/index.js",
"default": "./dist/server/index.js"
},
"./package.json": "./package.json"
}
}
```
We don't use `main`, `module` or `types` on the root level of the package.json because of the aforementioned reason (plugins don't have one entry).
If you've not written your plugin in typescript, you can omit the `types` value of an export map. This is the minimum setup required to build a plugin.

View File

@ -9,8 +9,7 @@ tags:
---
The `plugin:build` command is used to build plugins in a CJS/ESM compatible format that can be instantly published to NPM.
This is done by looking at the export fields of a package.json e.g. `main`, `module`, `types` and `exports`. By using the
exports map specifically we can build dual plugins that support a server & client output.
This is done by using `pack-up` underneath and a specific configuration, for this command we _do not_ look for a `packup.config` file.
## Usage
@ -24,48 +23,14 @@ strapi plugin:build
Bundle your strapi plugin for publishing.
Options:
-y, --yes Skip all confirmation prompts (default: false)
--force Automatically answer "yes" to all prompts, including potentially destructive requests, and run non-interactively.
-d, --debug Enable debugging mode with verbose logs (default: false)
--silent Don't log anything (default: false)
--sourcemap produce sourcemaps (default: false)
--minify minify the output (default: false)
-h, --help Display help for command
```
## Setting up your package
In order to build a plugin you need to have a `package.json` that must contain the following fields:
- `name`
- `version`
In regards to the export keys of your package.json because a plugin _typically_ has both a server and client
side output we recommend doing the following:
```json
{
"name": "@strapi/plugin",
"version": "1.0.0",
"exports": {
"./strapi-admin": {
"types": "./dist/admin/index.d.ts",
"source": "./admin/src/index.ts",
"import": "./dist/admin/index.mjs",
"require": "./dist/admin/index.js",
"default": "./dist/admin/index.js"
},
"./strapi-server": {
"types": "./dist/server/index.d.ts",
"source": "./server/src/index.ts",
"import": "./dist/server/index.mjs",
"require": "./dist/server/index.js",
"default": "./dist/server/index.js"
},
"./package.json": "./package.json"
}
}
```
We don't use `main`, `module` or `types` on the root level of the package.json because of the aforementioned reason (plugins don't have one entry).
If you've not written your plugin in typescript, you can omit the `types` value of an export map. This is the minimum setup required to build a plugin.
## How it works
The command sequence can be visualised as follows:
@ -73,37 +38,6 @@ The command sequence can be visualised as follows:
- Load package.json
- Validate that package.json against a `yup` schema
- Validate the ordering of an export map if `pkg.exports` is defined
- Create a build context, this holds information like:
- The transpilation target
- The external dependencies (that we don't want to bundle)
- Where the output should go e.g. `dist`
- The exports we're about to use to create build tasks
- Create a list of build tasks based on the `exports` from the build context, these can currently either be `"build:js"` or `"build:dts"`
- Pass the build task to a specific task handler e.g. `vite` or `tsc`
- Create a set of "bundles" to build ignoring the package.json exports map that is _specifically_ set up for strapi-plugins.
- Pass the created config to `pack-up`'s build API.
- Finish
## Transpilation target
There are three different runtimes available for plugins:
- `node` which equates to a `node16` target
- `web` which equates to a `esnext` target
- `*` (universal) which equates to `["last 3 major versions", "Firefox ESR", "last 2 Opera versions", "not dead", "node 16.0.0"]`
The `node` and `web` targets are specifically used for the export maps with they keys `./strapi-server` and `./strapi-admin` respectively.
Any other export map values will be transpiled to the universal target. The universal target can be overwritten by adding the `browserslist`
key to your `package.json` (seen below):
```json
{
"name": "@strapi/plugin",
"version": "1.0.0",
"browserslist": [
"last 3 major versions",
"Firefox ESR",
"last 2 Opera versions",
"not dead",
"node 16.0.0"
]
}
```

View File

@ -0,0 +1,40 @@
---
title: plugin:build
description: An in depth look at the plugin:build command of the Strapi CLI
tags:
- CLI
- commands
- plugins
- building
---
The `plugin:watch` command is used to watch plugin source files and compile them to production viable assets in real-time.
This is done by using `pack-up` underneath and a specific configuration, for this command we _do not_ look for a `packup.config` file.
## Usage
```bash
strapi plugin:watch
```
### Options
```bash
Watch & compile your strapi plugin for local development.
Options:
-d, --debug Enable debugging mode with verbose logs (default: false)
--silent Don't log anything (default: false)
-h, --help Display help for command
```
## How it works
The command sequence can be visualised as follows:
- Load package.json
- Validate that package.json against a `yup` schema
- Validate the ordering of an export map if `pkg.exports` is defined
- Create a set of "bundles" to build ignoring the package.json exports map that is _specifically_ set up for strapi-plugins.
- Pass the created config to `pack-up`'s watch API.
- Run's indefinitely

View File

@ -32,6 +32,8 @@ build();
```ts
interface BuildOptions {
configFile: false;
config?: Config;
cwd?: string;
debug?: boolean;
minify?: boolean;

View File

@ -27,6 +27,8 @@ watch();
```ts
interface WatchOptions {
configFile: false;
config?: Config;
cwd?: string;
debug?: boolean;
silent?: boolean;

View File

@ -47,6 +47,12 @@ interface Config {
* Whether to minify the output or not.
*/
minify?: boolean;
/**
* Instead of creating as few chunks as possible, this mode
* will create separate chunks for all modules using the original module
* names as file names
*/
preserveModules?: boolean;
/**
* Whether to generate sourcemaps for the output or not.
*/
@ -56,6 +62,10 @@ interface Config {
* Node.js workers and you want them to be transpiled for the node environment.
*/
runtime?: Runtime;
/**
* path to the tsconfig file to use for the bundle.
*/
tsconfig?: string;
}
interface ConfigBundle {
@ -63,6 +73,8 @@ interface ConfigBundle {
import?: string;
require?: string;
runtime?: Runtime;
tsconfig?: string;
types?: string;
}
type Runtime = '*' | 'node' | 'web';

View File

@ -16,7 +16,7 @@
"better-sqlite3": "8.6.0"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"license": "MIT"

View File

@ -38,7 +38,7 @@
"styled-components": "5.3.3"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"strapi": {

View File

@ -24,7 +24,7 @@
"styled-components": "5.3.3"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"strapi": {

View File

@ -29,7 +29,7 @@
"styled-components": "5.3.3"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"strapi": {

View File

@ -59,6 +59,7 @@
"test:front:watch": "cross-env IS_EE=true run test:front --watch",
"test:front:watch:ce": "cross-env IS_EE=false run test:front --watch",
"test:generate-app": "yarn build:ts && node test/scripts/generate-test-app.js",
"test:generate-app:no-build": "node test/scripts/generate-test-app.js",
"test:ts": "yarn test:ts:packages && yarn test:ts:front && yarn test:ts:back",
"test:ts:back": "nx run-many --target=test:ts:back --nx-ignore-cycles",
"test:ts:front": "nx run-many --target=test:ts:front --nx-ignore-cycles",
@ -137,7 +138,7 @@
},
"packageManager": "yarn@3.6.4",
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
}
}

View File

@ -82,6 +82,6 @@
"@testing-library/jest-dom": "^5.16.5"
},
"engines": {
"node": ">=16.0.0 <=20.x.x"
"node": ">=18.0.0 <=20.x.x"
}
}

View File

@ -54,7 +54,7 @@
"tsconfig": "4.14.4"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
}
}

View File

@ -59,7 +59,7 @@
"tsconfig": "4.14.4"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
}
}

View File

@ -10,23 +10,20 @@ import { BrowserRouter } from 'react-router-dom';
import Logo from './assets/images/logo-strapi-2022.svg';
import { LANGUAGE_LOCAL_STORAGE_KEY } from './components/LanguageProvider';
import Providers from './components/Providers';
import {
HOOKS,
INJECTION_ZONES
} from './constants';
import { Providers } from './components/Providers';
import { HOOKS, INJECTION_ZONES } from './constants';
import { customFields, Plugin, Reducers } from './core/apis';
import { configureStore } from './core/store';
import { configureStore } from './core/store/configure';
import { basename, createHook } from './core/utils';
import favicon from './favicon.png';
import App from './pages/App';
import languageNativeNames from './translations/languageNativeNames';
const {
INJECT_COLUMN_IN_TABLE,
INJECT_COLUMN_IN_TABLE,
MUTATE_COLLECTION_TYPES_LINKS,
MUTATE_EDIT_VIEW_LAYOUT,
MUTATE_SINGLE_TYPES_LINKS
MUTATE_SINGLE_TYPES_LINKS,
} = HOOKS;
class StrapiApp {

View File

@ -0,0 +1,184 @@
import * as React from 'react';
import {
AppInfoContextValue,
AppInfoProvider,
auth,
LoadingIndicatorPage,
useFetchClient,
useGuidedTour,
} from '@strapi/helper-plugin';
import lodashGet from 'lodash/get';
import { useQueries } from 'react-query';
import lt from 'semver/functions/lt';
import valid from 'semver/functions/valid';
// TODO: DS add loader
import packageJSON from '../../../package.json';
import { UserEntity } from '../../../shared/entities';
import { useConfiguration } from '../hooks/useConfiguration';
import { APIResponse, APIResponseUsersLegacy } from '../types/adminAPI';
// @ts-expect-error - no types yet.
import { getFullName, hashAdminUserEmail } from '../utils';
import { NpsSurvey } from './NpsSurvey';
import { PluginsInitializer } from './PluginsInitializer';
import { RBACProvider, Permission } from './RBACProvider';
const strapiVersion = packageJSON.version;
const AuthenticatedApp = () => {
const { setGuidedTourVisibility } = useGuidedTour();
const userInfo = auth.get('userInfo');
const userName = userInfo
? lodashGet(userInfo, 'username') || getFullName(userInfo.firstname, userInfo.lastname)
: null;
const [userDisplayName, setUserDisplayName] = React.useState(userName);
const [userId, setUserId] = React.useState<string>();
const { showReleaseNotification } = useConfiguration();
const { get } = useFetchClient();
const [
{ data: appInfos, status },
{ data: tagName, isLoading },
{ data: permissions, status: fetchPermissionsStatus, refetch, isFetching },
{ data: userRoles },
] = useQueries([
{
queryKey: 'app-infos',
async queryFn() {
const { data } = await get<
APIResponse<
Pick<
AppInfoContextValue,
| 'currentEnvironment'
| 'autoReload'
| 'communityEdition'
| 'dependencies'
| 'useYarn'
| 'projectId'
| 'strapiVersion'
| 'nodeVersion'
>
>
>('/admin/information');
return data.data;
},
},
{
queryKey: 'strapi-release',
async queryFn() {
try {
const res = await fetch('https://api.github.com/repos/strapi/strapi/releases/latest');
if (!res.ok) {
throw new Error();
}
const response = (await res.json()) as { tag_name: string | null | undefined };
if (!response.tag_name) {
throw new Error();
}
return response.tag_name;
} catch (err) {
// Don't throw an error
return strapiVersion;
}
},
enabled: showReleaseNotification,
initialData: strapiVersion,
},
{
queryKey: 'admin-users-permission',
async queryFn() {
const { data } = await get<{ data: Permission[] }>('/admin/users/me/permissions');
return data.data;
},
initialData: [],
},
{
queryKey: 'user-roles',
async queryFn() {
const {
data: {
data: { roles },
},
} = await get<APIResponseUsersLegacy<UserEntity>>('/admin/users/me');
return roles;
},
},
]);
const shouldUpdateStrapi = checkLatestStrapiVersion(strapiVersion, tagName);
/**
* TODO: does this actually need to be an effect?
*/
React.useEffect(() => {
if (userRoles) {
const isUserSuperAdmin = userRoles.find(({ code }) => code === 'strapi-super-admin');
if (isUserSuperAdmin && appInfos?.autoReload) {
setGuidedTourVisibility(true);
}
}
}, [userRoles, appInfos, setGuidedTourVisibility]);
React.useEffect(() => {
const getUserId = async () => {
const userId = await hashAdminUserEmail(userInfo);
setUserId(userId);
};
getUserId();
}, [userInfo]);
// We don't need to wait for the release query to be fetched before rendering the plugins
// however, we need the appInfos and the permissions
const shouldShowNotDependentQueriesLoader =
isFetching || status === 'loading' || fetchPermissionsStatus === 'loading';
const shouldShowLoader = isLoading || shouldShowNotDependentQueriesLoader;
if (shouldShowLoader) {
return <LoadingIndicatorPage />;
}
// TODO: add error state
if (status === 'error') {
return <div>error...</div>;
}
return (
<AppInfoProvider
{...appInfos}
userId={userId}
latestStrapiReleaseTag={tagName}
setUserDisplayName={setUserDisplayName}
shouldUpdateStrapi={shouldUpdateStrapi}
userDisplayName={userDisplayName}
>
<RBACProvider permissions={permissions ?? []} refetchPermissions={refetch}>
<NpsSurvey />
<PluginsInitializer />
</RBACProvider>
</AppInfoProvider>
);
};
const checkLatestStrapiVersion = (
currentPackageVersion: string,
latestPublishedVersion: string = ''
): boolean => {
if (!valid(currentPackageVersion) || !valid(latestPublishedVersion)) {
return false;
}
return lt(currentPackageVersion, latestPublishedVersion);
};
export { AuthenticatedApp };

View File

@ -1,116 +0,0 @@
import React, { useEffect, useState } from 'react';
import {
AppInfoProvider,
auth,
LoadingIndicatorPage,
useGuidedTour,
useNotification,
} from '@strapi/helper-plugin';
import get from 'lodash/get';
import { useQueries } from 'react-query';
// TODO: DS add loader
import packageJSON from '../../../../package.json';
import { useConfiguration } from '../../hooks/useConfiguration';
import { getFullName, hashAdminUserEmail } from '../../utils';
import { NpsSurvey } from '../NpsSurvey';
import { PluginsInitializer } from '../PluginsInitializer';
import RBACProvider from '../RBACProvider';
import { fetchAppInfo, fetchCurrentUserPermissions, fetchUserRoles } from './utils/api';
import { checkLatestStrapiVersion } from './utils/checkLatestStrapiVersion';
import { fetchStrapiLatestRelease } from './utils/fetchStrapiLatestRelease';
const strapiVersion = packageJSON.version;
const AuthenticatedApp = () => {
const { setGuidedTourVisibility } = useGuidedTour();
const toggleNotification = useNotification();
const userInfo = auth.getUserInfo();
const userName = get(userInfo, 'username') || getFullName(userInfo.firstname, userInfo.lastname);
const [userDisplayName, setUserDisplayName] = useState(userName);
const [userId, setUserId] = useState(null);
const { showReleaseNotification } = useConfiguration();
const [
{ data: appInfos, status },
{ data: tagName, isLoading },
{ data: permissions, status: fetchPermissionsStatus, refetch, isFetching },
{ data: userRoles },
] = useQueries([
{ queryKey: 'app-infos', queryFn: fetchAppInfo },
{
queryKey: 'strapi-release',
queryFn: () => fetchStrapiLatestRelease(toggleNotification),
enabled: showReleaseNotification,
initialData: strapiVersion,
},
{
queryKey: 'admin-users-permission',
queryFn: fetchCurrentUserPermissions,
initialData: [],
},
{
queryKey: 'user-roles',
queryFn: fetchUserRoles,
},
]);
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) {
setGuidedTourVisibility(true);
}
}
}, [userRoles, appInfos, setGuidedTourVisibility]);
useEffect(() => {
const getUserId = async () => {
const userId = await hashAdminUserEmail(userInfo);
setUserId(userId);
};
getUserId();
}, [userInfo]);
// We don't need to wait for the release query to be fetched before rendering the plugins
// however, we need the appInfos and the permissions
const shouldShowNotDependentQueriesLoader =
isFetching || status === 'loading' || fetchPermissionsStatus === 'loading';
const shouldShowLoader = isLoading || shouldShowNotDependentQueriesLoader;
if (shouldShowLoader) {
return <LoadingIndicatorPage />;
}
// TODO: add error state
if (status === 'error') {
return <div>error...</div>;
}
return (
<AppInfoProvider
{...appInfos}
userId={userId}
latestStrapiReleaseTag={tagName}
setUserDisplayName={setUserDisplayName}
shouldUpdateStrapi={shouldUpdateStrapi}
userDisplayName={userDisplayName}
>
<RBACProvider permissions={permissions} refetchPermissions={refetch}>
<NpsSurvey />
<PluginsInitializer />
</RBACProvider>
</AppInfoProvider>
);
};
export default AuthenticatedApp;

View File

@ -1,47 +0,0 @@
import { getFetchClient } from '@strapi/helper-plugin';
const { get } = getFetchClient();
const fetchAppInfo = async () => {
try {
const { data, headers } = await get('/admin/information');
if (!headers['content-type'].includes('application/json')) {
throw new Error('Not found');
}
return data.data;
} catch (error) {
throw new Error(error);
}
};
const fetchCurrentUserPermissions = async () => {
try {
const { data, headers } = await get('/admin/users/me/permissions');
if (!headers['content-type'].includes('application/json')) {
throw new Error('Not found');
}
return data.data;
} catch (err) {
throw new Error(err);
}
};
const fetchUserRoles = async () => {
try {
const {
data: {
data: { roles },
},
} = await get('/admin/users/me');
return roles;
} catch (err) {
throw new Error(err);
}
};
export { fetchAppInfo, fetchCurrentUserPermissions, fetchUserRoles };

View File

@ -1,13 +0,0 @@
import lt from 'semver/functions/lt';
import valid from 'semver/functions/valid';
export const checkLatestStrapiVersion = (
currentPackageVersion: string,
latestPublishedVersion: string
): boolean => {
if (!valid(currentPackageVersion) || !valid(latestPublishedVersion)) {
return false;
}
return lt(currentPackageVersion, latestPublishedVersion);
};

View File

@ -1,19 +0,0 @@
import packageJSON from '../../../../../package.json';
const strapiVersion = packageJSON.version;
export const fetchStrapiLatestRelease = async () => {
try {
const res = await fetch('https://api.github.com/repos/strapi/strapi/releases/latest');
if (!res.ok) {
throw new Error('Failed to fetch latest Strapi version.');
}
const { tag_name } = await res.json();
return tag_name;
} catch (err) {
// Don't throw an error
return strapiVersion;
}
};

View File

@ -1,20 +0,0 @@
import { checkLatestStrapiVersion } from '../checkLatestStrapiVersion';
describe('ADMIN | utils | checkLatestStrapiVersion', () => {
it('should return true if the current version is lower than the latest published version', () => {
expect(checkLatestStrapiVersion('v3.3.2', 'v3.3.4')).toBeTruthy();
expect(checkLatestStrapiVersion('3.3.2', 'v3.3.4')).toBeTruthy();
expect(checkLatestStrapiVersion('v3.3.2', '3.3.4')).toBeTruthy();
expect(checkLatestStrapiVersion('3.3.2', '3.3.4')).toBeTruthy();
});
it('should return false if the current version is equal to the latest published version', () => {
expect(checkLatestStrapiVersion('3.3.4', 'v3.3.4')).toBeFalsy();
expect(checkLatestStrapiVersion('v3.3.4', '3.3.4')).toBeFalsy();
expect(checkLatestStrapiVersion('3.3.4', '3.3.4')).toBeFalsy();
});
it('should return false if the current version is a beta of the next release', () => {
expect(checkLatestStrapiVersion('3.4.0-beta.1', 'v3.3.4')).toBeFalsy();
expect(checkLatestStrapiVersion('v3.4.0-beta.1', '3.3.4')).toBeFalsy();
expect(checkLatestStrapiVersion('3.4.0-beta.1', '3.3.4')).toBeFalsy();
});
});

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import { ConfigurationContext, ConfigurationContextValue } from '../contexts/configuration';
export interface ConfigurationProviderProps {
interface ConfigurationProviderProps {
children: React.ReactNode;
authLogo: string;
menuLogo: string;
@ -65,3 +65,4 @@ const ConfigurationProvider = ({
};
export { ConfigurationProvider };
export type { ConfigurationProviderProps };

View File

@ -127,3 +127,4 @@ const reducer = (state = initialState, action: Action) => {
};
export { LanguageProvider, useLocales, LANGUAGE_LOCAL_STORAGE_KEY };
export type { LanguageProviderProps, LocalesContextValue };

View File

@ -0,0 +1,125 @@
import * as React from 'react';
import {
AutoReloadOverlayBlockerProvider,
CustomFieldsProvider,
CustomFieldsProviderProps,
LibraryProvider,
LibraryProviderProps,
NotificationsProvider,
OverlayBlockerProvider,
StrapiAppProvider,
StrapiAppProviderProps,
} from '@strapi/helper-plugin';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { AdminContext, AdminContextValue } from '../contexts/admin';
import { ConfigurationProvider, ConfigurationProviderProps } from './ConfigurationProvider';
import { GuidedTourProvider } from './GuidedTour/Provider';
import { LanguageProvider, LanguageProviderProps } from './LanguageProvider';
import { Theme } from './Theme';
import { ThemeToggleProvider, ThemeToggleProviderProps } from './ThemeToggleProvider';
import type { Store } from '../core/store/configure';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
interface ProvidersProps
extends Pick<ThemeToggleProviderProps, 'themes'>,
Pick<LanguageProviderProps, 'messages' | 'localeNames'>,
Pick<
ConfigurationProviderProps,
'authLogo' | 'menuLogo' | 'showReleaseNotification' | 'showTutorials'
>,
Pick<AdminContextValue, 'getAdminInjectedComponents'>,
Pick<CustomFieldsProviderProps, 'customFields'>,
Pick<LibraryProviderProps, 'components' | 'fields'>,
Pick<
StrapiAppProviderProps,
| 'getPlugin'
| 'menu'
| 'plugins'
| 'runHookParallel'
| 'runHookSeries'
| 'runHookWaterfall'
| 'settings'
> {
children: React.ReactNode;
store: Store;
}
const Providers = ({
authLogo,
children,
components,
customFields,
fields,
getAdminInjectedComponents,
getPlugin,
localeNames,
menu,
menuLogo,
messages,
plugins,
runHookParallel,
runHookSeries,
runHookWaterfall,
settings,
showReleaseNotification,
showTutorials,
store,
themes,
}: ProvidersProps) => {
return (
<LanguageProvider messages={messages} localeNames={localeNames}>
<ThemeToggleProvider themes={themes}>
<Theme>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AdminContext.Provider value={{ getAdminInjectedComponents }}>
<ConfigurationProvider
authLogo={authLogo}
menuLogo={menuLogo}
showReleaseNotification={showReleaseNotification}
showTutorials={showTutorials}
>
<StrapiAppProvider
getPlugin={getPlugin}
menu={menu}
plugins={plugins}
runHookParallel={runHookParallel}
runHookWaterfall={runHookWaterfall}
runHookSeries={runHookSeries}
settings={settings}
>
<LibraryProvider components={components} fields={fields}>
<CustomFieldsProvider customFields={customFields}>
<AutoReloadOverlayBlockerProvider>
<OverlayBlockerProvider>
<GuidedTourProvider>
<NotificationsProvider>{children}</NotificationsProvider>
</GuidedTourProvider>
</OverlayBlockerProvider>
</AutoReloadOverlayBlockerProvider>
</CustomFieldsProvider>
</LibraryProvider>
</StrapiAppProvider>
</ConfigurationProvider>
</AdminContext.Provider>
</Provider>
</QueryClientProvider>
</Theme>
</ThemeToggleProvider>
</LanguageProvider>
);
};
export { Providers };

View File

@ -1,156 +0,0 @@
import React from 'react';
import {
AutoReloadOverlayBlockerProvider,
CustomFieldsProvider,
LibraryProvider,
NotificationsProvider,
OverlayBlockerProvider,
StrapiAppProvider,
} from '@strapi/helper-plugin';
import PropTypes from 'prop-types';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { AdminContext } from '../../contexts/admin';
import { ConfigurationProvider } from '../ConfigurationProvider';
import { GuidedTourProvider } from '../GuidedTour/Provider';
import { LanguageProvider } from '../LanguageProvider';
import { Theme } from '../Theme';
import { ThemeToggleProvider } from '../ThemeToggleProvider';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
const Providers = ({
authLogo,
children,
components,
customFields,
fields,
getAdminInjectedComponents,
getPlugin,
localeNames,
menu,
menuLogo,
messages,
plugins,
runHookParallel,
runHookSeries,
runHookWaterfall,
settings,
showReleaseNotification,
showTutorials,
store,
themes,
}) => {
return (
<LanguageProvider messages={messages} localeNames={localeNames}>
<ThemeToggleProvider themes={themes}>
<Theme>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AdminContext.Provider value={{ getAdminInjectedComponents }}>
<ConfigurationProvider
authLogo={authLogo}
menuLogo={menuLogo}
showReleaseNotification={showReleaseNotification}
showTutorials={showTutorials}
>
<StrapiAppProvider
getPlugin={getPlugin}
menu={menu}
plugins={plugins}
runHookParallel={runHookParallel}
runHookWaterfall={runHookWaterfall}
runHookSeries={runHookSeries}
settings={settings}
>
<LibraryProvider components={components} fields={fields}>
<CustomFieldsProvider customFields={customFields}>
<AutoReloadOverlayBlockerProvider>
<OverlayBlockerProvider>
<GuidedTourProvider>
<NotificationsProvider>{children}</NotificationsProvider>
</GuidedTourProvider>
</OverlayBlockerProvider>
</AutoReloadOverlayBlockerProvider>
</CustomFieldsProvider>
</LibraryProvider>
</StrapiAppProvider>
</ConfigurationProvider>
</AdminContext.Provider>
</Provider>
</QueryClientProvider>
</Theme>
</ThemeToggleProvider>
</LanguageProvider>
);
};
Providers.propTypes = {
authLogo: PropTypes.oneOfType([PropTypes.string, PropTypes.any]).isRequired,
children: PropTypes.node.isRequired,
components: PropTypes.object.isRequired,
customFields: PropTypes.object.isRequired,
fields: PropTypes.object.isRequired,
getAdminInjectedComponents: PropTypes.func.isRequired,
getPlugin: PropTypes.func.isRequired,
localeNames: PropTypes.objectOf(PropTypes.string).isRequired,
menu: PropTypes.arrayOf(
PropTypes.shape({
to: PropTypes.string.isRequired,
icon: PropTypes.func.isRequired,
intlLabel: PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
}).isRequired,
permissions: PropTypes.array,
Component: PropTypes.func,
})
).isRequired,
menuLogo: PropTypes.oneOfType([PropTypes.string, PropTypes.any]).isRequired,
messages: PropTypes.object.isRequired,
plugins: PropTypes.object.isRequired,
runHookParallel: PropTypes.func.isRequired,
runHookWaterfall: PropTypes.func.isRequired,
runHookSeries: PropTypes.func.isRequired,
settings: PropTypes.object.isRequired,
showReleaseNotification: PropTypes.bool.isRequired,
showTutorials: PropTypes.bool.isRequired,
store: PropTypes.object.isRequired,
themes: PropTypes.shape({
light: PropTypes.shape({
colors: PropTypes.object.isRequired,
shadows: PropTypes.object.isRequired,
sizes: PropTypes.object.isRequired,
zIndices: PropTypes.array.isRequired,
spaces: PropTypes.array.isRequired,
borderRadius: PropTypes.string.isRequired,
mediaQueries: PropTypes.object.isRequired,
fontSizes: PropTypes.array.isRequired,
lineHeights: PropTypes.array.isRequired,
fontWeights: PropTypes.object.isRequired,
}).isRequired,
dark: PropTypes.shape({
colors: PropTypes.object.isRequired,
shadows: PropTypes.object.isRequired,
sizes: PropTypes.object.isRequired,
zIndices: PropTypes.array.isRequired,
spaces: PropTypes.array.isRequired,
borderRadius: PropTypes.string.isRequired,
mediaQueries: PropTypes.object.isRequired,
fontSizes: PropTypes.array.isRequired,
lineHeights: PropTypes.array.isRequired,
fontWeights: PropTypes.object.isRequired,
}).isRequired,
custom: PropTypes.object,
}).isRequired,
};
export default Providers;

View File

@ -0,0 +1,124 @@
import * as React from 'react';
import {
LoadingIndicatorPage,
Permission,
RBACContext,
RBACContextValue,
} from '@strapi/helper-plugin';
import produce from 'immer';
import { useTypedSelector, useTypedDispatch } from '../core/store/hooks';
/* -------------------------------------------------------------------------------------------------
* RBACProvider
* -----------------------------------------------------------------------------------------------*/
interface RBACProviderProps {
children: React.ReactNode;
permissions: Permission[];
refetchPermissions: RBACContextValue['refetchPermissions'];
}
const RBACProvider = ({ children, permissions, refetchPermissions }: RBACProviderProps) => {
const allPermissions = useTypedSelector((state) => state.rbacProvider.allPermissions);
const dispatch = useTypedDispatch();
React.useEffect(() => {
dispatch(setPermissionsAction(permissions));
return () => {
dispatch(resetStoreAction());
};
}, [permissions, dispatch]);
if (!allPermissions) {
return <LoadingIndicatorPage />;
}
return (
<RBACContext.Provider value={{ allPermissions, refetchPermissions }}>
{children}
</RBACContext.Provider>
);
};
/* -------------------------------------------------------------------------------------------------
* RBACReducer
* -----------------------------------------------------------------------------------------------*/
interface RBACState {
allPermissions: null | Permission[];
collectionTypesRelatedPermissions: Record<string, Record<string, Permission[]>>;
}
const initialState = {
allPermissions: null,
collectionTypesRelatedPermissions: {},
};
const RESET_STORE = 'StrapiAdmin/RBACProvider/RESET_STORE';
const SET_PERMISSIONS = 'StrapiAdmin/RBACProvider/SET_PERMISSIONS';
interface ResetStoreAction {
type: typeof RESET_STORE;
}
const resetStoreAction = (): ResetStoreAction => ({ type: RESET_STORE });
interface SetPermissionsAction {
type: typeof SET_PERMISSIONS;
permissions: Permission[];
}
const setPermissionsAction = (
permissions: SetPermissionsAction['permissions']
): SetPermissionsAction => ({
type: SET_PERMISSIONS,
permissions,
});
type Actions = ResetStoreAction | SetPermissionsAction;
const RBACReducer = (state: RBACState = initialState, action: Actions) =>
produce(state, (draftState) => {
switch (action.type) {
case SET_PERMISSIONS: {
draftState.allPermissions = action.permissions;
draftState.collectionTypesRelatedPermissions = action.permissions
.filter((perm) => perm.subject)
.reduce<Record<string, Record<string, Permission[]>>>((acc, current) => {
const { subject, action } = current;
if (!subject) return acc;
if (!acc[subject]) {
acc[subject] = {};
}
acc[subject] = acc[subject][action]
? { ...acc[subject], [action]: [...acc[subject][action], current] }
: { ...acc[subject], [action]: [current] };
return acc;
}, {});
break;
}
case RESET_STORE: {
return initialState;
}
default:
return state;
}
});
export { RBACProvider, RBACReducer, resetStoreAction, setPermissionsAction };
export type {
RBACState,
Actions,
RBACProviderProps,
ResetStoreAction,
SetPermissionsAction,
Permission,
};

View File

@ -1,10 +0,0 @@
import { RESET_STORE, SET_PERMISSIONS } from './constants';
const resetStore = () => ({ type: RESET_STORE });
const setPermissions = (permissions) => ({
type: SET_PERMISSIONS,
permissions,
});
export { resetStore, setPermissions };

View File

@ -1,2 +0,0 @@
export const RESET_STORE = 'StrapiAdmin/RBACProvider/RESET_STORE';
export const SET_PERMISSIONS = 'StrapiAdmin/RBACProvider/SET_PERMISSIONS';

View File

@ -1,39 +0,0 @@
import React, { useEffect } from 'react';
import { LoadingIndicatorPage, RBACProviderContext } from '@strapi/helper-plugin';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { resetStore, setPermissions } from './actions';
const RBACProvider = ({ children, permissions, refetchPermissions }) => {
const { allPermissions } = useSelector((state) => state.rbacProvider);
const dispatch = useDispatch();
useEffect(() => {
dispatch(setPermissions(permissions));
return () => {
dispatch(resetStore());
};
}, [permissions, dispatch]);
if (!allPermissions) {
return <LoadingIndicatorPage />;
}
return (
<RBACProviderContext.Provider value={{ allPermissions, refetchPermissions }}>
{children}
</RBACProviderContext.Provider>
);
};
RBACProvider.propTypes = {
children: PropTypes.node.isRequired,
permissions: PropTypes.array.isRequired,
refetchPermissions: PropTypes.func.isRequired,
};
export default RBACProvider;

View File

@ -1,51 +0,0 @@
/*
*
* RBACProvider reducer
* The goal of this reducer is to provide
* the plugins with an access to the user's permissions
* in our middleware system
*
*/
import produce from 'immer';
import { RESET_STORE, SET_PERMISSIONS } from './constants';
const initialState = {
allPermissions: null,
collectionTypesRelatedPermissions: {},
};
const reducer = (state = initialState, action) =>
// eslint-disable-next-line consistent-return
produce(state, (draftState) => {
switch (action.type) {
case SET_PERMISSIONS: {
draftState.allPermissions = action.permissions;
draftState.collectionTypesRelatedPermissions = action.permissions
.filter((perm) => perm.subject)
.reduce((acc, current) => {
const { subject, action } = current;
if (!acc[subject]) {
acc[subject] = {};
}
acc[subject] = acc[subject][action]
? { ...acc[subject], [action]: [...acc[subject][action], current] }
: { ...acc[subject], [action]: [current] };
return acc;
}, {});
break;
}
case RESET_STORE: {
return initialState;
}
default:
return state;
}
});
export default reducer;
export { initialState };

View File

@ -48,3 +48,4 @@ const ThemeToggleProvider = ({ children, themes }: ThemeToggleProviderProps) =>
};
export { ThemeToggleProvider };
export type { ThemeToggleProviderProps };

View File

@ -1,8 +1,6 @@
import React from 'react';
import { render, waitFor } from '@tests/utils';
import AuthenticatedApp from '../index';
import { AuthenticatedApp } from '../AuthenticatedApp';
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
@ -11,14 +9,14 @@ jest.mock('@strapi/helper-plugin', () => ({
*/
usePersistentState: jest.fn().mockImplementation(() => [{ enabled: false }, jest.fn()]),
auth: {
getUserInfo: () => ({ firstname: 'kai', lastname: 'doe', email: 'testemail@strapi.io' }),
get: () => ({ firstname: 'kai', lastname: 'doe', email: 'testemail@strapi.io' }),
},
useGuidedTour: jest.fn(() => ({
setGuidedTourVisibility: jest.fn(),
})),
}));
jest.mock('../../PluginsInitializer', () => ({
jest.mock('../PluginsInitializer', () => ({
PluginsInitializer() {
return <div>PluginsInitializer</div>;
},
@ -34,10 +32,10 @@ describe('AuthenticatedApp', () => {
});
it('should not crash', async () => {
const { queryByText } = render(<AuthenticatedApp />);
const { queryByText, getByText } = render(<AuthenticatedApp />);
await waitFor(() => expect(queryByText(/Loading/)).not.toBeInTheDocument());
expect(queryByText(/PluginsInitializer/)).toBeInTheDocument();
expect(getByText(/PluginsInitializer/)).toBeInTheDocument();
});
});

View File

@ -1,10 +1,15 @@
import { fixtures } from '@strapi/admin-test-utils';
import { resetStore, setPermissions } from '../actions';
import rbacProviderReducer, { initialState } from '../reducer';
import {
Permission,
RBACReducer,
RBACState,
resetStoreAction,
setPermissionsAction,
} from '../RBACProvider';
describe('rbacProviderReducer', () => {
let state;
describe('RBACReducer', () => {
let state: RBACState;
beforeEach(() => {
state = {
@ -16,24 +21,41 @@ describe('rbacProviderReducer', () => {
it('returns the initial state', () => {
const expected = state;
expect(rbacProviderReducer(undefined, {})).toEqual(expected);
// @ts-expect-error testing the default case
expect(RBACReducer(undefined, {})).toEqual(expected);
});
describe('resetStore', () => {
describe('resetStoreAction', () => {
it('should reset the state to its initial value', () => {
state.allPermissions = true;
state.collectionTypesRelatedPermissions = true;
state.allPermissions = [];
state.collectionTypesRelatedPermissions = {
apple: {},
};
expect(rbacProviderReducer(state, resetStore())).toEqual(initialState);
expect(RBACReducer(state, resetStoreAction())).toMatchInlineSnapshot(`
{
"allPermissions": null,
"collectionTypesRelatedPermissions": {},
}
`);
});
});
describe('setPermissions', () => {
describe('setPermissionsAction', () => {
it('should set the allPermissions value correctly', () => {
const permissions = [{ action: 'test', subject: null }];
const permissions: Permission[] = [
{
id: 0,
action: 'test',
subject: null,
conditions: [],
properties: {},
actionParameters: {},
},
];
const expected = { ...state, allPermissions: permissions };
expect(rbacProviderReducer(state, setPermissions(permissions))).toEqual(expected);
expect(RBACReducer(state, setPermissionsAction(permissions))).toEqual(expected);
});
it('should set the collectionTypesRelatedPermissions correctly', () => {
@ -89,7 +111,7 @@ describe('rbacProviderReducer', () => {
};
expect(
rbacProviderReducer(state, setPermissions(fixtures.permissions.allPermissions))
RBACReducer(state, setPermissionsAction(fixtures.permissions.contentManager))
.collectionTypesRelatedPermissions
).toEqual(expected);
});

View File

@ -284,13 +284,13 @@ describe('useBlocksStore', () => {
);
const link = screen.getByRole('link', 'Some link');
expect(screen.queryByLabelText(/Delete/i, { selector: 'button' })).not.toBeInTheDocument();
expect(screen.queryByLabelText(/Edit/i, { selector: 'button' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Delete/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Edit/i })).not.toBeInTheDocument();
await user.click(link);
expect(screen.queryByLabelText(/Delete/i, { selector: 'button' })).toBeInTheDocument();
expect(screen.queryByLabelText(/Edit/i, { selector: 'button' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Delete/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Edit/i })).toBeInTheDocument();
});
it('renders link fields to edit when user clicks the edit option and check save button disabled state', async () => {

View File

@ -2,15 +2,16 @@ import * as React from 'react';
import {
Box,
Icon,
Typography,
BaseLink,
Popover,
IconButton,
Field,
FieldLabel,
FieldInput,
Flex,
Button,
Tooltip,
} from '@strapi/design-system';
import {
Code,
@ -255,6 +256,18 @@ Image.propTypes = {
}).isRequired,
};
// Make sure the tooltip is above the popover
const TooltipCustom = styled(Tooltip)`
z-index: 6;
`;
// Used for the Edit and Cancel buttons in the link popover
const CustomButton = styled(Button)`
& > span {
line-height: normal;
}
`;
const Link = React.forwardRef(({ element, children, ...attributes }, forwardedRef) => {
const { formatMessage } = useIntl();
const editor = useSlate();
@ -376,25 +389,49 @@ const Link = React.forwardRef(({ element, children, ...attributes }, forwardedRe
</StyledBaseLink>
</Typography>
<Flex justifyContent="end" width="100%" gap={2}>
<IconButton
icon={<Trash />}
size="L"
variant="danger"
onClick={() => removeLink(editor)}
label={formatMessage({
<TooltipCustom
description={formatMessage({
id: 'components.Blocks.popover.delete',
defaultMessage: 'Delete',
})}
/>
<IconButton
icon={<Pencil />}
size="L"
onClick={() => setIsEditing(true)}
label={formatMessage({
>
<CustomButton
size="S"
width="2rem"
variant="danger-light"
onClick={() => removeLink(editor)}
aria-label={formatMessage({
id: 'components.Blocks.popover.delete',
defaultMessage: 'Delete',
})}
type="button"
justifyContent="center"
>
<Icon width={3} height={3} as={Trash} />
</CustomButton>
</TooltipCustom>
<TooltipCustom
description={formatMessage({
id: 'components.Blocks.popover.edit',
defaultMessage: 'Edit',
})}
/>
>
<CustomButton
size="S"
width="2rem"
variant="tertiary"
onClick={() => setIsEditing(true)}
aria-label={formatMessage({
id: 'components.Blocks.popover.edit',
defaultMessage: 'Edit',
})}
type="button"
justifyContent="center"
>
<Icon width={3} height={3} as={Pencil} />
</CustomButton>
</TooltipCustom>
</Flex>
</Flex>
)}

View File

@ -44,7 +44,7 @@ import { useDispatch } from 'react-redux';
import { useHistory, useLocation, Link as ReactRouterLink } from 'react-router-dom';
import { HOOKS } from '../../../constants';
import { useTypedSelector } from '../../../core/store';
import { useTypedSelector } from '../../../core/store/hooks';
import { useAdminUsers } from '../../../hooks/useAdminUsers';
import { useEnterprise } from '../../../hooks/useEnterprise';
import { InjectionZone } from '../../../shared/components';

View File

@ -1,25 +0,0 @@
import React, { createContext, useContext } from 'react';
import PropTypes from 'prop-types';
const ApiTokenPermissionsContext = createContext({});
const ApiTokenPermissionsContextProvider = ({ children, ...rest }) => {
return (
<ApiTokenPermissionsContext.Provider value={rest}>
{children}
</ApiTokenPermissionsContext.Provider>
);
};
const useApiTokenPermissionsContext = () => useContext(ApiTokenPermissionsContext);
ApiTokenPermissionsContextProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export {
ApiTokenPermissionsContext,
ApiTokenPermissionsContextProvider,
useApiTokenPermissionsContext,
};

View File

@ -16,3 +16,4 @@ const AdminContext = React.createContext<AdminContextValue>({
const useAdmin = () => React.useContext(AdminContext);
export { AdminContext, useAdmin };
export type { AdminContextValue };

View File

@ -0,0 +1,62 @@
/* eslint-disable check-file/filename-naming-convention */
import * as React from 'react';
import { Entity } from '@strapi/types';
interface PseudoEvent {
target: { value: string };
}
interface APITokenPermissionsContextProviderProps {
selectedAction: string[] | null;
routes: string[];
selectedActions: string[];
data: {
allActionsIds: Entity.ID[];
permissions: {
apiId: string;
label: string;
controllers: { controller: string; actions: { actionId: string; action: string } }[];
}[];
};
onChange: ({ target: { value } }: PseudoEvent) => void;
onChangeSelectAll: ({ target: { value } }: PseudoEvent) => void;
setSelectedAction: ({ target: { value } }: PseudoEvent) => void;
}
interface ApiTokenPermissionsContextProviderProps extends APITokenPermissionsContextProviderProps {
children: React.ReactNode[];
}
const ApiTokenPermissionsContext = React.createContext<APITokenPermissionsContextProviderProps>({
selectedAction: null,
routes: [],
selectedActions: [],
data: {
allActionsIds: [],
permissions: [],
},
onChange: () => {},
onChangeSelectAll: () => {},
setSelectedAction: () => {},
});
const ApiTokenPermissionsContextProvider = ({
children,
...rest
}: ApiTokenPermissionsContextProviderProps) => {
return (
<ApiTokenPermissionsContext.Provider value={rest}>
{children}
</ApiTokenPermissionsContext.Provider>
);
};
const useApiTokenPermissionsContext = () => React.useContext(ApiTokenPermissionsContext);
export {
ApiTokenPermissionsContext,
ApiTokenPermissionsContextProvider,
useApiTokenPermissionsContext,
};

View File

@ -4,42 +4,28 @@ import {
Middleware,
Reducer,
combineReducers,
createSelector,
Selector,
} from '@reduxjs/toolkit';
import { useDispatch, useStore, TypedUseSelectorHook, useSelector } from 'react-redux';
import { RBACReducer } from '../../components/RBACProvider';
// @ts-expect-error no types, yet.
import rbacProviderReducer from '../components/RBACProvider/reducer';
import rbacManagerReducer from '../../content-manager/hooks/useSyncRbac/reducer';
// @ts-expect-error no types, yet.
import rbacManagerReducer from '../content-manager/hooks/useSyncRbac/reducer';
import cmAppReducer from '../../content-manager/pages/App/reducer';
// @ts-expect-error no types, yet.
import cmAppReducer from '../content-manager/pages/App/reducer';
import editViewLayoutManagerReducer from '../../content-manager/pages/EditViewLayoutManager/reducer';
// @ts-expect-error no types, yet.
import editViewLayoutManagerReducer from '../content-manager/pages/EditViewLayoutManager/reducer';
import listViewReducer from '../../content-manager/pages/ListView/reducer';
// @ts-expect-error no types, yet.
import listViewReducer from '../content-manager/pages/ListView/reducer';
import editViewCrudReducer from '../../content-manager/sharedReducers/crudReducer/reducer';
// @ts-expect-error no types, yet.
import editViewCrudReducer from '../content-manager/sharedReducers/crudReducer/reducer';
// @ts-expect-error no types, yet.
import appReducer from '../pages/App/reducer';
const createReducer = (
appReducers: Record<string, Reducer>,
asyncReducers: Record<string, Reducer>
) => {
return combineReducers({
...appReducers,
...asyncReducers,
});
};
import appReducer from '../../pages/App/reducer';
/**
* @description Static reducers are ones we know, they live in the admin package.
*/
const staticReducers: Record<string, Reducer> = {
const staticReducers = {
admin_app: appReducer,
rbacProvider: rbacProviderReducer,
rbacProvider: RBACReducer,
'content-manager_app': cmAppReducer,
'content-manager_listView': listViewReducer,
'content-manager_rbacManager': rbacManagerReducer,
@ -60,8 +46,13 @@ const injectReducerStoreEnhancer: (appReducers: Record<string, Reducer>) => Stor
asyncReducers,
injectReducer: (key: string, asyncReducer: Reducer) => {
asyncReducers[key] = asyncReducer;
// @ts-expect-error we dynamically add reducers which makes the types uncomfortable.
store.replaceReducer(createReducer(appReducers, asyncReducers));
store.replaceReducer(
// @ts-expect-error we dynamically add reducers which makes the types uncomfortable.
combineReducers({
...appReducers,
...asyncReducers,
})
);
},
};
};
@ -74,10 +65,10 @@ const configureStoreImpl = (
appMiddlewares: Array<() => Middleware> = [],
injectedReducers: Record<string, Reducer> = {}
) => {
const coreReducers = { ...staticReducers, ...injectedReducers };
const coreReducers = { ...staticReducers, ...injectedReducers } as const;
const store = configureStore({
reducer: createReducer(coreReducers, {}),
reducer: coreReducers,
devTools: process.env.NODE_ENV !== 'production',
middleware: (getDefaultMiddleware) => [
...getDefaultMiddleware(),
@ -95,20 +86,6 @@ type Store = ReturnType<typeof configureStoreImpl> & {
};
type RootState = ReturnType<Store['getState']>;
type AppDispatch = Store['dispatch'];
const useTypedDispatch: () => AppDispatch = useDispatch;
const useTypedStore = useStore as () => Store;
const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
const createTypedSelector = <TResult>(selector: Selector<RootState, TResult>) =>
createSelector((state: RootState) => state, selector);
export {
useTypedDispatch,
useTypedStore,
useTypedSelector,
configureStoreImpl as configureStore,
createTypedSelector,
};
export type { RootState };
export { configureStoreImpl as configureStore };
export type { RootState, Store };

View File

@ -0,0 +1,15 @@
import { createSelector, Selector } from '@reduxjs/toolkit';
import { useDispatch, useStore, TypedUseSelectorHook, useSelector } from 'react-redux';
import type { RootState, Store } from './configure';
type AppDispatch = Store['dispatch'];
const useTypedDispatch: () => AppDispatch = useDispatch;
const useTypedStore = useStore as () => Store;
const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
const createTypedSelector = <TResult>(selector: Selector<RootState, TResult>) =>
createSelector((state: RootState) => state, selector);
export { useTypedDispatch, useTypedStore, useTypedSelector, createTypedSelector };

View File

@ -3,7 +3,7 @@ import React from 'react';
import { renderHook } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureStore } from '../../../core/store';
import { configureStore } from '../../../core/store/configure';
import { useInjectReducer } from '../useInjectReducer';
const store = configureStore();

View File

@ -33,10 +33,12 @@ const MarketplacePage = lazy(() =>
import(/* webpackChunkName: "Admin_marketplace" */ '../MarketplacePage')
);
const NotFoundPage = lazy(() =>
import(/* webpackChunkName: "Admin_NotFoundPage" */ '../NotFoundPage')
import(/* webpackChunkName: "Admin_NotFoundPage" */ '../NotFoundPage').then(({ NotFoundPage }) => ({ default: NotFoundPage }))
);
const InternalErrorPage = lazy(() =>
import(/* webpackChunkName: "Admin_InternalErrorPage" */ '../InternalErrorPage')
import(/* webpackChunkName: "Admin_InternalErrorPage" */ '../InternalErrorPage').then(({ InternalErrorPage }) => ({
default: InternalErrorPage
}))
);
const ProfilePage = lazy(() =>

View File

@ -27,13 +27,15 @@ import { useConfiguration } from '../../hooks/useConfiguration';
import { useEnterprise } from '../../hooks/useEnterprise';
import { createRoute, makeUniqueRoutes } from '../../utils';
import AuthPage from '../AuthPage';
import NotFoundPage from '../NotFoundPage';
import { NotFoundPage } from '../NotFoundPage';
import UseCasePage from '../UseCasePage';
import { ROUTES_CE, SET_ADMIN_PERMISSIONS } from './constants';
const AuthenticatedApp = lazy(() =>
import(/* webpackChunkName: "Admin-authenticatedApp" */ '../../components/AuthenticatedApp')
import(/* webpackChunkName: "Admin-authenticatedApp" */ '../../components/AuthenticatedApp').then(
(mod) => ({ default: mod.AuthenticatedApp })
)
);
function App() {

View File

@ -4,15 +4,21 @@
* This is the page we show when the user gets a 500 error
*
*/
import React from 'react';
import { ContentLayout, EmptyStateLayout, HeaderLayout, Main } from '@strapi/design-system';
import { LinkButton, useFocusWhenNavigate } from '@strapi/helper-plugin';
import {
ContentLayout,
EmptyStateLayout,
HeaderLayout,
LinkButton,
Main,
} from '@strapi/design-system';
import { useFocusWhenNavigate } from '@strapi/helper-plugin';
import { ArrowRight, EmptyPictures } from '@strapi/icons';
import { useIntl } from 'react-intl';
const InternalErrorPage = () => {
export const InternalErrorPage = () => {
const { formatMessage } = useIntl();
useFocusWhenNavigate();
return (
@ -46,5 +52,3 @@ const InternalErrorPage = () => {
</Main>
);
};
export default InternalErrorPage;

View File

@ -1,433 +0,0 @@
import React from 'react';
import { lightTheme, ThemeProvider } from '@strapi/design-system';
import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { IntlProvider } from 'react-intl';
import { Router } from 'react-router-dom';
import InternalErrorPage from '../index';
const history = createMemoryHistory();
const App = (
<ThemeProvider theme={lightTheme}>
<IntlProvider locale="en" messages={{}} textComponent="span">
<Router history={history}>
<InternalErrorPage />
</Router>
</IntlProvider>
</ThemeProvider>
);
describe('InternalErrorPage', () => {
it('renders and matches the snapshot', () => {
const {
container: { firstChild },
} = render(App);
expect(firstChild).toMatchInlineSnapshot(`
.c6 {
font-weight: 600;
font-size: 2rem;
line-height: 1.25;
color: #32324d;
}
.c13 {
font-weight: 500;
font-size: 1rem;
line-height: 1.25;
color: #666687;
text-align: center;
}
.c18 {
font-size: 0.75rem;
line-height: 1.33;
font-weight: 600;
color: #ffffff;
}
.c1 {
background: #f6f6f9;
padding-top: 40px;
padding-right: 56px;
padding-bottom: 40px;
padding-left: 56px;
}
.c3 {
min-width: 0;
}
.c7 {
padding-right: 56px;
padding-left: 56px;
}
.c8 {
background: #ffffff;
padding: 64px;
border-radius: 4px;
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
}
.c10 {
padding-bottom: 24px;
}
.c12 {
padding-bottom: 16px;
}
.c14 {
background: #4945ff;
padding-top: 8px;
padding-right: 16px;
padding-bottom: 8px;
padding-left: 16px;
border-radius: 4px;
border-color: #4945ff;
border: 1px solid #4945ff;
}
.c2 {
-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;
}
.c4 {
-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;
}
.c9 {
-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: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c15 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
gap: 8px;
}
.c16 {
position: relative;
outline: none;
}
.c16 > svg {
height: 12px;
width: 12px;
}
.c16 > svg > g,
.c16 > svg path {
fill: #ffffff;
}
.c16[aria-disabled='true'] {
pointer-events: none;
}
.c16: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;
}
.c16:focus-visible {
outline: none;
}
.c16:focus-visible:after {
border-radius: 8px;
content: '';
position: absolute;
top: -5px;
bottom: -5px;
left: -5px;
right: -5px;
border: 2px solid #4945ff;
}
.c11 svg {
height: 5.5rem;
}
.c0:focus-visible {
outline: none;
}
.c17 {
-webkit-text-decoration: none;
text-decoration: none;
border: 1px solid #d9d8ff;
background: #f0f0ff;
}
.c17[aria-disabled='true'] {
border: 1px solid #dcdce4;
background: #eaeaef;
}
.c17[aria-disabled='true'] .c5 {
color: #666687;
}
.c17[aria-disabled='true'] svg > g,
.c17[aria-disabled='true'] svg path {
fill: #666687;
}
.c17[aria-disabled='true']:active {
border: 1px solid #dcdce4;
background: #eaeaef;
}
.c17[aria-disabled='true']:active .c5 {
color: #666687;
}
.c17[aria-disabled='true']:active svg > g,
.c17[aria-disabled='true']:active svg path {
fill: #666687;
}
.c17:hover {
background-color: #ffffff;
}
.c17:active {
background-color: #ffffff;
border: 1px solid #4945ff;
}
.c17:active .c5 {
color: #4945ff;
}
.c17:active svg > g,
.c17:active svg path {
fill: #4945ff;
}
.c17 .c5 {
color: #271fe0;
}
.c17 svg > g,
.c17 svg path {
fill: #271fe0;
}
<main
aria-labelledby="title"
class="c0"
id="main-content"
tabindex="-1"
>
<div
style="height: 0px;"
>
<div
class="c1"
data-strapi-header="true"
>
<div
class="c2"
>
<div
class="c3 c4"
>
<h1
class="c5 c6"
id="title"
>
Page not found
</h1>
</div>
</div>
</div>
</div>
<div
class="c7"
>
<div
class="c8 c9"
>
<div
aria-hidden="true"
class="c10 c11"
>
<svg
fill="none"
height="1rem"
viewBox="0 0 216 120"
width="10rem"
xmlns="http://www.w3.org/2000/svg"
>
<g
opacity="0.88"
>
<path
clip-rule="evenodd"
d="M119 28a7 7 0 1 1 0 14h64a7 7 0 1 1 0 14h22a7 7 0 1 1 0 14h-19a7 7 0 1 0 0 14h6a7 7 0 1 1 0 14h-52a7.024 7.024 0 0 1-1.5-.161A7.024 7.024 0 0 1 137 98H46a7 7 0 1 1 0-14H7a7 7 0 1 1 0-14h40a7 7 0 1 0 0-14H22a7 7 0 1 1 0-14h40a7 7 0 1 1 0-14h57Zm90 56a7 7 0 1 1 0 14 7 7 0 0 1 0-14Z"
fill="#D9D8FF"
fill-opacity="0.8"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="m73.83 102.273-8.621 1.422a4 4 0 0 1-4.518-3.404L49.557 21.069a4 4 0 0 1 3.404-4.518l78.231-10.994a4 4 0 0 1 4.518 3.404c.475 3.377 2.408 16.468 2.572 17.63"
fill="#fff"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="m71.805 98.712-3.696.526a3.618 3.618 0 0 1-4.096-3.085l-9.995-71.925a3.646 3.646 0 0 1 3.097-4.108l71.037-10.096a3.618 3.618 0 0 1 4.097 3.085l.859 6.18 9.205 66.599c.306 2.212-1.219 4.257-3.407 4.566a4.31 4.31 0 0 1-.071.01l-67.03 8.248Z"
fill="#F0F0FF"
fill-rule="evenodd"
/>
<path
d="m69.278 103.123-4.07.572a4 4 0 0 1-4.517-3.404L49.557 21.069a4 4 0 0 1 3.404-4.518l78.231-10.994a4 4 0 0 1 4.518 3.404l.957 6.808M137.5 20.38l.5 3.12"
stroke="#7B79FF"
stroke-linecap="round"
stroke-width="2.5"
/>
<path
clip-rule="evenodd"
d="M164.411 30.299 85.844 22.04a2.74 2.74 0 0 0-2.018.598 2.741 2.741 0 0 0-1.004 1.85l-8.363 79.561c-.079.755.155 1.471.598 2.018a2.74 2.74 0 0 0 1.85 1.004l78.567 8.258a2.739 2.739 0 0 0 2.018-.598 2.741 2.741 0 0 0 1.005-1.849l8.362-79.562a2.743 2.743 0 0 0-.598-2.018 2.74 2.74 0 0 0-1.85-1.004Z"
fill="#fff"
fill-rule="evenodd"
stroke="#7B79FF"
stroke-width="2.5"
/>
<path
clip-rule="evenodd"
d="m92.99 30.585 62.655 6.585a3 3 0 0 1 2.67 3.297l-5.54 52.71a3 3 0 0 1-3.297 2.67L86.823 89.26a3 3 0 0 1-2.67-3.297l5.54-52.71a3 3 0 0 1 3.297-2.67Z"
fill="#fff"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="m92.74 73.878 9.798-6.608a4 4 0 0 1 5.168.594l7.173 7.723a1 1 0 0 0 1.362.096l15.34-12.43a4 4 0 0 1 5.878.936l9.98 15.438 1.434 2.392-.687 8.124a1 1 0 0 1-1.106.91l-56.963-6.329a1 1 0 0 1-.886-1.085l.755-8.199 2.755-1.562Z"
fill="#F0F0FF"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="M155.514 38.413 92.86 31.828c-.481-.05-.937.098-1.285.38a1.745 1.745 0 0 0-.639 1.177l-5.54 52.71c-.05.48.099.936.38 1.284.282.348.697.589 1.178.64l62.655 6.585a1.747 1.747 0 0 0 1.923-1.558l5.54-52.71a1.75 1.75 0 0 0-1.558-1.923Z"
stroke="#7B79FF"
stroke-width="2.5"
/>
<path
d="M104.405 55.917a6 6 0 1 0 1.254-11.934 6 6 0 0 0-1.254 11.934Z"
fill="#F0F0FF"
stroke="#7B79FF"
stroke-width="2.5"
/>
<path
d="m90.729 75.425 11.809-8.155a4 4 0 0 1 5.168.594l7.173 7.723a1 1 0 0 0 1.362.096l15.34-12.43a4 4 0 0 1 5.878.936l11.064 17.556"
stroke="#7B79FF"
stroke-linecap="round"
stroke-width="2.5"
/>
</g>
</svg>
</div>
<div
class="c12"
>
<p
class="c5 c13"
>
An error occured
</p>
</div>
<a
aria-current="page"
aria-disabled="false"
class="c14 c15 c16 c17 active"
href="/"
>
<span
class="c5 c18"
>
Back to homepage
</span>
<div
aria-hidden="true"
class="c4"
>
<svg
fill="none"
height="1rem"
viewBox="0 0 24 24"
width="1rem"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 10.7c0-.11.09-.2.2-.2h18.06l-8.239-8.239a.2.2 0 0 1 0-.282L11.86.14a.2.2 0 0 1 .282 0L23.86 11.86a.2.2 0 0 1 0 .282L12.14 23.86a.2.2 0 0 1-.282 0L10.02 22.02a.2.2 0 0 1 0-.282L18.26 13.5H.2a.2.2 0 0 1-.2-.2v-2.6Z"
fill="#212134"
/>
</svg>
</div>
</a>
</div>
</div>
</main>
`);
});
});

View File

@ -4,14 +4,18 @@
* This is the page we show when the user visits a url that doesn't have a route
*
*/
import React from 'react';
import { ContentLayout, EmptyStateLayout, HeaderLayout, Main } from '@strapi/design-system';
import { LinkButton, useFocusWhenNavigate } from '@strapi/helper-plugin';
import {
ContentLayout,
EmptyStateLayout,
HeaderLayout,
LinkButton,
Main,
} from '@strapi/design-system';
import { useFocusWhenNavigate } from '@strapi/helper-plugin';
import { ArrowRight, EmptyPictures } from '@strapi/icons';
import { useIntl } from 'react-intl';
const NoContentType = () => {
export const NotFoundPage = () => {
const { formatMessage } = useIntl();
useFocusWhenNavigate();
@ -46,5 +50,3 @@ const NoContentType = () => {
</Main>
);
};
export default NoContentType;

View File

@ -1,433 +0,0 @@
import React from 'react';
import { lightTheme, ThemeProvider } from '@strapi/design-system';
import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { IntlProvider } from 'react-intl';
import { Router } from 'react-router-dom';
import NotFoundPage from '../index';
const history = createMemoryHistory();
const App = (
<ThemeProvider theme={lightTheme}>
<IntlProvider locale="en" messages={{}} textComponent="span">
<Router history={history}>
<NotFoundPage />
</Router>
</IntlProvider>
</ThemeProvider>
);
describe('NotFoundPage', () => {
it('renders and matches the snapshot', () => {
const {
container: { firstChild },
} = render(App);
expect(firstChild).toMatchInlineSnapshot(`
.c6 {
font-weight: 600;
font-size: 2rem;
line-height: 1.25;
color: #32324d;
}
.c13 {
font-weight: 500;
font-size: 1rem;
line-height: 1.25;
color: #666687;
text-align: center;
}
.c18 {
font-size: 0.75rem;
line-height: 1.33;
font-weight: 600;
color: #ffffff;
}
.c1 {
background: #f6f6f9;
padding-top: 40px;
padding-right: 56px;
padding-bottom: 40px;
padding-left: 56px;
}
.c3 {
min-width: 0;
}
.c7 {
padding-right: 56px;
padding-left: 56px;
}
.c8 {
background: #ffffff;
padding: 64px;
border-radius: 4px;
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
}
.c10 {
padding-bottom: 24px;
}
.c12 {
padding-bottom: 16px;
}
.c14 {
background: #4945ff;
padding-top: 8px;
padding-right: 16px;
padding-bottom: 8px;
padding-left: 16px;
border-radius: 4px;
border-color: #4945ff;
border: 1px solid #4945ff;
}
.c2 {
-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;
}
.c4 {
-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;
}
.c9 {
-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: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c15 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
gap: 8px;
}
.c16 {
position: relative;
outline: none;
}
.c16 > svg {
height: 12px;
width: 12px;
}
.c16 > svg > g,
.c16 > svg path {
fill: #ffffff;
}
.c16[aria-disabled='true'] {
pointer-events: none;
}
.c16: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;
}
.c16:focus-visible {
outline: none;
}
.c16:focus-visible:after {
border-radius: 8px;
content: '';
position: absolute;
top: -5px;
bottom: -5px;
left: -5px;
right: -5px;
border: 2px solid #4945ff;
}
.c11 svg {
height: 5.5rem;
}
.c0:focus-visible {
outline: none;
}
.c17 {
-webkit-text-decoration: none;
text-decoration: none;
border: 1px solid #d9d8ff;
background: #f0f0ff;
}
.c17[aria-disabled='true'] {
border: 1px solid #dcdce4;
background: #eaeaef;
}
.c17[aria-disabled='true'] .c5 {
color: #666687;
}
.c17[aria-disabled='true'] svg > g,
.c17[aria-disabled='true'] svg path {
fill: #666687;
}
.c17[aria-disabled='true']:active {
border: 1px solid #dcdce4;
background: #eaeaef;
}
.c17[aria-disabled='true']:active .c5 {
color: #666687;
}
.c17[aria-disabled='true']:active svg > g,
.c17[aria-disabled='true']:active svg path {
fill: #666687;
}
.c17:hover {
background-color: #ffffff;
}
.c17:active {
background-color: #ffffff;
border: 1px solid #4945ff;
}
.c17:active .c5 {
color: #4945ff;
}
.c17:active svg > g,
.c17:active svg path {
fill: #4945ff;
}
.c17 .c5 {
color: #271fe0;
}
.c17 svg > g,
.c17 svg path {
fill: #271fe0;
}
<main
aria-labelledby="title"
class="c0"
id="main-content"
tabindex="-1"
>
<div
style="height: 0px;"
>
<div
class="c1"
data-strapi-header="true"
>
<div
class="c2"
>
<div
class="c3 c4"
>
<h1
class="c5 c6"
id="title"
>
Page not found
</h1>
</div>
</div>
</div>
</div>
<div
class="c7"
>
<div
class="c8 c9"
>
<div
aria-hidden="true"
class="c10 c11"
>
<svg
fill="none"
height="1rem"
viewBox="0 0 216 120"
width="10rem"
xmlns="http://www.w3.org/2000/svg"
>
<g
opacity="0.88"
>
<path
clip-rule="evenodd"
d="M119 28a7 7 0 1 1 0 14h64a7 7 0 1 1 0 14h22a7 7 0 1 1 0 14h-19a7 7 0 1 0 0 14h6a7 7 0 1 1 0 14h-52a7.024 7.024 0 0 1-1.5-.161A7.024 7.024 0 0 1 137 98H46a7 7 0 1 1 0-14H7a7 7 0 1 1 0-14h40a7 7 0 1 0 0-14H22a7 7 0 1 1 0-14h40a7 7 0 1 1 0-14h57Zm90 56a7 7 0 1 1 0 14 7 7 0 0 1 0-14Z"
fill="#D9D8FF"
fill-opacity="0.8"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="m73.83 102.273-8.621 1.422a4 4 0 0 1-4.518-3.404L49.557 21.069a4 4 0 0 1 3.404-4.518l78.231-10.994a4 4 0 0 1 4.518 3.404c.475 3.377 2.408 16.468 2.572 17.63"
fill="#fff"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="m71.805 98.712-3.696.526a3.618 3.618 0 0 1-4.096-3.085l-9.995-71.925a3.646 3.646 0 0 1 3.097-4.108l71.037-10.096a3.618 3.618 0 0 1 4.097 3.085l.859 6.18 9.205 66.599c.306 2.212-1.219 4.257-3.407 4.566a4.31 4.31 0 0 1-.071.01l-67.03 8.248Z"
fill="#F0F0FF"
fill-rule="evenodd"
/>
<path
d="m69.278 103.123-4.07.572a4 4 0 0 1-4.517-3.404L49.557 21.069a4 4 0 0 1 3.404-4.518l78.231-10.994a4 4 0 0 1 4.518 3.404l.957 6.808M137.5 20.38l.5 3.12"
stroke="#7B79FF"
stroke-linecap="round"
stroke-width="2.5"
/>
<path
clip-rule="evenodd"
d="M164.411 30.299 85.844 22.04a2.74 2.74 0 0 0-2.018.598 2.741 2.741 0 0 0-1.004 1.85l-8.363 79.561c-.079.755.155 1.471.598 2.018a2.74 2.74 0 0 0 1.85 1.004l78.567 8.258a2.739 2.739 0 0 0 2.018-.598 2.741 2.741 0 0 0 1.005-1.849l8.362-79.562a2.743 2.743 0 0 0-.598-2.018 2.74 2.74 0 0 0-1.85-1.004Z"
fill="#fff"
fill-rule="evenodd"
stroke="#7B79FF"
stroke-width="2.5"
/>
<path
clip-rule="evenodd"
d="m92.99 30.585 62.655 6.585a3 3 0 0 1 2.67 3.297l-5.54 52.71a3 3 0 0 1-3.297 2.67L86.823 89.26a3 3 0 0 1-2.67-3.297l5.54-52.71a3 3 0 0 1 3.297-2.67Z"
fill="#fff"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="m92.74 73.878 9.798-6.608a4 4 0 0 1 5.168.594l7.173 7.723a1 1 0 0 0 1.362.096l15.34-12.43a4 4 0 0 1 5.878.936l9.98 15.438 1.434 2.392-.687 8.124a1 1 0 0 1-1.106.91l-56.963-6.329a1 1 0 0 1-.886-1.085l.755-8.199 2.755-1.562Z"
fill="#F0F0FF"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="M155.514 38.413 92.86 31.828c-.481-.05-.937.098-1.285.38a1.745 1.745 0 0 0-.639 1.177l-5.54 52.71c-.05.48.099.936.38 1.284.282.348.697.589 1.178.64l62.655 6.585a1.747 1.747 0 0 0 1.923-1.558l5.54-52.71a1.75 1.75 0 0 0-1.558-1.923Z"
stroke="#7B79FF"
stroke-width="2.5"
/>
<path
d="M104.405 55.917a6 6 0 1 0 1.254-11.934 6 6 0 0 0-1.254 11.934Z"
fill="#F0F0FF"
stroke="#7B79FF"
stroke-width="2.5"
/>
<path
d="m90.729 75.425 11.809-8.155a4 4 0 0 1 5.168.594l7.173 7.723a1 1 0 0 0 1.362.096l15.34-12.43a4 4 0 0 1 5.878.936l11.064 17.556"
stroke="#7B79FF"
stroke-linecap="round"
stroke-width="2.5"
/>
</g>
</svg>
</div>
<div
class="c12"
>
<p
class="c5 c13"
>
Oops! We can't seem to find the page you're looging for...
</p>
</div>
<a
aria-current="page"
aria-disabled="false"
class="c14 c15 c16 c17 active"
href="/"
>
<span
class="c5 c18"
>
Back to homepage
</span>
<div
aria-hidden="true"
class="c4"
>
<svg
fill="none"
height="1rem"
viewBox="0 0 24 24"
width="1rem"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 10.7c0-.11.09-.2.2-.2h18.06l-8.239-8.239a.2.2 0 0 1 0-.282L11.86.14a.2.2 0 0 1 .282 0L23.86 11.86a.2.2 0 0 1 0 .282L12.14 23.86a.2.2 0 0 1-.282 0L10.02 22.02a.2.2 0 0 1 0-.282L18.26 13.5H.2a.2.2 0 0 1-.2-.2v-2.6Z"
fill="#212134"
/>
</svg>
</div>
</a>
</div>
</div>
</main>
`);
});
});

View File

@ -3,7 +3,7 @@ import React from 'react';
import { Flex, GridItem, Typography } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import { useApiTokenPermissionsContext } from '../../../../../../../contexts/ApiTokenPermissions';
import { useApiTokenPermissionsContext } from '../../../../../../../contexts/apiTokenPermissions';
import BoundRoute from '../BoundRoute';
const ActionBoundRoutes = () => {

View File

@ -17,7 +17,7 @@ import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import styled from 'styled-components';
import { useApiTokenPermissionsContext } from '../../../../../../../contexts/ApiTokenPermissions';
import { useApiTokenPermissionsContext } from '../../../../../../../contexts/apiTokenPermissions';
import CheckboxWrapper from './CheckBoxWrapper';

View File

@ -3,7 +3,7 @@ import React, { memo } from 'react';
import { Flex, Grid, GridItem, Typography } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import { useApiTokenPermissionsContext } from '../../../../../../../contexts/ApiTokenPermissions';
import { useApiTokenPermissionsContext } from '../../../../../../../contexts/apiTokenPermissions';
import ActionBoundRoutes from '../ActionBoundRoutes';
import ContentTypesSection from '../ContenTypesSection';

View File

@ -18,7 +18,7 @@ import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { useHistory, useRouteMatch } from 'react-router-dom';
import { ApiTokenPermissionsContextProvider } from '../../../../../contexts/ApiTokenPermissions';
import { ApiTokenPermissionsContextProvider } from '../../../../../contexts/apiTokenPermissions';
import { formatAPIErrors } from '../../../../../utils';
import { selectAdminPermissions } from '../../../../App/selectors';
import { API_TOKEN_TYPE } from '../../../components/Tokens/constants';

View File

@ -148,7 +148,7 @@
"@strapi/strapi": "^4.3.4"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"nx": {

View File

@ -31,7 +31,7 @@
"qs": "6.11.1"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"strapi": {

View File

@ -63,7 +63,7 @@
"styled-components": "5.3.3"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"strapi": {

View File

@ -82,7 +82,7 @@
"@strapi/strapi": "^4.14.4"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
}
}

View File

@ -55,7 +55,7 @@
"tsconfig": "4.14.4"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
}
}

View File

@ -85,7 +85,7 @@
"styled-components": "5.3.3"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"strapi": {

View File

@ -104,7 +104,7 @@
"styled-components": "^5.3.3"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"nx": {

View File

@ -3,15 +3,11 @@ import * as React from 'react';
import { Redirect } from 'react-router-dom';
import { useNotification } from '../features/Notifications';
import { useRBACProvider } from '../features/RBAC';
import { Permission, useRBACProvider } from '../features/RBAC';
import { hasPermissions } from '../utils/hasPermissions';
import { LoadingIndicatorPage } from './LoadingIndicatorPage';
import type { domain } from '@strapi/permissions';
type Permission = domain.permission.Permission;
export interface CheckPagePermissionsProps {
children: React.ReactNode;
permissions?: Permission[];

View File

@ -1,13 +1,9 @@
import * as React from 'react';
import { useNotification } from '../features/Notifications';
import { useRBACProvider } from '../features/RBAC';
import { useRBACProvider, Permission } from '../features/RBAC';
import { hasPermissions } from '../utils/hasPermissions';
import type { domain } from '@strapi/permissions';
type Permission = domain.permission.Permission;
// NOTE: this component is very similar to the CheckPagePermissions
// except that it does not handle redirections nor loading state

View File

@ -115,3 +115,5 @@ export {
useAppInfo,
useAppInfos,
};
export type { AppInfoContextValue, AppInfoProviderProps };

View File

@ -128,3 +128,10 @@ const CustomFieldsProvider = ({ children, customFields }: CustomFieldsProviderPr
const useCustomFields = () => React.useContext(CustomFieldsContext);
export { CustomFieldsContext, CustomFieldsProvider, useCustomFields };
export type {
CustomFieldsProviderProps,
CustomField,
CustomFieldComponents,
CustomFieldOption,
CustomFieldOptions,
};

View File

@ -4,7 +4,7 @@ import * as React from 'react';
* Context
* -----------------------------------------------------------------------------------------------*/
export interface LibraryContextValue {
interface LibraryContextValue {
fields?: Record<string, React.ComponentType>;
components?: Record<string, React.ComponentType>;
}
@ -32,3 +32,4 @@ const LibraryProvider = ({ children, fields, components }: LibraryProviderProps)
const useLibrary = () => React.useContext(LibraryContext);
export { LibraryContext, LibraryProvider, useLibrary };
export type { LibraryContextValue, LibraryProviderProps };

View File

@ -1,9 +1,23 @@
import * as React from 'react';
import type { domain } from '@strapi/permissions';
import type { Entity } from '@strapi/types';
import type { QueryObserverBaseResult } from 'react-query';
type Permission = domain.permission.Permission;
/**
* This is duplicated from the `@strapi/admin` package.
*/
type Permission = {
id?: Entity.ID;
action: string;
subject: string | null;
actionParameters?: object;
properties?: {
fields?: string[];
locales?: string[];
[key: string]: unknown;
};
conditions?: string[];
};
/* -------------------------------------------------------------------------------------------------
* Context

View File

@ -4,10 +4,7 @@ import { LinkProps } from 'react-router-dom';
import { TranslationMessage } from '../types';
import type { domain } from '@strapi/permissions';
type Permission = domain.permission.Permission;
import type { Permission } from './RBAC';
interface MenuItem extends Pick<LinkProps, 'to'> {
to: string;
icon: React.ElementType;
@ -62,7 +59,7 @@ type RunHookWaterfall = <InitialValue, Store>(
store: Store
) => unknown | Promise<unknown>;
export interface StrapiAppContextValue {
interface StrapiAppContextValue {
menu: MenuItem[];
plugins: Record<string, Plugin>;
settings: Record<string, StrapiAppSetting>;
@ -124,3 +121,13 @@ const StrapiAppProvider = ({
const useStrapiApp = () => React.useContext(StrapiAppContext);
export { StrapiAppContext, StrapiAppProvider, useStrapiApp };
export type {
StrapiAppProviderProps,
StrapiAppContextValue,
MenuItem,
Plugin,
StrapiAppSettingLink,
StrapiAppSetting,
RunHookSeries,
RunHookWaterfall,
};

View File

@ -2,15 +2,12 @@ import { useCallback, useMemo, useState } from 'react';
import { useQueries } from 'react-query';
import { useRBACProvider } from '../features/RBAC';
import { useRBACProvider, Permission } from '../features/RBAC';
import { useFetchClient } from './useFetchClient';
import type { domain } from '@strapi/permissions';
import type { AxiosResponse } from 'axios';
type Permission = domain.permission.Permission;
type AllowedActions = Record<string, boolean>;
export const useRBAC = (
@ -71,8 +68,7 @@ export const useRBAC = (
data: { data },
} = await post<
{ data: { data: boolean[] } },
AxiosResponse<{ data: { data: boolean[] } }>,
{ permissions: Permission[] }
AxiosResponse<{ data: { data: boolean[] } }>
>('/admin/permissions/check', {
permissions: matchingPermissions.map(({ action, subject }) => ({
action,

View File

@ -98,26 +98,16 @@ const auth = {
sessionStorage.clear();
},
get<T extends keyof StorageItems>(key: T): StorageItems[T] | string | null {
const localStorageItem = localStorage.getItem(key);
if (localStorageItem) {
get<T extends keyof StorageItems>(key: T): StorageItems[T] | null {
const item = localStorage.getItem(key) ?? sessionStorage.getItem(key);
if (item) {
try {
const parsedItem = JSON.parse(localStorageItem);
const parsedItem = JSON.parse(item);
return parsedItem;
} catch (error) {
// Failed to parse return the string value
return localStorageItem;
}
}
const sessionStorageItem = sessionStorage.getItem(key);
if (sessionStorageItem) {
try {
const parsedItem = JSON.parse(sessionStorageItem);
return parsedItem;
} catch (error) {
// Failed to parse return the string value
return sessionStorageItem;
// @ts-expect-error - this is fine
return item;
}
}

View File

@ -1,10 +1,8 @@
import { getFetchClient } from './getFetchClient';
import type { domain } from '@strapi/permissions';
import type { Permission } from '../features/RBAC';
import type { GenericAbortSignal } from 'axios';
type Permission = domain.permission.Permission;
const findMatchingPermissions = (userPermissions: Permission[], permissions: Permission[]) =>
userPermissions.reduce<Permission[]>((acc, curr) => {
const associatedPermission = permissions.find(
@ -24,7 +22,7 @@ const formatPermissionsForRequest = (permissions: Permission[]) =>
return {};
}
const returnedPermission: Permission = {
const returnedPermission: Partial<Permission> = {
action: permission.action,
};

View File

@ -5,9 +5,7 @@ import {
shouldCheckPermissions,
} from '../hasPermissions';
import type { domain } from '@strapi/permissions';
type Permission = domain.permission.Permission;
import type { Permission } from '../../features/RBAC';
const hasPermissionsTestData: Record<string, Record<string, Permission[]>> = {
userPermissions: {

View File

@ -49,7 +49,7 @@
"tsconfig": "4.14.4"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
}
}

View File

@ -94,6 +94,7 @@ Strapi only supports maintenance and LTS versions of Node.js. Please refer to th
| Strapi Version | Recommended | Minimum |
| --------------- | ----------- | ------- |
| 4.14.5 and up | 20.x | 18.x |
| 4.11.0 and up | 18.x | 16.x |
| 4.3.9 to 4.10.x | 18.x | 14.x |
| 4.0.x to 4.3.8 | 16.x | 14.x |

View File

@ -171,7 +171,7 @@
"typescript": "5.2.2"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"nx": {

View File

@ -16,7 +16,7 @@ export default async ({ force, ...opts }: ActionOptions) => {
* Notify users this is an experimental command and get them to approve first
* this can be opted out by setting the argument --yes
*/
await notifyExperimentalCommand({ force });
await notifyExperimentalCommand('plugin:build', { force });
const cwd = process.cwd();

View File

@ -0,0 +1,103 @@
import boxen from 'boxen';
import chalk from 'chalk';
import { ConfigBundle, WatchCLIOptions, watch } from '@strapi/pack-up';
import { notifyExperimentalCommand } from '../../../utils/helpers';
import { createLogger } from '../../../utils/logger';
import { Export, loadPkg, validatePkg } from '../../../utils/pkg';
interface ActionOptions extends WatchCLIOptions {
force?: boolean;
}
export default async ({ force, ...opts }: ActionOptions) => {
const logger = createLogger({ debug: opts.debug, silent: opts.silent, timestamp: false });
try {
/**
* Notify users this is an experimental command and get them to approve first
* this can be opted out by setting the argument --yes
*/
await notifyExperimentalCommand('plugin:watch', { force });
const cwd = process.cwd();
const pkg = await loadPkg({ cwd, logger });
const pkgJson = await validatePkg({ pkg });
if (!pkgJson.exports['./strapi-admin'] && !pkgJson.exports['./strapi-server']) {
throw new Error(
'You need to have either a strapi-admin or strapi-server export in your package.json'
);
}
const bundles: ConfigBundle[] = [];
if (pkgJson.exports['./strapi-admin']) {
const exp = pkgJson.exports['./strapi-admin'] as Export;
const bundle: ConfigBundle = {
source: exp.source,
import: exp.import,
require: exp.require,
runtime: 'web',
};
if (exp.types) {
bundle.types = exp.types;
// TODO: should this be sliced from the source path...?
bundle.tsconfig = './admin/tsconfig.build.json';
}
bundles.push(bundle);
}
if (pkgJson.exports['./strapi-server']) {
const exp = pkgJson.exports['./strapi-server'] as Export;
const bundle: ConfigBundle = {
source: exp.source,
import: exp.import,
require: exp.require,
runtime: 'node',
};
if (exp.types) {
bundle.types = exp.types;
// TODO: should this be sliced from the source path...?
bundle.tsconfig = './server/tsconfig.build.json';
}
bundles.push(bundle);
}
await watch({
cwd,
configFile: false,
config: {
bundles,
dist: './dist',
/**
* ignore the exports map of a plugin, because we're streamlining the
* process and ensuring the server package and admin package are built
* with the correct runtime and their individual tsconfigs
*/
exports: {},
},
...opts,
});
} catch (err) {
logger.error(
'There seems to be an unexpected error, try again with --debug for more information \n'
);
if (err instanceof Error && err.stack) {
console.log(
chalk.red(
boxen(err.stack, {
padding: 1,
align: 'left',
})
)
);
}
process.exit(1);
}
};

View File

@ -0,0 +1,17 @@
import type { StrapiCommand } from '../../../types';
import { runAction } from '../../../utils/helpers';
import action from './action';
/**
* `$ strapi plugin:build`
*/
const command: StrapiCommand = ({ command }) => {
command
.command('plugin:watch')
.description('Watch & compile your strapi plugin for local development.')
.option('-d, --debug', 'Enable debugging mode with verbose logs', false)
.option('--silent', "Don't log anything", false)
.action(runAction('plugin:watch', action));
};
export default command;

View File

@ -28,6 +28,7 @@ import uninstallCommand from './actions/uninstall/command';
import versionCommand from './actions/version/command';
import watchAdminCommand from './actions/watch-admin/command';
import buildPluginCommand from './actions/plugin/build-command/command';
import watchPluginCommand from './actions/plugin/watch/command';
const strapiCommands = {
createAdminUser,
@ -58,6 +59,7 @@ const strapiCommands = {
versionCommand,
watchAdminCommand,
buildPluginCommand,
watchPluginCommand,
} as const;
const buildStrapiCommand = (argv: string[], command = new Command()) => {

View File

@ -151,22 +151,20 @@ const runAction =
* @description Notify users this is an experimental command and get them to approve first
* this can be opted out by passing `yes` as a property of the args object.
*
* @type {(args?: { force?: boolean }) => Promise<void>}
*
* @example
* ```ts
* const { notifyExperimentalCommand } = require('../utils/helpers');
*
* const myCommand = async ({ force }) => {
* await notifyExperimentalCommand({ force });
* await notifyExperimentalCommand('plugin:build', { force });
* }
* ```
*/
const notifyExperimentalCommand = async ({ force }: { force?: boolean } = {}) => {
const notifyExperimentalCommand = async (name: string, { force }: { force?: boolean } = {}) => {
console.log(
boxen(
`The ${chalk.bold(
chalk.underline('plugin:build')
chalk.underline(name)
)} command is considered experimental, use at your own risk.`,
{
title: 'Warning',

View File

@ -68,7 +68,7 @@
"typescript": "5.2.2"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
}
}

View File

@ -69,7 +69,7 @@
"styled-components": "5.3.3"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"strapi": {

View File

@ -62,7 +62,7 @@
"tsconfig": "4.14.4"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
}
}

View File

@ -62,7 +62,7 @@
"copyfiles": "2.4.1"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
}
}

View File

@ -1,4 +1,4 @@
export default {
node: '>=16.0.0 <=20.x.x',
node: '>=18.0.0 <=20.x.x',
npm: '>=6.0.0',
};

View File

@ -62,7 +62,7 @@
"tsconfig": "4.14.4"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
}
}

View File

@ -35,7 +35,7 @@
}
],
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"license": "MIT"

View File

@ -40,7 +40,7 @@
}
],
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"scripts": {

View File

@ -42,7 +42,7 @@
"strapi-server.js"
],
"scripts": {
"build": "NODE_ENV=production strapi plugin:build --force",
"build": "strapi plugin:build --force",
"clean": "run -T rimraf ./dist",
"lint": "yarn lint:project && yarn lint:back && yarn lint:front",
"lint:back": "run -T eslint ./server -c ./server/.eslintrc.js",
@ -54,7 +54,7 @@
"test:front:watch": "run -T cross-env IS_EE=true jest --config ./jest.config.front.js --watchAll",
"test:front:watch:ce": "run -T cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll",
"test:ts:front": "run -T tsc -p admin/tsconfig.json",
"watch": "run -T tsc -w --preserveWatchOutput"
"watch": "strapi plugin:watch"
},
"dependencies": {
"@strapi/design-system": "1.12.2",
@ -83,7 +83,7 @@
"styled-components": "5.3.3"
},
"engines": {
"node": ">=16.0.0 <=20.x.x",
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
},
"strapi": {

Some files were not shown because too many files have changed in this diff Show More