diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index e6b4989a24..62c8c735eb 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -11,7 +11,7 @@ jobs: matrix: node-version: [18.x, 20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} @@ -19,7 +19,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -32,7 +32,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -42,7 +42,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} @@ -95,15 +95,15 @@ jobs: matrix: node-version: [18.x, 20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -114,7 +114,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -124,7 +124,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} @@ -177,15 +177,15 @@ jobs: matrix: node-version: [18.x, 20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -196,7 +196,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -206,7 +206,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} @@ -259,15 +259,15 @@ jobs: matrix: node-version: [18.x, 20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -278,7 +278,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -288,7 +288,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} @@ -341,15 +341,15 @@ jobs: matrix: node-version: [18.x, 20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -360,7 +360,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -370,7 +370,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} @@ -423,15 +423,15 @@ jobs: matrix: node-version: [18.x, 20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -442,7 +442,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -452,7 +452,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} @@ -504,15 +504,15 @@ jobs: matrix: node-version: [18.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -523,7 +523,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -533,7 +533,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} @@ -586,15 +586,15 @@ jobs: matrix: node-version: [18.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -605,7 +605,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -615,7 +615,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} @@ -668,15 +668,15 @@ jobs: # matrix: # node-version: [18.x] # steps: - # - uses: actions/checkout@v3 + # - uses: actions/checkout@v4 # # node setup # - name: Use Node.js ${{ matrix.node-version }} - # uses: actions/setup-node@v3 + # uses: actions/setup-node@v4 # with: # node-version: ${{ matrix.node-version }} - # - uses: pnpm/action-setup@v2 + # - uses: pnpm/action-setup@v3 # name: Install pnpm # with: # version: 8 @@ -687,7 +687,7 @@ jobs: # run: | # echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - # - uses: actions/cache@v3 + # - uses: actions/cache@v4 # name: Setup pnpm cache # with: # path: ${{ env.STORE_PATH }} @@ -739,15 +739,15 @@ jobs: matrix: node-version: [18.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -758,7 +758,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -768,7 +768,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} @@ -822,15 +822,15 @@ jobs: node-version: [18.x] # python-version: [3.7] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -841,7 +841,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -851,7 +851,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} @@ -932,15 +932,15 @@ jobs: project-directory: ./my-volto-app steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -951,7 +951,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -961,7 +961,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} @@ -1069,15 +1069,15 @@ jobs: matrix: node-version: [18.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -1088,7 +1088,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -1098,7 +1098,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} @@ -1140,7 +1140,7 @@ jobs: - uses: actions/upload-artifact@v1 if: failure() with: - name: cypress-videos + name: cypress-videos-seamless path: packages/volto/cypress/videos multilingualseamless: @@ -1153,15 +1153,15 @@ jobs: matrix: node-version: [18.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -1172,7 +1172,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -1182,7 +1182,7 @@ jobs: - name: Cache Cypress Binary id: cache-cypress-binary - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/Cypress key: binary-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index c6a16acd47..04b8739752 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -12,7 +12,7 @@ jobs: towncrier: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # Fetch all history fetch-depth: '0' diff --git a/.github/workflows/code-analysis.yml b/.github/workflows/code-analysis.yml index cdba0307d1..a4dad86bde 100644 --- a/.github/workflows/code-analysis.yml +++ b/.github/workflows/code-analysis.yml @@ -10,15 +10,15 @@ jobs: name: Prettier runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ env.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -29,7 +29,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -47,15 +47,15 @@ jobs: name: ESlint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ env.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -66,7 +66,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -87,15 +87,15 @@ jobs: name: Stylelint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ env.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -106,7 +106,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -127,15 +127,15 @@ jobs: name: i18n runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ env.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -146,7 +146,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} diff --git a/.github/workflows/deployment_tests.yml b/.github/workflows/deployment_tests.yml new file mode 100644 index 0000000000..7324662881 --- /dev/null +++ b/.github/workflows/deployment_tests.yml @@ -0,0 +1,197 @@ +name: Deployment Tests +on: [push, pull_request] +jobs: + vitessr: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + name: Vite SSR + timeout-minutes: 5 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + # node setup + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - uses: pnpm/action-setup@v3 + name: Install pnpm + with: + version: 8 + # We don't want to install until later, + # when the cache and Cypress are in place + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build packages + run: pnpm build:deps && pnpm build:components + + - name: Start backend + run: make start-backend-docker-detached + + - name: Build + run: pnpm --filter plone-vite-ssr build + + - name: Start server + run: nohup pnpm --filter plone-vite-ssr start:prod & + + - name: Wait + run: packages/scripts/node_modules/.bin/wait-on --httpTimeout 20000 http-get://127.0.0.1:8080/Plone + + - name: Run tests + run: curl http://localhost:3000 || true + + - name: Run tests + run: curl http://127.0.0.1:3000 || true + + - name: Run tests + run: node packages/scripts/check_deployment.js + + - name: Stop backend + run: make stop-backend-docker-detached + + nextjs: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + name: Next.JS + timeout-minutes: 5 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + # node setup + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - uses: pnpm/action-setup@v3 + name: Install pnpm + with: + version: 8 + # We don't want to install until later, + # when the cache and Cypress are in place + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build packages + run: pnpm build:deps && pnpm build:components + + - name: Start backend + run: make start-backend-docker-detached + + - name: Build + run: pnpm --filter plone-nextjs build + + - name: Start server + run: nohup pnpm --filter plone-nextjs start:prod & + + - name: Wait + run: packages/scripts/node_modules/.bin/wait-on --httpTimeout 20000 http-get://127.0.0.1:8080/Plone + + - name: Run tests + run: curl http://localhost:3000 || true + + - name: Run tests + run: curl http://127.0.0.1:3000 || true + + - name: Run tests + run: node packages/scripts/check_deployment.js + + - name: Stop backend + run: make stop-backend-docker-detached + + remix: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + name: Remix + timeout-minutes: 5 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + # node setup + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - uses: pnpm/action-setup@v3 + name: Install pnpm + with: + version: 8 + # We don't want to install until later, + # when the cache and Cypress are in place + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build packages + run: pnpm build:deps && pnpm build:components + + - name: Start backend + run: make start-backend-docker-detached + + - name: Build + run: pnpm --filter plone-remix build + + - name: Start server + run: nohup pnpm --filter plone-remix start:prod & + + - name: Wait + run: packages/scripts/node_modules/.bin/wait-on --httpTimeout 20000 http-get://127.0.0.1:8080/Plone + + - name: Run tests + run: node packages/scripts/check_deployment.js + + - name: Stop backend + run: make stop-backend-docker-detached diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 08763d2f3a..987ac42c7c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: matrix: python-version: ['3.10'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 9f2ec8ec26..01c18ec7f9 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -14,15 +14,15 @@ jobs: matrix: node-version: [18.x, 20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -33,7 +33,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -64,15 +64,15 @@ jobs: matrix: node-version: [18.x, 20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -83,7 +83,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -105,15 +105,15 @@ jobs: matrix: node-version: [18.x, 20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # node setup - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -124,7 +124,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -141,14 +141,14 @@ jobs: name: '@plone/client' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ env.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.node-version }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v3 name: Install pnpm with: version: 8 @@ -159,7 +159,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} diff --git a/.husky/post-checkout b/.husky/post-checkout index a0df979ef1..8ec99a06f9 100755 --- a/.husky/post-checkout +++ b/.husky/post-checkout @@ -1,3 +1,2 @@ [ -n "$CI" ] && exit 0 -. "$(dirname -- "$0")/_/husky.sh" -pnpm yarnhook +pnpm lockhook diff --git a/.husky/post-merge b/.husky/post-merge index a0df979ef1..8ec99a06f9 100755 --- a/.husky/post-merge +++ b/.husky/post-merge @@ -1,3 +1,2 @@ [ -n "$CI" ] && exit 0 -. "$(dirname -- "$0")/_/husky.sh" -pnpm yarnhook +pnpm lockhook diff --git a/.husky/post-rebase b/.husky/post-rebase index a0df979ef1..8ec99a06f9 100755 --- a/.husky/post-rebase +++ b/.husky/post-rebase @@ -1,3 +1,2 @@ [ -n "$CI" ] && exit 0 -. "$(dirname -- "$0")/_/husky.sh" -pnpm yarnhook +pnpm lockhook diff --git a/.husky/pre-commit b/.husky/pre-commit index ca5e004fa3..17092dc583 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,2 @@ -#!/usr/bin/env sh [ -n "$CI" ] && exit 0 -. "$(dirname -- "$0")/_/husky.sh" - pnpm lint-staged diff --git a/Makefile b/Makefile index e3eb1c7db6..179130e6c6 100644 --- a/Makefile +++ b/Makefile @@ -189,6 +189,14 @@ copyreleasenotestodocs: start-backend-docker: docker run -it --rm --name=backend -p 8080:8080 -e SITE=Plone -e ADDONS='$(KGS)' $(DOCKER_IMAGE) +.PHONY: start-backend-docker-detached +start-backend-docker-detached: + docker run -d --rm --name=backend -p 8080:8080 -e SITE=Plone -e ADDONS='$(KGS)' $(DOCKER_IMAGE) + +.PHONY: stop-backend-docker-detached +stop-backend-docker-detached: + docker kill backend + .PHONY: start-backend-docker-no-cors start-backend-docker-no-cors: docker run -it --rm --name=backend -p 8080:8080 -e SITE=Plone -e ADDONS='$(KGS)' -e CORS_=true $(DOCKER_IMAGE) @@ -385,3 +393,7 @@ start-test-acceptance-server-5: ## Start Test Acceptance Server Main Fixture Plo .PHONY: start-test-acceptance-server-detached start-test-acceptance-server-detached: ## Start Test Acceptance Server Main Fixture (docker container) in a detached (daemon) mode docker run -d --name plone-client-acceptance-server -i --rm -p 55001:55001 $(DOCKER_IMAGE_ACCEPTANCE) + +.PHONY: stop-test-acceptance-server-detached +stop-test-acceptance-server-detached: ## Stop Test Acceptance Server Main Fixture (docker container) in a detached (daemon) mode + docker kill plone-client-acceptance-server diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 33b5b41b3c..6e29f5f31e 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -6,19 +6,17 @@ "dev": "next dev", "check-ts": "tsc --project tsconfig.json", "build": "next build", - "start": "next start", - "lint": "next lint", - "prettier": "prettier --check 'src/**/*.{js,jsx,ts,tsx}'", - "prettier:fix": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'" + "start:prod": "next start", + "lint": "next lint" }, "dependencies": { "@plone/client": "workspace: *", "@plone/components": "workspace: *", - "@tanstack/react-query": "^5.0.5", - "@tanstack/react-query-devtools": "^5.4.2", - "next": "14.0.4", + "@tanstack/react-query": "^5.24.6", + "@tanstack/react-query-devtools": "^5.24.6", + "next": "14.1.1", "react": "^18", - "react-aria-components": "^1.0.0-rc.0", + "react-aria-components": "^1.1.1", "react-dom": "^18" }, "devDependencies": { @@ -26,10 +24,8 @@ "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^8", - "eslint-config-next": "14.0.4", + "eslint-config-next": "14.1.1", "mrs-developer": "^2.1.1", - "prettier": "3.0.3", - "sass": "^1.69.1", "typescript": "5.2.2" } } diff --git a/apps/nextjs/src/app/content.tsx b/apps/nextjs/src/app/content.tsx index 3ae8de2a2c..c2c6ad2235 100644 --- a/apps/nextjs/src/app/content.tsx +++ b/apps/nextjs/src/app/content.tsx @@ -6,7 +6,7 @@ import Link from 'next/link'; import { flattenToAppURL } from './utils'; import { usePloneClient } from '@plone/client/provider'; import { Breadcrumbs } from '@plone/components'; -import '@plone/components/src/styles/main.scss'; +import '@plone/components/dist/basic.css'; export default function Content() { const { getContentQuery } = usePloneClient(); diff --git a/apps/plone/package.json b/apps/plone/package.json index b0c8d50d03..d104cc1a07 100644 --- a/apps/plone/package.json +++ b/apps/plone/package.json @@ -212,7 +212,7 @@ "redux": "4.1.0", "redux-actions": "2.6.5", "redux-connect": "10.0.0", - "redux-devtools-extension": "2.13.8", + "@redux-devtools/extension": "^3.3.0", "redux-localstorage-simple": "2.3.1", "redux-mock-store": "1.5.4", "redux-thunk": "2.3.0", @@ -257,14 +257,14 @@ "babel-plugin-react-intl": "5.1.17", "babel-plugin-root-import": "6.1.0", "css-loader": "5.2.7", - "eslint": "8.49.0", - "eslint-config-prettier": "9.0.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "9.1.0", "eslint-config-react-app": "7.0.1", "eslint-import-resolver-alias": "1.1.2", "eslint-import-resolver-babel-plugin-root-import": "1.1.1", - "eslint-plugin-import": "2.28.1", - "eslint-plugin-jsx-a11y": "6.7.1", - "eslint-plugin-prettier": "5.0.0", + "eslint-plugin-import": "2.29.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-prettier": "5.1.3", "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", "glob": "7.1.6", @@ -279,13 +279,13 @@ "postcss-loader": "7.0.2", "postcss-overrides": "3.1.4", "postcss-scss": "4.0.6", - "prettier": "3.0.3", + "prettier": "3.2.5", "razzle": "4.2.18", "start-server-and-test": "1.14.0", "style-loader": "3.3.1", - "stylelint": "15.10.3", - "stylelint-config-idiomatic-order": "9.0.0", - "stylelint-prettier": "4.0.2", + "stylelint": "^16.2.1", + "stylelint-config-idiomatic-order": "10.0.0", + "stylelint-prettier": "5.0.0", "svg-loader": "0.0.2", "svgo-loader": "3.0.3", "ts-jest": "^26.4.2", diff --git a/apps/remix/package.json b/apps/remix/package.json index 0b95c9eb28..2da2695bce 100644 --- a/apps/remix/package.json +++ b/apps/remix/package.json @@ -7,7 +7,7 @@ "build": "remix build", "dev": "remix dev --manual", "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", - "start": "remix-serve ./build/index.js", + "start:prod": "remix-serve ./build/index.js", "typecheck": "tsc" }, "dependencies": { @@ -16,8 +16,8 @@ "@remix-run/node": "^2.4.0", "@remix-run/react": "^2.4.0", "@remix-run/serve": "^2.4.0", - "@tanstack/react-query": "5.0.5", - "@tanstack/react-query-devtools": "*", + "@tanstack/react-query": "^5.24.6", + "@tanstack/react-query-devtools": "^5.24.6", "isbot": "^3.6.8", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -33,7 +33,7 @@ "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", - "typescript": "^5.1.6" + "typescript": "^5.2.2" }, "engines": { "node": ">=18.0.0" diff --git a/apps/vite-ssr/package.json b/apps/vite-ssr/package.json index 650967a5b7..1db33a17e2 100644 --- a/apps/vite-ssr/package.json +++ b/apps/vite-ssr/package.json @@ -8,7 +8,7 @@ "build": "npm run build:client && npm run build:server", "build:client": "vite build --outDir dist/client", "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server", - "serve": "NODE_ENV=production node server", + "start:prod": "NODE_ENV=production node server", "debug": "node --inspect-brk server" }, "dependencies": { diff --git a/apps/vite-ssr/server.js b/apps/vite-ssr/server.js index d073028c17..e5af2f9156 100644 --- a/apps/vite-ssr/server.js +++ b/apps/vite-ssr/server.js @@ -1,14 +1,18 @@ import fs from 'node:fs/promises'; import express from 'express'; import getPort, { portNumbers } from 'get-port'; +import dns from 'dns'; const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD; +// If DNS returns both ipv4 and ipv6 addresses, prefer ipv4 + export async function createServer( root = process.cwd(), isProd = process.env.NODE_ENV === 'production', hmrPort, ) { + dns.setDefaultResultOrder('ipv4first'); const app = express(); const prodIndexHtml = isProd @@ -93,8 +97,12 @@ export async function createServer( if (!isTest) { createServer().then(async ({ app }) => - app.listen(await getPort({ port: portNumbers(3000, 3100) }), () => { - console.log('Client Server: http://localhost:3000'); - }), + app.listen( + await getPort({ port: portNumbers(3000, 3100) }), + '0.0.0.0', + () => { + console.log('Client Server: http://localhost:3000'); + }, + ), ); } diff --git a/docs/source/conf.py b/docs/source/conf.py index b60d6eabac..6360e35dae 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -91,10 +91,11 @@ # Ignore github.com pages with anchors r"https://github.com/.*#.*", # Ignore other specific anchors - r"https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors#Identifying_the_issue", - r"https://docs.cypress.io/guides/references/migration-guide#Migrating-to-Cypress-version-10-0", r"https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi", # TODO retest with latest Sphinx when upgrading theme. chromewebstore recently changed its URL and has "too many redirects". r"https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd", # TODO retest with latest Sphinx when upgrading theme. chromewebstore recently changed its URL and has "too many redirects". + r"https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors#Identifying_the_issue", + r"https://docs.cypress.io/guides/references/migration-guide#Migrating-to-Cypress-version-10-0", + r"https://stackoverflow.com", # volto and documentation # TODO retest with latest Sphinx. ] linkcheck_anchors = True linkcheck_timeout = 10 diff --git a/docs/source/configuration/index.md b/docs/source/configuration/index.md index fde6916ff3..3375c41f9c 100644 --- a/docs/source/configuration/index.md +++ b/docs/source/configuration/index.md @@ -26,4 +26,5 @@ workingcopy environmentvariables expanders locking +slots ``` diff --git a/docs/source/configuration/slots.md b/docs/source/configuration/slots.md new file mode 100644 index 0000000000..6c2c9701e5 --- /dev/null +++ b/docs/source/configuration/slots.md @@ -0,0 +1,412 @@ +--- +myst: + html_meta: + "description": "Slots are insertion points in the Volto rendering tree structure." + "property=og:description": "Slots are insertion points in the Volto rendering tree structure." + "property=og:title": "Slots" + "keywords": "Volto, Plone, frontend, React, configuration, slots, viewlets" +--- + +# Slots + +Slots provide a way for Volto add-ons to insert their own components at predefined locations in the rendered page. + +```{note} +This concept is inspired by the Plone Classic UI {doc}`plone:classic-ui/viewlets`. +``` + + +## Anatomy + +Slots have a name, and they contain a list of named slot components. + +Volto renders slots using the `SlotRenderer` component. +You can add slot insertion points in your code, as shown in the following example. + +```ts + +``` + +Slot components are registered in the {ref}`configuration registry using a specific API for slots `. + +The rendering of a slot component is controlled by the presence or absence of a list of conditions called {term}`predicates`. + +You can register multiple slot components with the same name under the same slot, as long as they have different predicates or components. + +To illustrate how slots are structured and work, let's register a slot component, where the component is `PageHeader`, and the predicate matches a route that begins with `/de/about`. + +```ts +config.registerSlotComponent({ + slot: 'aboveContent', + name: 'header', + component: PageHeader, + predicates: [RouteCondition('/de/about')], +}); +``` + +The following tree structure diagram illustrates the resultant registration. + +```text +Slot (`name`=`aboveContent`) +└── SlotComponent + ├── `slot`=`aboveContent` + ├── `name`=`header` + ├── `component`=PageHeader + └── predicate of "only appear under `/de/about`" +``` + +Next, let's register another slot component in the same slot, with the same name and component, but with a different predicate where the content type matches either `Document` or `News Item`. + +```ts +config.registerSlotComponent({ + slot: 'aboveContent', + name: 'header', + component: PageHeader, + predicates: [ContentTypeCondition(['Document', 'News Item'])], +}); +``` + +The following tree structure diagram illustrates the result of the second registration. + +```text +Slot (`name`=`aboveContent`) +├── SlotComponent +│ ├── `slot`=`aboveContent` +│ ├── `name`=`header` +│ ├── `component`=PageHeader +│ └── predicate of "only appear under `/de/about`" +└── SlotComponent + ├── `slot`=`aboveContent` + ├── `name`=`header` + ├── `component`=PageHeader + └── predicate of "only appear when the content type is either a Document or News Item" +``` + +Finally, let's register another slot component in the same slot, with the same name, but with a different component and without a predicate. + +```ts +config.registerSlotComponent({ + slot: 'aboveContent', + name: 'header', + component: 'DefaultHeader', +}); +``` + +The following tree structure diagram illustrates the result of the third registration. + +```text +Slot (`name`=`aboveContent`) +├── SlotComponent +│ ├── `slot`=`aboveContent` +│ ├── `name`=`header` +│ ├── `component`=PageHeader +│ └── predicate of "only appear under `/de/about`" +├── SlotComponent +│ ├── `slot`=`aboveContent` +│ ├── `name`=`header` +│ ├── `component`=PageHeader +│ └── predicate of "only appear when the content type is either a Document or News Item" +└── SlotComponent + ├── `slot`=`aboveContent` + ├── `name`=`header` + └── `component`=`DefaultHeader` +``` + +The rendering of slot components follows an algorithm: + +- The last registered slot component is evaluated first. +- The first evaluated slot component's predicates to return `true` has its component rendered in the slot. +- A slot component without predicates becomes the fallback for other slot components with predicates. + +Working through the above diagram from bottom to top, let's assume a visitor goes to the route `/de/about` and views an Event content type. + +1. The algorithm looks for the third slot component's predicates. + Because it has no predicates to be evaluated, and therefore cannot return `true`, its component is a fallback to other slot components. + +2. Moving upward, the second slot component's predicates are evaluated. + If they are `true`, then its component is rendered in the slot, and evaluation stops. + But in this case, the content type is an Event, thus it returns `false`, and evaluation continues upward. + +3. The first slot component's predicates are evaluated. + In this case, they are true because the visitor is on the route `/de/about`. + Evaluation stops, and its component is rendered in the slot. + +Within a slot, slot components are grouped by their name. +The order in which the grouped slot components are evaluated is governed by the order in which they are registered. + +Extending our previous example, let's register another slot component with a different name. + +```ts +config.registerSlotComponent({ + slot: 'aboveContent', + name: 'subheader', + component: PageSubHeader, + predicates: [ContentTypeCondition(['Document', 'News Item'])], +}); +``` + +Thus the order of evaluation of the named slot components would be `header`, `subheader`. +As each group of slot components is evaluated, their predicates will determine what is rendered in their position. + +You can change the order of the named slot components for a different slot using the {ref}`slots-reorderSlotComponent-label` API. +In our example, you can reorder the `subheading` before the `heading`, although it would probably look strange. + +```ts +config.reorderSlotComponent({ + slot: 'aboveContent', + name: 'subheader', + action: 'before', + target: 'header', +}); +``` + +You can even delete the rendering of a registered slot component using the {ref}`slots-unregisterSlotComponent-label` API. + + +## Default slots + +Volto comes with the following default slots. + +- `aboveContent` +- `belowContent` + + +(configuration-registry-for-slot-components)= + +## Configuration registry for slot components + +You can manage slot components using the configuration registry for slot components and its API. + + +### `registerSlotComponent` + +`registerSlotComponent` registers a slot component as shown. + +```ts +config.registerSlotComponent({ + slot: 'aboveContent', + name: 'header', + component: PageHeader, + predicates: [ + RouteCondition('/de/about'), + ContentTypeCondition(['Document', 'News Item']) + ], +}); +``` + +A slot component must have the following parameters. + +`slot` +: The name of the slot, where the slot components are stored. + +`name` +: The name of the slot component that we are registering. + +`component` +: The component that we want to render in the slot. + +`predicates` +: A list of functions that return a function with this signature. + + ```ts + export type SlotPredicate = (args: any) => boolean; + ``` + + +#### Predicate helpers + +There are two predicate helpers available in the Volto helpers. +You can also create custom predicate helpers. + + +##### `RouteCondition` + +```ts +export function RouteCondition(path: string, exact?: boolean) { + return ({ pathname }: { pathname: string }) => + Boolean(matchPath(pathname, { path, exact })); +} +``` + +The `RouteCondition` predicate helper renders a slot if the specified route matches. +It accepts the following parameters. + +`path` +: String. + Required. + The route. + +`exact` +: Boolean. + Optional. + If `true`, then the match will be exact, else matches "begins with", for the given string from `path`. + + +##### `ContentTypeCondition` + +```ts +export function ContentTypeCondition(contentType: string[]) { + return ({ content }: { content: Content }) => + contentType.includes(content['@type']); +} +``` + +The `ContentTypeCondition` helper predicate allows you to render a slot when the given content type matches the current content type. +It accepts a list of possible content types. + + +##### Custom predicates + +You can create your own predicate helpers to determine whether your slot component should render. +The `SlotRenderer` will pass down the current `content` and the `pathname` into your custom predicate helper. +You can also tailor your own `SlotRenderer`s, or shadow the original `SlotRenderer`, to satisfy your requirements. + + +(slots-getSlot-label)= + +### `getSlot` + +`getSlot` returns the components to be rendered for the given named slot. +You should use this method while building you own slot renderer or customizing the existing `SlotRenderer`. +You can use the implementation of `SlotRenderer` as a template. +This is the signature: + +```ts +config.getSlot(name: string, args: GetSlotArgs): GetSlotReturn +``` + +It has the following parameters. + +`name` +: String. + Required. + The name of the slot we want to render. + +`args` +: Object. + Required. + An object containing the arguments to pass to the predicates. + + +(slots-getSlotComponents-label)= + +### `getSlotComponents` + +`getSlotComponents` returns the list of named slot components registered in a given slot. +This is useful to debug what is registered and in what order, informing you whether you need to change their order. +This is the signature: + +```ts +config.getSlotComponents(slot: string): string[] +``` + +`slot` +: String. + Required. + The name of the slot where the slot components are stored. + + +(slots-getSlotComponent-label)= + +### `getSlotComponent` + +`getSlotComponent` returns the registered named component's data for the given slot component name. +This is the signature: + +```ts +config.getSlotComponent(slot: string, name: string): SlotComponent[] +``` + +`slot` +: String. + Required. + The name of the slot where the slot components are stored. + +`name` +: String. + Required. + The name of the slot component to retrieve. + + +(slots-reorderSlotComponent-label)= + +### `reorderSlotComponent` + +`reorderSlotComponent` reorders the list of named slot components registered per slot. + +Given a `slot` and the `name` of a slot component, you must either specify the desired `position` or perform an `action` to reposition the slot component in the given slot, but not both. + +The available actions are `"after"`, `"before"`, `"first"`, and `"last"`. +`"first"` and `"last"` do not accept a `target`. + +This is the signature: + +```ts +config.reorderSlotComponent({ slot, name, position, action, target }: { + slot: string; + name: string; + position?: number | undefined; + action?: "after" | "before" | "first" | "last" | undefined; + target?: string | undefined; +}): void +``` + +`slot` +: String. + Required. + The name of the slot where the slot components are stored. + +`name` +: String. + Required. + The name of the slot component to reposition in the list of slot components. + +`position` +: Number. + Exactly one of `position` or `action` is required. + The destination position in the registered list of slot components. + The position is zero-indexed. + +`action` +: Enum: `"after"` | `"before"` | `"first"` | `"last"` | undefined. + Exactly one of `position` or `action` is required. + The action to perform on `name`. + + When using either the `"after"` or `"before"` values, a `target` is required. + The slot component will be repositioned relative to the `target`. + + When using either the `"first"` and `"last"` values, a `target` must not be used. + The slot component will be repositioned to either the first or last position. + +`target` +: String. + Required when `action` is either `"after"` or `"before"`, else must not be provided. + The name of the slot component targeted for the given `action`. + + +(slots-unregisterSlotComponent-label)= + +### `unregisterSlotComponent` + +`unregisterSlotComponent` removes a registration for a named slot component, given its registration position. +This is the signature: + +```ts +config.unRegisterSlotComponent(slot: string, name: string, position: number): void +``` + +`slot` +: String. + Required. + The name of the slot that contains the slot component to unregister. + +`name` +: String. + Required. + The name of the slot component to unregister inside the component. + +`position` +: Number. + Required. + The component position to remove in the slot component registration. + Use {ref}`slots-getSlotComponent-label` to find the zero-indexed position of the registered component to remove. diff --git a/docs/source/getting-started/roadmap.md b/docs/source/getting-started/roadmap.md index cba274e63b..244b11159c 100644 --- a/docs/source/getting-started/roadmap.md +++ b/docs/source/getting-started/roadmap.md @@ -89,3 +89,16 @@ Not really advanced but perhaps less common, here's some stuff you can do: - Customize Volto's Webpack configuration or project loader using Razzle - Write a Redux middleware - Start hacking on Volto. + +## Add-on first approach + +Developing for Plone's frontend means to add code to a Volto project. +The frontend files of the project are created in the `frontend` folder. +The generator also creates a default add-on in the `frontend/src/addons` folder. +The project is already configured to use this add-on. +Add your code and customizations to this add-on, and Volto will load them on start up or a restart. +This add-on is configured as a theme add-on, so you are able to customize the look and feel of your site as well. + +```{seealso} +For more information about how to develop a Volto project as an add-on, see {doc}`training:voltoaddons/index`. +``` diff --git a/docs/source/recipes/developing-a-project.md b/docs/source/recipes/developing-a-project.md deleted file mode 100644 index c015c9b3f8..0000000000 --- a/docs/source/recipes/developing-a-project.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -myst: - html_meta: - "description": "Developing a Volto project" - "property=og:description": "Developing a Volto project" - "property=og:title": "AppExtras component" - "keywords": "Volto, Plone, frontend, React, app extra component" ---- - -(frontend-developing-a-project-label)= - -# Developing a project - -When you start developing a Plone project, it is recommended that you use Plone's cookiecutter boilerplate project generator. -See {doc}`plone:install/create-project` for more information. - -The generator will output the project folder structure. -It is organized in three folders named `frontend`, `backend`, and `devops`, each of which corresponds to its primary use. -It also contains the convenience `Makefile` commands to perform all the usual actions while developing. - -## Add-on first approach - -Developing for Plone's frontend means to add code to a Volto project. -The frontend files of the project are created in the `frontend` folder. -The generator also creates a default add-on in the `frontend/src/addons` folder. -The project is already configured to use this add-on. -Add your code and customizations to this add-on, and Volto will load them on start up or a restart. -This add-on is configured as a theme add-on, so you are able to customize the look and feel of your site as well. - -```{seealso} -For more information about how to develop a Volto project as an add-on, see {doc}`training:voltoaddons/index`. -``` diff --git a/docs/source/recipes/index.md b/docs/source/recipes/index.md index d75154ba39..97133ec6dd 100644 --- a/docs/source/recipes/index.md +++ b/docs/source/recipes/index.md @@ -13,7 +13,6 @@ myst: :maxdepth: 1 creating-project -developing-a-project folder-structure environment-variables customizing-components diff --git a/docs/source/release-notes/index.md b/docs/source/release-notes/index.md index b148fe9e5c..1273a7286f 100644 --- a/docs/source/release-notes/index.md +++ b/docs/source/release-notes/index.md @@ -17,6 +17,50 @@ myst: +## 18.0.0-alpha.16 (2024-03-02) + +### Internal + +- Update dependencies + Fix prettier due to new version @sneridagh [#5815](https://github.com/plone/volto/issues/5815) + +### Documentation + +- Linkcheck thinks `README.md` is `http://README.md`. Bad linkcheck, no more 🍺 for you. @stevepiercy [#5816](https://github.com/plone/volto/issues/5816) + +## 18.0.0-alpha.15 (2024-03-01) + +### Breaking + +- Upgrade Volto core to use React 18.2.0 @sneridagh [#3221](https://github.com/plone/volto/issues/3221) + +## 18.0.0-alpha.14 (2024-03-01) + +### Breaking + +- Improved accessibility of logo component. @Molochem [#5776](https://github.com/plone/volto/issues/5776) + +### Feature + +- Support for slots @sneridagh [#5775](https://github.com/plone/volto/issues/5775) + +### Bugfix + +- Fixed toolbar menus not closing when clicking again on the toolbar buttons that show menus. @ichim-david + Add focus-visible rule to toolbar buttons so that it's visible to the user what button is focused when using tab navigation @ichim-david [#5645](https://github.com/plone/volto/issues/5645) +- Enhance findBlocks to check for blocks also in data for add-ons such as @eeacms/volto-tabs-block. @ichim-david [#5796](https://github.com/plone/volto/issues/5796) +- Fixed ArrayWidget sorting items. @giuliaghisini [#5805](https://github.com/plone/volto/issues/5805) + +### Internal + +- New types declarations with @types/react@18 - make tsc happy @sneridagh [#5814](https://github.com/plone/volto/issues/5814) + +### Documentation + +- Added Release Management Notes. @sneridagh @stevepiercy [#5358](https://github.com/plone/volto/issues/5358) +- Delete redundant `developing-a-project.md`. @stevepiercy [#5675](https://github.com/plone/volto/issues/5675) +- Removed Memori and TwinCreator websites from README.md no longer made using Volto and giving 404 error. @ichim-david [#5802](https://github.com/plone/volto/issues/5802) + ## 18.0.0-alpha.13 (2024-02-22) ### Bugfix diff --git a/docs/source/upgrade-guide/index.md b/docs/source/upgrade-guide/index.md index a89577132b..06427363f5 100644 --- a/docs/source/upgrade-guide/index.md +++ b/docs/source/upgrade-guide/index.md @@ -28,6 +28,27 @@ Thus it is safe to run it on top of your project and answer the prompts. ## Upgrading to Volto 18.x.x +### Volto runs now on React 18.2.0 + +We have updated Volto to use React 18. +This has been the latest published stable version since June 2022. +This aligns Volto with the latests developments in the React ecosystem and opens the door to up to date software and React features, like client side `Suspense` and others: + +- Concurrent rendering in client (Suspense) +- Automatic batching updates +- Transitions +- New hooks `useId`, `useTransition`, `useDeferredValue`, `useSyncExternalStore`, and other hooks + +### `react-portal` dependency removed + +`react-portal` is deprecated and it was removed from Volto. +The Volto code that relied on it was mainly CMS UI components. +If your project relies on it, either in your code or the shadowed components you may have, you should update to use the standard React API, `createPortal`. +You can update your shadows taking the modified components as templates. +As a last resort, you can install `react-portal` as a dependency of your project. +However, this is discouraged, because the React 18 rendering could have unexpected side effects. +It is recommended that you use the React API instead. + ### ESlint project configuration update `@plone/registry` and [other packages on which Volto depends](https://github.com/plone/volto/tree/main/packages) are now stand-alone releases in the monorepo structure released in 18.0.0-alpha.4. diff --git a/package.json b/package.json index c5b5b04a23..76ffe12480 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,10 @@ "scripts": { "preinstall": "npx only-allow pnpm", "postinstall": "make setup", - "watch": "turbo run watch", - "build:deps": "turbo run build --filter @plone/registry --filter @plone/client", + "watch": "pnpm --filter @plone/registry watch", + "build:deps": "pnpm --filter @plone/registry --filter @plone/client build", "build:registry": "pnpm --filter @plone/registry run build", + "build:components": "pnpm --filter @plone/components run build", "build": "pnpm build:deps && pnpm --filter @plone/volto build", "start": "pnpm build:deps && pnpm --filter @plone/volto start", "start:project": "pnpm --filter plone run start", @@ -17,34 +18,36 @@ "i18n:ci": "pnpm --filter @plone/volto i18n:ci", "prettier": "prettier --check '{apps,packages}/**/*.{js,jsx,ts,tsx}'", "prettier:fix": "prettier --write '{apps,packages}/**/*.{js,jsx,ts,tsx}' ", + "stylelint": "stylelint '{apps,packages}/**/*.{css,scss,less}'", + "stylelint:fix": "stylelint '{packages}/**/*.{css,scss,less}' --fix", "format": "prettier --write \"**/*.{ts,tsx,md}\"", "lint-staged": "lint-staged", - "prepare": "husky install", + "lockhook": "node packages/scripts/lockhook.js", + "prepare": "husky", "husky:uninstall": "husky uninstall", "prereleaser": "node packages/scripts/preleaser.js" }, "devDependencies": { - "@parcel/packager-ts": "2.11.0", - "@parcel/transformer-typescript-types": "2.11.0", + "@parcel/packager-ts": "^2.12.0", + "@parcel/transformer-typescript-types": "^2.12.0", "@typescript-eslint/eslint-plugin": "^6.8.0", "@typescript-eslint/parser": "^6.8.0", - "eslint": "^8.53.0", - "eslint-config-prettier": "9.0.0", + "concurrently": "^8.2.2", + "eslint": "^8.57.0", + "eslint-config-prettier": "9.1.0", "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "2.28.1", - "eslint-plugin-prettier": "5.0.0", + "eslint-plugin-import": "2.29.1", + "eslint-plugin-prettier": "5.1.3", "eslint-plugin-react": "7.33.2", - "husky": "^8.0.3", - "lint-staged": "15.0.2", - "prettier": "3.0.3", - "stylelint": "^15.11.0", + "husky": "9.0.11", + "lint-staged": "15.2.2", + "prettier": "3.2.5", + "stylelint": "^16.2.1", "tsconfig": "workspace:*", - "turbo": "latest", "typescript": "5.2.2", - "vitest": "^0.34.6", - "yarnhook": "0.6.1" + "vitest": "^1.3.1" }, - "packageManager": "pnpm@8.9.0", + "packageManager": "pnpm@8.15.4", "pnpm": { "overrides": { "webpack": "5.90.1" diff --git a/packages/blocks/package.json b/packages/blocks/package.json index d09d8c474d..b098e5c688 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -54,13 +54,13 @@ }, "dependencies": {}, "devDependencies": { - "@parcel/packager-ts": "2.10.2", - "@parcel/transformer-typescript-types": "2.10.2", + "@parcel/packager-ts": "^2.12.0", + "@parcel/transformer-typescript-types": "^2.12.0", "@plone/registry": "workspace:*", "@plone/types": "workspace:*", "@types/react": "^18", "@types/react-dom": "^18", - "parcel": "2.10.2", + "parcel": "^2.12.0", "release-it": "17.1.1", "tsconfig": "workspace:*", "typescript": "5.2.2", diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index d954167003..112e69e9c8 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -8,6 +8,16 @@ +## 1.0.0-alpha.13 (2024-03-02) + +### Internal + +- Update dependencies @sneridagh [#5815](https://github.com/plone/volto/pull/5815) + +### Documentation + +- Reorganize `README.md`, merging content into authoritative locations. Add `awesome_bot` to check links in all READMEs. @stevepiercy [#5437](https://github.com/plone/volto/pull/5437) + ## 1.0.0-alpha.12 (2024-01-02) ### Internal diff --git a/packages/client/news/5437.documentation b/packages/client/news/5437.documentation deleted file mode 100644 index 377e651c6b..0000000000 --- a/packages/client/news/5437.documentation +++ /dev/null @@ -1 +0,0 @@ -Reorganize `README.md`, merging content into authoritative locations. Add `awesome_bot` to check links in all READMEs. @stevepiercy diff --git a/packages/client/news/5824.internal b/packages/client/news/5824.internal new file mode 100644 index 0000000000..c17d9fa550 --- /dev/null +++ b/packages/client/news/5824.internal @@ -0,0 +1 @@ +Upgrade TSQ to latest @sneridagh diff --git a/packages/client/package.json b/packages/client/package.json index 5f8a09b0f2..7bec23b8e8 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -8,7 +8,7 @@ } ], "license": "MIT", - "version": "1.0.0-alpha.12", + "version": "1.0.0-alpha.13", "repository": { "type": "git", "url": "git@github.com:plone/volto.git" @@ -61,10 +61,6 @@ "vitest": "vitest", "check-ts": "tsc --project tsconfig.json", "coverage": "vitest run --coverage --no-threads", - "lint": "eslint 'src/**/*.{js,ts,tsx}' --quiet", - "lint:fix": "eslint 'src/**/*.{js,ts,tsx}' --quiet --fix", - "prettier": "prettier --check 'src/**/*.{js,jsx,ts,tsx}'", - "prettier:fix": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'", "dry-release": "release-it --dry-run", "release": "release-it", "release-major-alpha": "release-it major --preRelease=alpha", @@ -82,26 +78,26 @@ "@types/react-dom": "18.2.12", "@types/uuid": "^9.0.2", "@vitejs/plugin-react": "^4.1.0", - "@vitest/coverage-c8": "0.28.5", + "@vitest/coverage-v8": "^1.3.1", "glob": "7.1.6", "jsdom": "^21.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "release-it": "16.2.1", - "tsup": "^8.0.1", + "release-it": "17.1.1", + "tsup": "^8.0.2", "typescript": "5.2.2", - "uuid": "^9.0.0", - "vite": "^4.5.1", - "vite-plugin-dts": "^3.6.0", - "vitest": "^0.34.6", - "wait-on": "^7.0.1" + "uuid": "^9.0.1", + "vite": "^5.1.4", + "vite-plugin-dts": "^3.7.3", + "vitest": "^1.3.1", + "wait-on": "^7.2.0" }, "dependencies": { - "@tanstack/react-query": "5.0.5", - "axios": "^1.5.1", - "debug": "4.3.2", - "query-string": "^8.1.0", - "zod": "^3.21.4" + "@tanstack/react-query": "5.24.1", + "axios": "^1.6.7", + "debug": "4.3.4", + "query-string": "^9.0.0", + "zod": "^3.22.4" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", diff --git a/packages/client/testRunner.js b/packages/client/testRunner.js index 86fc6367dc..d3d9870b9c 100644 --- a/packages/client/testRunner.js +++ b/packages/client/testRunner.js @@ -46,7 +46,7 @@ directories.forEach((dir) => { // if there are any matches, run vitest on this directory if (matches.length > 0) { console.log(`Running vitest on src/${dir}`); - execSync(`pnpm vitest run src/restapi/${dir} --no-threads`, { + execSync(`pnpm vitest run src/restapi/${dir}`, { stdio: 'inherit', }); } diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts index 9676ff4d51..9c0f8405d4 100644 --- a/packages/client/vite.config.ts +++ b/packages/client/vite.config.ts @@ -23,6 +23,7 @@ export default defineConfig({ '@tanstack/react-query': 'reactQuery', axios: 'axios', zod: 'zod', + 'query-string': 'queryString', }, }, }, diff --git a/packages/client/vitest.config.ts b/packages/client/vitest.config.ts index dc525dfa51..02c3465eae 100644 --- a/packages/client/vitest.config.ts +++ b/packages/client/vitest.config.ts @@ -5,20 +5,25 @@ export default defineConfig({ globals: true, environment: 'jsdom', setupFiles: './setupTesting.ts', + poolOptions: { + threads: { + singleThread: true, + }, + }, // you might want to disable it, if you don't have tests that rely on CSS // since parsing CSS is slow css: true, environmentOptions: { jsdom: { /* - We need to set the url parameter below to the url of the API server + We need to set the url parameter below to the url of the API server to avoid CORS issue. - - This is because JSDom environment sets the - origin header (axios cannot control it on its own, just like in a - browser) automatically to the url specified here. - - If this url is not provided then vitest sets the default value as + + This is because JSDom environment sets the + origin header (axios cannot control it on its own, just like in a + browser) automatically to the url specified here. + + If this url is not provided then vitest sets the default value as localhost:3000 which causes CORS error with the test server. */ url: 'http://localhost:55001', diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 5106923826..9c696a5e92 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -8,6 +8,28 @@ +## 2.0.0-alpha.4 (2024-03-02) + +### Bugfix + +- Remove `lodash` dependency. + Several packages upgrades and general cleanup. @sneridagh [#5822](https://github.com/plone/volto/issues/5822) +- Proxy the `PopoverContext` inside the `Select` component, to be able to override it from the outside @sneridagh [#5823](https://github.com/plone/volto/issues/5823) + +### Internal + +- Update dependencies @sneridagh [#5815](https://github.com/plone/volto/issues/5815) + +## 2.0.0-alpha.3 (2024-03-01) + +### Breaking + +- Use `pathname` instead of the full location as prop in `BlocksRenderer` @sneridagh [#5798](https://github.com/plone/volto/issues/5798) + +### Bugfix + +- Fix `lodash` imports for bundling @sneridagh [#5798](https://github.com/plone/volto/issues/5798) + ## 2.0.0-alpha.2 (2024-02-23) ### Bugfix diff --git a/packages/components/README.md b/packages/components/README.md index 7f0fc45617..0731a8e49f 100644 --- a/packages/components/README.md +++ b/packages/components/README.md @@ -45,7 +45,7 @@ You can use the CSS bundled for all components in a single file, or use the spec They are distributed along with the components code in the `dist` folder of the library. ```js -import '@plone/components/basic.css'; +import '@plone/components/dist/basic.css'; ``` or selectively: @@ -75,8 +75,8 @@ You could take Quanta as example to build your own layer of styles over basic st To use a theme built upon the basic styling, you need to import both the basic and the theme CSS, in this order: ```js -import '@plone/components/basic.css'; -import '@plone/components/quanta.css'; +import '@plone/components/dist/basic.css'; +import '@plone/components/dist/quanta.css'; ``` You have the option of doing it selectively per component, too: diff --git a/packages/components/news/5798.breaking b/packages/components/news/5798.breaking deleted file mode 100644 index 263432a2c2..0000000000 --- a/packages/components/news/5798.breaking +++ /dev/null @@ -1 +0,0 @@ -Use `pathname` instead of the full location as prop in `BlocksRenderer` @sneridagh diff --git a/packages/components/news/5798.bugfix b/packages/components/news/5798.bugfix deleted file mode 100644 index d6e35d6924..0000000000 --- a/packages/components/news/5798.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `lodash` imports for bundling @sneridagh diff --git a/packages/components/news/5824.bugfix b/packages/components/news/5824.bugfix new file mode 100644 index 0000000000..c1568b430f --- /dev/null +++ b/packages/components/news/5824.bugfix @@ -0,0 +1 @@ +Fix some case inconsistencies in CSS declarations @sneridagh diff --git a/packages/components/package.json b/packages/components/package.json index 3a0bd73e9a..670db5334e 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -8,7 +8,7 @@ } ], "license": "MIT", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "repository": { "type": "git", "url": "http://github.com/plone/components.git" @@ -45,13 +45,12 @@ "quanta" ], "scripts": { - "build": "parcel build --no-cache && pnpm build:srctypes && pnpm build:css", + "build": "parcel build --no-cache && pnpm build:css", "build:css": "pnpm build:basic && pnpm build:quanta", "build:basic": "lightningcss --browserslist --bundle --sourcemap src/styles/basic/main.css -o basic.css && mv basic.css* dist/.", "build:quanta": "lightningcss --browserslist --bundle --sourcemap src/styles/quanta/main.css -o quanta.css && mv quanta.css* dist/.", - "build:srctypes": "mkdir -p dist/src && cp dist/index.d.ts dist/src/index.d.ts", "check-ts": "tsc --project tsconfig.json", - "test": "vitest --no-threads --passWithNoTests", + "test": "vitest --passWithNoTests", "coverage": "vitest run --coverage --no-threads", "lint": "pnpm eslint && pnpm prettier && pnpm stylelint", "format": "pnpm eslint:fix && pnpm prettier:fix && pnpm stylelint:fix", @@ -77,62 +76,57 @@ "not dead" ], "devDependencies": { - "@parcel/config-default": "2.11.0", - "@parcel/core": "^2.11.0", - "@parcel/packager-ts": "^2.11.0", - "@parcel/transformer-js": "^2.11.0", - "@parcel/transformer-react-refresh-wrap": "^2.11.0", - "@parcel/transformer-typescript-types": "^2.11.0", + "@parcel/config-default": "^2.12.0", + "@parcel/core": "^2.12.0", + "@parcel/packager-ts": "^2.12.0", + "@parcel/transformer-js": "^2.12.0", + "@parcel/transformer-react-refresh-wrap": "^2.12.0", + "@parcel/transformer-typescript-types": "^2.12.0", "@plone/types": "workspace: *", "@react-types/shared": "^3.22.0", - "@storybook/addon-essentials": "^7.5.1", - "@storybook/addon-interactions": "^7.5.1", - "@storybook/addon-links": "^7.5.1", - "@storybook/addon-mdx-gfm": "^7.5.1", - "@storybook/blocks": "^7.5.1", + "@storybook/addon-essentials": "^7.6.17", + "@storybook/addon-interactions": "^7.6.17", + "@storybook/addon-links": "^7.6.17", + "@storybook/blocks": "^7.6.17", "@storybook/manager-api": "^7.6.17", - "@storybook/react": "^7.5.1", - "@storybook/react-vite": "^7.5.1", + "@storybook/react": "^7.6.17", + "@storybook/react-vite": "^7.6.17", "@storybook/testing-library": "^0.2.2", "@storybook/theming": "^7.6.17", - "@testing-library/jest-dom": "6.1.4", - "@testing-library/react": "14.0.0", + "@testing-library/jest-dom": "6.4.2", + "@testing-library/react": "14.2.1", "@types/jest-axe": "^3.5.7", - "@types/lodash": "^4.14.201", "@types/react": "^18", "@types/react-dom": "^18", "@typescript-eslint/eslint-plugin": "^6.8.0", "@typescript-eslint/parser": "^6.8.0", "@vitejs/plugin-react": "^4.1.0", - "@vitest/coverage-c8": "0.33.0", + "@vitest/coverage-v8": "^1.3.1", "browserslist": "^4.23.0", - "eslint": "^8.53.0", - "eslint-plugin-storybook": "^0.6.15", - "globby": "^14.0.0", + "eslint": "^8.57.0", + "eslint-plugin-storybook": "^0.8.0", "jest-axe": "^8.0.0", "jsdom": "^22.1.0", - "lightningcss": "^1.23.0", - "lightningcss-cli": "^1.23.0", - "parcel": "^2.11.0", + "lightningcss": "^1.24.0", + "lightningcss-cli": "^1.24.0", + "parcel": "^2.12.0", "parcel-optimizer-react-client": "workspace:^", - "prettier": "3.0.3", - "release-it": "16.2.1", - "storybook": "^7.5.1", - "stylelint": "15.11.0", - "stylelint-config-idiomatic-order": "9.0.0", + "prettier": "3.2.5", + "release-it": "17.1.1", + "storybook": "^7.6.17", + "stylelint": "16.2.1", + "stylelint-config-idiomatic-order": "10.0.0", "stylelint-config-prettier": "9.0.5", - "stylelint-prettier": "4.0.2", + "stylelint-prettier": "5.0.0", "typescript": "5.2.2", - "vite": "^4.5.0", - "vitest": "^0.34.6", + "vite": "^5.1.4", + "vitest": "^1.3.1", "vitest-axe": "^0.1.0" }, "dependencies": { "@react-aria/utils": "^3.22.0", "@react-spectrum/utils": "^3.11.1", - "classnames": "^2.3.2", "clsx": "^2.0.0", - "lodash": "^4.17.21", "react-aria-components": "^1.1.1" }, "peerDependencies": { diff --git a/packages/components/src/components/Container/Container.tsx b/packages/components/src/components/Container/Container.tsx index 622c97d052..1f157792ed 100644 --- a/packages/components/src/components/Container/Container.tsx +++ b/packages/components/src/components/Container/Container.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react'; import { getElementType } from '../helpers'; -import cx from 'classnames'; +import cx from 'clsx'; type ContainerProps = { /** Primary content. */ diff --git a/packages/components/src/components/Select/Select.tsx b/packages/components/src/components/Select/Select.tsx index cbe5c31c42..fdbe84c784 100644 --- a/packages/components/src/components/Select/Select.tsx +++ b/packages/components/src/components/Select/Select.tsx @@ -7,10 +7,12 @@ import { ListBoxItem, ListBoxItemProps, Popover, + PopoverContext, Select as RACSelect, SelectProps as RACSelectProps, SelectValue, Text, + useContextProps, ValidationResult, } from 'react-aria-components'; @@ -49,6 +51,9 @@ export function Select({ items, ...props }: SelectProps) { + // In case that we want to customize the Popover, we proxy the PopoverContext props down + const [popoverProps] = useContextProps({}, null, PopoverContext); + return ( {({ isOpen }) => ( @@ -63,7 +68,7 @@ export function Select({ {description && {description}} {errorMessage} - + {children} diff --git a/packages/components/src/components/quanta/Select/Select.tsx b/packages/components/src/components/quanta/Select/Select.tsx index 664121627c..6e43494084 100644 --- a/packages/components/src/components/quanta/Select/Select.tsx +++ b/packages/components/src/components/quanta/Select/Select.tsx @@ -1,12 +1,14 @@ import React from 'react'; -import { TextFieldContext } from 'react-aria-components'; +import { SelectContext, PopoverContext } from 'react-aria-components'; import { Select, SelectItem, SelectProps } from '../../Select/Select'; export function QuantaSelect(props: SelectProps) { return ( - - + + ); } diff --git a/packages/components/src/helpers/blocks.ts b/packages/components/src/helpers/blocks.ts index 73accdfbf9..34a10f4ba2 100644 --- a/packages/components/src/helpers/blocks.ts +++ b/packages/components/src/helpers/blocks.ts @@ -1,13 +1,9 @@ import type { Content } from '@plone/types'; -import find from 'lodash/find'; -import keys from 'lodash/keys'; -import endsWith from 'lodash/endsWith'; export function hasBlocksData(content: Content) { return ( - find( - keys(content), - (key) => key !== 'volto.blocks' && endsWith(key, 'blocks'), + Object.keys(content).find( + (key) => key !== 'volto.blocks' && key.endsWith('blocks'), ) !== undefined ); } diff --git a/packages/components/src/stories/Introduction.stories.mdx b/packages/components/src/stories/Introduction.stories.mdx index 1743ccc57c..41ecc9f5d6 100644 --- a/packages/components/src/stories/Introduction.stories.mdx +++ b/packages/components/src/stories/Introduction.stories.mdx @@ -33,3 +33,47 @@ const MyComponent = (props) => { export default MyComponent; ``` +## Styling + +This package provide a basic set of CSS rules. +You should add them to your project build to make the components properly styled. + +You can bring your own styles, but the CSS you provide should replace what the basic stylying does and style the bare components from scratch. + +You can use the CSS bundled for all components in a single file, or use the specific files for your components. +They are distributed along with the components code in the `dist` folder of the library. + +```js +import '@plone/components/dist/basic.css'; +``` + +or selectively: + +```js +import '@plone/components/src/styles/basic/TextField.css'; +``` + + +## Quanta + +This package also features the Quanta components. +The Quanta theme is an example of it. +These components use the basic styling as a baseline, not only in styling, but also in the component side, reusing the CSS and custom CSS properties in it. + +Quanta is built upon the basic styles in an additive way. +The use of the Quanta CSS implies using it upon basic styling. +You could take Quanta as example to build your own layer of styles over basic styling for your theme. + +To use a theme built upon the basic styling, you need to import both the basic and the theme CSS, in this order: + +```js +import '@plone/components/dist/basic.css'; +import '@plone/components/dist/quanta.css'; +``` + +You have the option of doing it selectively per component, too: + +```js +import '@plone/components/src/styles/basic/TextField.css'; +import '@plone/components/src/styles/quanta/TextField.css'; +``` diff --git a/packages/components/src/styles/basic/main.css b/packages/components/src/styles/basic/main.css index 09a850cf68..fa7999e24f 100644 --- a/packages/components/src/styles/basic/main.css +++ b/packages/components/src/styles/basic/main.css @@ -4,7 +4,7 @@ @import './Label.css'; @import './Checkbox.css'; @import './CheckboxGroup.css'; -@import './Combobox.css'; +@import './ComboBox.css'; @import './NumberField.css'; @import './RadioGroup.css'; @import './Switch.css'; @@ -22,8 +22,8 @@ @import './TextField.css'; @import './Menu.css'; -@import './Listbox.css'; -@import './Gridlist.css'; +@import './ListBox.css'; +@import './GridList.css'; @import './Form.css'; @import './Table.css'; diff --git a/packages/components/src/styles/quanta/Select.css b/packages/components/src/styles/quanta/Select.css index 99855ac40a..dda92780e5 100644 --- a/packages/components/src/styles/quanta/Select.css +++ b/packages/components/src/styles/quanta/Select.css @@ -1,6 +1,6 @@ /* Quanta does not have to redefine colors, but it has to override the original ones coming from the basic styles. Then, if people override the basic ones, then they won't get overridden in Quanta. */ -.react-aria-Select { +.q.react-aria-Select { .react-aria-Label { color: var(--quanta-sapphire); } @@ -30,7 +30,7 @@ from the basic styles. Then, if people override the basic ones, then they won't } } -.react-aria-Popover[data-trigger='Select'] { +.q.react-aria-Popover[data-trigger='Select'] { .react-aria-ListBoxItem { --highlight-background: var(--basic-300); --highlight-foreground: var(--text-color); diff --git a/packages/components/src/views/RenderBlocks/RenderBlocks.tsx b/packages/components/src/views/RenderBlocks/RenderBlocks.tsx index 2bde61d6f6..e0489596b8 100644 --- a/packages/components/src/views/RenderBlocks/RenderBlocks.tsx +++ b/packages/components/src/views/RenderBlocks/RenderBlocks.tsx @@ -1,6 +1,4 @@ import React, { Fragment } from 'react'; -// import { defineMessages, useIntl } from 'react-intl'; - import { hasBlocksData } from '../../helpers/blocks'; import { DefaultBlockView } from './DefaultBlockView'; import type { Content } from '@plone/types'; diff --git a/packages/components/tsconfig.node.json b/packages/components/tsconfig.node.json index b5376a7db7..473bb13b8a 100644 --- a/packages/components/tsconfig.node.json +++ b/packages/components/tsconfig.node.json @@ -5,5 +5,5 @@ "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, - "include": ["vitest.config.ts"] + "include": ["vitest.config.mts"] } diff --git a/packages/components/vite.config.ts b/packages/components/vite.config.mts similarity index 94% rename from packages/components/vite.config.ts rename to packages/components/vite.config.mts index 0bf55cebb7..e79241277d 100644 --- a/packages/components/vite.config.ts +++ b/packages/components/vite.config.mts @@ -18,8 +18,6 @@ export default defineConfig({ 'react-aria', 'react-aria-components', '@react-spectrum/utils', - 'classnames', - 'lodash', ], output: { globals: { diff --git a/packages/components/vitest.config.ts b/packages/components/vitest.config.mts similarity index 100% rename from packages/components/vitest.config.ts rename to packages/components/vitest.config.mts diff --git a/packages/coresandbox/package.json b/packages/coresandbox/package.json index e90e2e480c..aaf007882e 100644 --- a/packages/coresandbox/package.json +++ b/packages/coresandbox/package.json @@ -26,17 +26,17 @@ "react" ], "peerDependencies": { - "react": "17.0.2", - "react-dom": "17.0.2", + "react": "18.2.0", + "react-dom": "18.2.0", "react-intl": "3.8.0", "react-redux": "7.2.4", "semantic-ui-react": "2.0.3" }, "devDependencies": { "@plone/types": "workspace:*", + "@types/react": "^18", + "@types/react-dom": "^18", "@plone/registry": "workspace:*", - "@types/react": "^17.0.52", - "@types/react-dom": "^17", "@types/react-redux": "^7.1.33" } } diff --git a/packages/coresandbox/src/components/Slots/SlotTest.tsx b/packages/coresandbox/src/components/Slots/SlotTest.tsx new file mode 100644 index 0000000000..66f095c552 --- /dev/null +++ b/packages/coresandbox/src/components/Slots/SlotTest.tsx @@ -0,0 +1,10 @@ +const SlotComponentTest = () => { + return ( +
+

This is a test slot component

+

It should appear above the Content

+
+ ); +}; + +export default SlotComponentTest; diff --git a/packages/coresandbox/src/index.ts b/packages/coresandbox/src/index.ts index 1f4527a27c..b5c04d775b 100644 --- a/packages/coresandbox/src/index.ts +++ b/packages/coresandbox/src/index.ts @@ -9,6 +9,9 @@ import { conditionalVariationsSchemaEnhancer } from './components/Blocks/schemaE import codeSVG from '@plone/volto/icons/code.svg'; import type { BlockConfigBase } from '@plone/types'; import type { ConfigType } from '@plone/registry'; +import SlotComponentTest from './components/Slots/SlotTest'; +import { ContentTypeCondition } from '@plone/volto/helpers'; +import { RouteCondition } from '@plone/volto/helpers/Slots'; const testBlock: BlockConfigBase = { id: 'testBlock', @@ -171,6 +174,13 @@ const applyConfig = (config: ConfigType) => { config.blocks.blocksConfig.listing = listing(config); config.views.contentTypesViews.Folder = NewsAndEvents; + config.registerSlotComponent({ + slot: 'aboveContent', + name: 'testSlotComponent', + component: SlotComponentTest, + predicates: [ContentTypeCondition(['Document']), RouteCondition('/hello')], + }); + return config; }; diff --git a/packages/generator-volto/CHANGELOG.md b/packages/generator-volto/CHANGELOG.md index 9a115b56da..1389dfe0d4 100644 --- a/packages/generator-volto/CHANGELOG.md +++ b/packages/generator-volto/CHANGELOG.md @@ -8,6 +8,18 @@ +## 9.0.0-alpha.8 (2024-03-02) + +### Internal + +- Update dependencies @sneridagh [#5815](https://github.com/plone/volto/issues/5815) + +## 9.0.0-alpha.7 (2024-03-01) + +### Bugfix + +- Switch peerDependencies to Volto 18 for generator-volto alpha's/latest. [#5780](https://github.com/plone/volto/issues/5780) + ## 9.0.0-alpha.6 (2024-02-18) ### Bugfix diff --git a/packages/generator-volto/generators/app/templates/package.json.tpl b/packages/generator-volto/generators/app/templates/package.json.tpl index d19c2ca7c3..fb4195f4a1 100644 --- a/packages/generator-volto/generators/app/templates/package.json.tpl +++ b/packages/generator-volto/generators/app/templates/package.json.tpl @@ -151,11 +151,11 @@ "jest-junit": "8.0.0", "mrs-developer": "^2.1.1", "postcss": "8.4.13", - "prettier": "3.0.3", + "prettier": "3.2.5", "razzle": "4.2.18", - "stylelint": "15.10.3", - "stylelint-config-idiomatic-order": "9.0.0", - "stylelint-prettier": "4.0.2", + "stylelint": "^16.2.1", + "stylelint-config-idiomatic-order": "10.0.0", + "stylelint-prettier": "5.0.0", "ts-jest": "^26.4.2", "ts-loader": "9.4.4", "typescript": "5.2.2" diff --git a/packages/generator-volto/news/5780.bugfix b/packages/generator-volto/news/5780.bugfix deleted file mode 100644 index 996ccc7007..0000000000 --- a/packages/generator-volto/news/5780.bugfix +++ /dev/null @@ -1 +0,0 @@ -Switch peerDependencies to Volto 18 for generator-volto alpha's/latest. \ No newline at end of file diff --git a/packages/generator-volto/package.json b/packages/generator-volto/package.json index a214df45dd..254e70b19f 100644 --- a/packages/generator-volto/package.json +++ b/packages/generator-volto/package.json @@ -10,7 +10,7 @@ } ], "license": "MIT", - "version": "9.0.0-alpha.6", + "version": "9.0.0-alpha.8", "repository": { "type": "git", "url": "git+https://github.com/plone/generator-volto.git" diff --git a/packages/parcel-optimizer-react-client/package.json b/packages/parcel-optimizer-react-client/package.json index edb609816f..a9533e7207 100644 --- a/packages/parcel-optimizer-react-client/package.json +++ b/packages/parcel-optimizer-react-client/package.json @@ -4,10 +4,10 @@ "private": true, "main": "ReactClientOptimizer.js", "engines": { - "parcel": "^2.10.2" + "parcel": "^2.12.0" }, "dependencies": { - "@parcel/plugin": "^2.10.2", - "@parcel/utils": "^2.10.2" + "@parcel/plugin": "^2.12.0", + "@parcel/utils": "^2.12.0" } } diff --git a/packages/registry/CHANGELOG.md b/packages/registry/CHANGELOG.md index cdfd8c700e..b3468a12e4 100644 --- a/packages/registry/CHANGELOG.md +++ b/packages/registry/CHANGELOG.md @@ -8,6 +8,24 @@ +## 1.5.1 (2024-03-02) + +### Bugfix + +- Upgrade parcel @sneridagh [#5822](https://github.com/plone/volto/issues/5822) + +## 1.5.0 (2024-03-01) + +### Feature + +- Upgrade Volto core to use React 18.2.0 @sneridagh [#3221](https://github.com/plone/volto/issues/3221) + +## 1.4.0 (2024-03-01) + +### Feature + +- Support for slots @tiberiuichim @sneridagh [#5775](https://github.com/plone/volto/issues/5775) + ## 1.3.1 (2024-02-20) ### Bugfix diff --git a/packages/registry/package.json b/packages/registry/package.json index c8a2fa1c1f..8a4b376f6a 100644 --- a/packages/registry/package.json +++ b/packages/registry/package.json @@ -9,7 +9,7 @@ ], "funding": "https://github.com/sponsors/plone", "license": "MIT", - "version": "1.3.1", + "version": "1.5.1", "repository": { "type": "git", "url": "https://github.com/plone/volto.git" @@ -50,7 +50,7 @@ }, "scripts": { "watch": "parcel watch", - "build": "parcel build", + "build": "parcel build --no-cache", "test": "vitest", "dry-release": "release-it --dry-run", "release": "release-it", @@ -73,12 +73,14 @@ "glob": "7.1.6" }, "devDependencies": { - "@parcel/packager-ts": "2.10.2", - "@parcel/transformer-typescript-types": "2.10.2", + "@parcel/packager-ts": "^2.12.0", + "@parcel/transformer-typescript-types": "^2.12.0", "@plone/types": "workspace:*", - "@types/react": "^17.0.52", - "@types/react-dom": "^17", - "parcel": "2.10.2", + "@types/react": "^18", + "@types/react-dom": "^18", + "parcel": "^2.12.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", "release-it": "16.2.1", "tsconfig": "workspace:*", "typescript": "5.2.2", diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index dc81917a15..56740160ed 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -5,6 +5,10 @@ import type { ComponentsConfig, ExperimentalConfig, SettingsConfig, + GetSlotArgs, + GetSlotReturn, + SlotComponent, + SlotPredicate, SlotsConfig, ViewsConfig, WidgetsConfig, @@ -125,7 +129,7 @@ class Config { } getComponent( - options: { name: string; dependencies: string[] | string } | string, + options: { name: string; dependencies?: string[] | string } | string, ): GetComponentResult { if (typeof options === 'object') { const { name, dependencies = '' } = options; @@ -147,7 +151,7 @@ class Config { registerComponent(options: { name: string; - dependencies: string[] | string; + dependencies?: string[] | string; component: React.ComponentType; }) { const { name, component, dependencies = '' } = options; @@ -181,6 +185,214 @@ class Config { } } } + + getSlot(name: string, args: GetSlotArgs): GetSlotReturn { + if (!this._data.slots[name]) { + return; + } + const { slots, data } = this._data.slots[name]; + const slotComponents = []; + // For all enabled slots + for (const slotName of slots) { + // For all registered components for that slot, inversed, since the last one registered wins + // TODO: Cover ZCA use case, where if more predicates, more specificity wins if all true. + // Let's keep it simple here and stick to the registered order. + let noPredicateComponent: SlotComponent | undefined; + const reversedSlotComponents = data[slotName].slice().reverse(); // Immutable reversed copy + for (const slotComponent of reversedSlotComponents) { + let isPredicateTrueFound: boolean = false; + if (slotComponent.predicates) { + isPredicateTrueFound = slotComponent.predicates.every((predicate) => + predicate(args), + ); + } else { + // We mark the one with no predicates + noPredicateComponent = slotComponent; + } + + // If all the predicates are truthy + if (isPredicateTrueFound) { + slotComponents.push({ + component: slotComponent.component, + name: slotName, + }); + // We "reset" the marker, we already found a candidate + noPredicateComponent = undefined; + break; + } + } + + if (noPredicateComponent) { + slotComponents.push({ + component: noPredicateComponent.component, + name: slotName, + }); + } + } + + return slotComponents; + } + + registerSlotComponent(options: { + slot: string; + name: string; + predicates?: SlotPredicate[]; + component: SlotComponent['component']; + }): void { + const { name, component, predicates, slot } = options; + + if (!component) { + throw new Error('No component provided'); + } + if (!predicates) { + // Test if there's already one registered, we only support one + const hasRegisteredNoPredicatesComponent = this._data.slots?.[ + slot + ]?.data?.[name]?.find(({ predicates }) => !predicates); + if (hasRegisteredNoPredicatesComponent) { + throw new Error( + `There is already registered a component ${name} for the slot ${slot}. You can only register one slot component with no predicates per slot.`, + ); + } + } + + let currentSlot = this._data.slots[slot]; + if (!currentSlot) { + this._data.slots[slot] = { + slots: [], + data: {}, + }; + currentSlot = this._data.slots[slot]; + } + if (!currentSlot.data[name]) { + currentSlot.data[name] = []; + } + + const currentSlotComponents = currentSlot.data[name]; + if (!currentSlot.slots.includes(name)) { + currentSlot.slots.push(name); + } + const slotComponentData = { + component, + predicates, + }; + + // Try to set a displayName (useful for React dev tools) for the registered component + // Only if it's a function and it's not set previously + try { + const displayName = slotComponentData.component.displayName; + + if (!displayName && typeof slotComponentData?.component === 'function') { + slotComponentData.component.displayName = name; + } + } catch (error) { + // eslint-disable-next-line no-console + console.warn( + `Not setting the slot component displayName because ${error}`, + ); + } + + currentSlotComponents.push(slotComponentData); + } + + getSlotComponent(slot: string, name: string) { + const currentSlot = this._data.slots[slot]; + if (!slot || !currentSlot) { + throw new Error(`No slot ${slot} found`); + } + const currentSlotComponents = currentSlot.data[name]; + if (!currentSlotComponents) { + throw new Error(`No slot component ${name} in slot ${slot} found`); + } + return currentSlotComponents; + } + + getSlotComponents(slot: string) { + const currentSlot = this._data.slots[slot]; + if (!slot || !currentSlot) { + throw new Error(`No slot ${slot} found`); + } + return currentSlot.slots; + } + + reorderSlotComponent({ + slot, + name, + position, + action, + target, + }: { + slot: string; + name: string; + position?: number; + action?: 'after' | 'before' | 'first' | 'last'; + target?: string; + }) { + if (!position && !action) { + throw new Error(`At least a position or action is required as argument`); + } + if (position && action) { + throw new Error( + `You should provide only one of position or action as arguments`, + ); + } + if ((action == 'after' || action == 'before') && !target) { + throw new Error( + `No action target set. You should provide the name of a slot component as target when action is 'after' or 'before'.`, + ); + } + + const currentSlot = this._data.slots[slot]; + if (!slot || !currentSlot) { + throw new Error(`No slot ${slot} found`); + } + const origin = currentSlot.slots.indexOf(name); + const result = Array.from(currentSlot.slots); + const [removed] = result.splice(origin, 1); + + if (action) { + let targetIdx = 0; + if (target) { + targetIdx = currentSlot.slots.indexOf(target); + } + switch (action) { + case 'after': + result.splice(targetIdx, 0, removed); + break; + case 'before': + result.splice(targetIdx - 1, 0, removed); + break; + case 'last': + result.push(removed); + break; + case 'first': + result.unshift(removed); + break; + + default: + break; + } + } + + if (position) { + result.splice(position, 0, removed); + } + + currentSlot.slots = result; + } + + unRegisterSlotComponent(slot: string, name: string, position: number) { + const currentSlot = this._data.slots[slot]; + if (!slot || !currentSlot) { + throw new Error(`No slot ${slot} found`); + } + const currentSlotComponents = currentSlot.data[name]; + if (!currentSlotComponents) { + throw new Error(`No slot component ${name} in slot ${slot} found`); + } + const result = currentSlotComponents.slice(); + currentSlot.data[name] = result.splice(position, 1); + } } const instance = new Config(); diff --git a/packages/registry/src/registry.test.jsx b/packages/registry/src/registry.test.jsx deleted file mode 100644 index 861d26701f..0000000000 --- a/packages/registry/src/registry.test.jsx +++ /dev/null @@ -1,112 +0,0 @@ -import config from './index'; -import { describe, expect, it } from 'vitest'; - -config.set('components', { - Toolbar: { component: 'this is the Toolbar component' }, - 'Toolbar.Types': { component: 'this is the Types component' }, - 'Teaser|News Item': { component: 'This is the News Item Teaser component' }, -}); - -describe('registry', () => { - it('get components', () => { - expect(config.getComponent('Toolbar').component).toEqual( - 'this is the Toolbar component', - ); - }); - it('get components with context', () => { - expect( - config.getComponent({ name: 'Teaser', dependencies: 'News Item' }) - .component, - ).toEqual('This is the News Item Teaser component'); - }); - it('get components with dots (as an object)', () => { - expect(config.getComponent({ name: 'Toolbar.Types' }).component).toEqual( - 'this is the Types component', - ); - }); - it('get components with | and spaces (as a string)', () => { - expect(config.getComponent('Teaser|News Item').component).toEqual( - 'This is the News Item Teaser component', - ); - }); - it('resolves unexistent component (as a string)', () => { - expect(config.getComponent('Toolbar.Doh').component).toEqual(undefined); - expect(config.getComponent('Toolbar.Doh')).toEqual({}); - }); - it('registers and gets a component by name (as string)', () => { - config.registerComponent({ - name: 'Toolbar.Bar', - component: 'this is a Bar component', - }); - expect(config.getComponent('Toolbar.Bar').component).toEqual( - 'this is a Bar component', - ); - }); - it('registers and gets a component by name (as an object)', () => { - config.registerComponent({ - name: 'Toolbar.Bar', - component: 'this is a Bar component', - }); - expect(config.getComponent({ name: 'Toolbar.Bar' }).component).toEqual( - 'this is a Bar component', - ); - }); - it('registers and gets a component by name (as an object) - check displayName', () => { - config.registerComponent({ - name: 'Toolbar.Bar', - component: () =>
Hello
, - }); - expect(config.getComponent('Toolbar.Bar').component.displayName).toEqual( - 'Toolbar.Bar', - ); - }); - it('registers and gets a component by name (as an object) - check displayName if it has already one, it does not overwrite it', () => { - const TestComponent = () =>
Hello
; - TestComponent.displayName = 'DisplayNameAlreadySet'; - config.registerComponent({ - name: 'Toolbar.Bar', - component: TestComponent, - }); - - expect(config.getComponent('Toolbar.Bar').component.displayName).toEqual( - 'DisplayNameAlreadySet', - ); - }); - it('registers and gets a component by name (as an object) - check displayName - do not break if it is a normal function', () => { - function myFunction() { - return 'true'; - } - - config.registerComponent({ - name: 'Toolbar.Bar', - component: myFunction, - }); - expect(config.getComponent('Toolbar.Bar').component.displayName).toEqual( - 'Toolbar.Bar', - ); - }); - it('registers a component by name with dependencies', () => { - config.registerComponent({ - name: 'Toolbar.Bar', - component: 'this is a Bar component', - dependencies: 'News Item', - }); - expect( - config.getComponent({ name: 'Toolbar.Bar', dependencies: 'News Item' }) - .component, - ).toEqual('this is a Bar component'); - }); - it('registers a component by name with dependencies array', () => { - config.registerComponent({ - name: 'Toolbar.Bar', - component: 'this is a Bar component', - dependencies: ['News Item', 'StringFieldWidget'], - }); - expect( - config.getComponent({ - name: 'Toolbar.Bar', - dependencies: ['News Item', 'StringFieldWidget'], - }).component, - ).toEqual('this is a Bar component'); - }); -}); diff --git a/packages/registry/src/registry.test.tsx b/packages/registry/src/registry.test.tsx new file mode 100644 index 0000000000..f5879bebe6 --- /dev/null +++ b/packages/registry/src/registry.test.tsx @@ -0,0 +1,736 @@ +import config from './index'; +import { describe, expect, it, afterEach } from 'vitest'; + +config.set('components', { + Toolbar: { component: 'this is the Toolbar component' }, + 'Toolbar.Types': { component: 'this is the Types component' }, + 'Teaser|News Item': { component: 'This is the News Item Teaser component' }, +}); + +config.set('slots', {}); + +describe('Component registry', () => { + it('get components', () => { + expect(config.getComponent('Toolbar').component).toEqual( + 'this is the Toolbar component', + ); + }); + it('get components with context', () => { + expect( + config.getComponent({ name: 'Teaser', dependencies: 'News Item' }) + .component, + ).toEqual('This is the News Item Teaser component'); + }); + it('get components with dots (as an object)', () => { + expect(config.getComponent({ name: 'Toolbar.Types' }).component).toEqual( + 'this is the Types component', + ); + }); + it('get components with | and spaces (as a string)', () => { + expect(config.getComponent('Teaser|News Item').component).toEqual( + 'This is the News Item Teaser component', + ); + }); + it('resolves unexistent component (as a string)', () => { + expect(config.getComponent('Toolbar.Doh').component).toEqual(undefined); + expect(config.getComponent('Toolbar.Doh')).toEqual({}); + }); + it('registers and gets a component by name (as string)', () => { + config.registerComponent({ + name: 'Toolbar.Bar', + component: 'this is a Bar component', + }); + expect(config.getComponent('Toolbar.Bar').component).toEqual( + 'this is a Bar component', + ); + }); + it('registers and gets a component by name (as an object)', () => { + config.registerComponent({ + name: 'Toolbar.Bar', + component: 'this is a Bar component', + }); + expect(config.getComponent({ name: 'Toolbar.Bar' }).component).toEqual( + 'this is a Bar component', + ); + }); + it('registers and gets a component by name (as an object) - check displayName', () => { + config.registerComponent({ + name: 'Toolbar.Bar', + component: () =>
Hello
, + }); + expect(config.getComponent('Toolbar.Bar').component.displayName).toEqual( + 'Toolbar.Bar', + ); + }); + it('registers and gets a component by name (as an object) - check displayName if it has already one, it does not overwrite it', () => { + const TestComponent = () =>
Hello
; + TestComponent.displayName = 'DisplayNameAlreadySet'; + config.registerComponent({ + name: 'Toolbar.Bar', + component: TestComponent, + }); + + expect(config.getComponent('Toolbar.Bar').component.displayName).toEqual( + 'DisplayNameAlreadySet', + ); + }); + it('registers and gets a component by name (as an object) - check displayName - do not break if it is a normal function', () => { + function myFunction() { + return 'true'; + } + + config.registerComponent({ + name: 'Toolbar.Bar', + component: myFunction, + }); + expect(config.getComponent('Toolbar.Bar').component.displayName).toEqual( + 'Toolbar.Bar', + ); + }); + it('registers a component by name with dependencies', () => { + config.registerComponent({ + name: 'Toolbar.Bar', + component: 'this is a Bar component', + dependencies: 'News Item', + }); + expect( + config.getComponent({ name: 'Toolbar.Bar', dependencies: 'News Item' }) + .component, + ).toEqual('this is a Bar component'); + }); + it('registers a component by name with dependencies array', () => { + config.registerComponent({ + name: 'Toolbar.Bar', + component: 'this is a Bar component', + dependencies: ['News Item', 'StringFieldWidget'], + }); + expect( + config.getComponent({ + name: 'Toolbar.Bar', + dependencies: ['News Item', 'StringFieldWidget'], + }).component, + ).toEqual('this is a Bar component'); + }); +}); + +describe('Slots registry', () => { + afterEach(() => { + config.set('slots', {}); + }); + + // type Predicate = (predicateValues: unknown) = (predicateValues, args) => boolean + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const RouteConditionTrue = (route: string) => () => true; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const RouteConditionFalse = (route: string) => () => false; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const ContentTypeConditionTrue = (contentType: string[]) => () => true; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const ContentTypeConditionFalse = (contentType: string[]) => () => false; + + it('registers a single slot component with no predicate', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with no predicate', + }); + + expect(config.getSlot('toolbar', {})![0].component).toEqual( + 'this is a toolbar component with no predicate', + ); + }); + + it('registers two slot components with predicates - registered components order is respected', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: + 'this is a toolbar component with only a truth-ish route condition', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a Bar component with a false predicate and one true', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + expect(config.getSlot('toolbar', {})![0].component).toEqual( + 'this is a toolbar component with only a truth-ish route condition', + ); + }); + + it('registers two slot components with predicates - All registered components predicates are truthy, the last one registered wins', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: + 'this is a toolbar component with only a truth-ish route condition', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two truth-ish predicates', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + expect(config.getSlot('toolbar', {})![0].component).toEqual( + 'this is a toolbar component with two truth-ish predicates', + ); + }); + + it('registers two slot components with predicates - No registered component have a truthy predicate', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + expect(config.getSlot('toolbar', {})).toEqual([]); + }); + + it('registers two slot components one without predicates - registered component with predicates are truthy, the last one registered wins', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with no predicate', + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two truth-ish predicates', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + expect(config.getSlot('toolbar', {})![0].component).toEqual( + 'this is a toolbar component with two truth-ish predicates', + ); + }); + + it('registers two slot components one without predicates - registered components predicates are falsy, the one with no predicates wins', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with no predicate', + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two truth-ish predicates', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + expect(config.getSlot('toolbar', {})![0].component).toEqual( + 'this is a toolbar component with no predicate', + ); + }); + + it('registers two slot components one without predicates - registered components predicates are truthy, the one with predicates wins', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two truth-ish predicates', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with no predicate', + }); + + expect(config.getSlot('toolbar', {})![0].component).toEqual( + 'this is a toolbar component with two truth-ish predicates', + ); + }); + + it('registers 2 + 2 slot components with predicates - No registered component have a truthy predicate', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + expect(config.getSlot('toolbar', {})).toEqual([]); + }); + + it('registers 2 + 2 slot components with predicates - One truthy predicate per set', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + expect(config.getSlot('toolbar', {})![0].component).toEqual( + 'this is a toolbar save component with a true predicate', + ); + expect(config.getSlot('toolbar', {})![1].component).toEqual( + 'this is a toolbar edit component with true predicate', + ); + }); + + it('getSlotComponents - registers 2 + 2 slot components with predicates', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + expect(config.getSlotComponents('toolbar').length).toEqual(2); + expect(config.getSlotComponents('toolbar')).toEqual(['save', 'edit']); + }); + + it('getSlotComponent - registers 2 + 2 slot components with predicates', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + expect(config.getSlotComponent('toolbar', 'save').length).toEqual(2); + expect(config.getSlotComponent('toolbar', 'save')[0].component).toEqual( + 'this is a toolbar save component with a true predicate', + ); + }); + + it('reorderSlotComponent - position', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + expect(config.getSlotComponent('toolbar', 'save').length).toEqual(2); + expect(config.getSlotComponent('toolbar', 'save')[0].component).toEqual( + 'this is a toolbar save component with a true predicate', + ); + config.reorderSlotComponent({ slot: 'toolbar', name: 'save', position: 1 }); + expect(config.getSlotComponents('toolbar')).toEqual(['edit', 'save']); + }); + + it('reorderSlotComponent - after', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'cancel', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'bold', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + expect(config.getSlotComponents('toolbar')).toEqual([ + 'save', + 'edit', + 'cancel', + 'bold', + ]); + config.reorderSlotComponent({ + slot: 'toolbar', + name: 'save', + action: 'after', + target: 'cancel', + }); + expect(config.getSlotComponents('toolbar')).toEqual([ + 'edit', + 'cancel', + 'save', + 'bold', + ]); + }); + + it('reorderSlotComponent - before', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'cancel', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'bold', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + expect(config.getSlotComponents('toolbar')).toEqual([ + 'save', + 'edit', + 'cancel', + 'bold', + ]); + config.reorderSlotComponent({ + slot: 'toolbar', + name: 'save', + action: 'before', + target: 'cancel', + }); + expect(config.getSlotComponents('toolbar')).toEqual([ + 'edit', + 'save', + 'cancel', + 'bold', + ]); + }); + + it('reorderSlotComponent - last', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'cancel', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'bold', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + expect(config.getSlotComponents('toolbar')).toEqual([ + 'save', + 'edit', + 'cancel', + 'bold', + ]); + config.reorderSlotComponent({ + slot: 'toolbar', + name: 'save', + action: 'last', + }); + expect(config.getSlotComponents('toolbar')).toEqual([ + 'edit', + 'cancel', + 'bold', + 'save', + ]); + }); + + it('reorderSlotComponent - first', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'cancel', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'bold', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + + expect(config.getSlotComponents('toolbar')).toEqual([ + 'save', + 'edit', + 'cancel', + 'bold', + ]); + config.reorderSlotComponent({ + slot: 'toolbar', + name: 'bold', + action: 'first', + }); + expect(config.getSlotComponents('toolbar')).toEqual([ + 'bold', + 'save', + 'edit', + 'cancel', + ]); + }); + + it('unRegisterSlotComponent - registers 2 + 2 slot components with predicates', () => { + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar save component with a true predicate', + predicates: [RouteConditionTrue('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'save', + component: 'this is a toolbar component with two false predicate', + predicates: [ + RouteConditionFalse('/folder/path'), + ContentTypeConditionFalse(['News Item']), + ], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar component with a false predicate', + predicates: [RouteConditionFalse('/de')], + }); + + config.registerSlotComponent({ + slot: 'toolbar', + name: 'edit', + component: 'this is a toolbar edit component with true predicate', + predicates: [ + RouteConditionTrue('/folder/path'), + ContentTypeConditionTrue(['News Item']), + ], + }); + expect(config.getSlotComponent('toolbar', 'save').length).toEqual(2); + expect(config.getSlotComponent('toolbar', 'save')[0].component).toEqual( + 'this is a toolbar save component with a true predicate', + ); + config.unRegisterSlotComponent('toolbar', 'save', 1); + expect(config.getSlotComponent('toolbar', 'save').length).toEqual(1); + }); +}); diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 3e0ae44a23..e731d3c4bf 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -8,6 +8,16 @@ +## 3.4.0 (2024-03-02) + +### Feature + +- New `lockhook.js` script for replacing `yarnhook` @sneridagh [#5815](https://github.com/plone/volto/issues/5815) + +### Internal + +- Update dependencies @sneridagh [#5815](https://github.com/plone/volto/issues/5815) + ## 3.3.2 (2024-01-26) ### Bugfix diff --git a/packages/scripts/check_deployment.js b/packages/scripts/check_deployment.js new file mode 100644 index 0000000000..feb3ea0092 --- /dev/null +++ b/packages/scripts/check_deployment.js @@ -0,0 +1,41 @@ +import http from 'http'; +import waitOn from 'wait-on'; + +const url = new URL('http://localhost:3000'); // replace with your service URL +const searchString = 'Welcome to Plone'; // replace with the string you want to search for + +// Wait for the service to be available +waitOn({ resources: [url.href] }) + .then(() => { + console.log('Service is available'); + + // Fetch the HTML + http + .get(url.href, (res) => { + let data = ''; + + // A chunk of data has been received. + res.on('data', (chunk) => { + data += chunk; + }); + + // The whole response has been received. + res.on('end', () => { + // Check if the HTML contains the specific string + if (data.includes(searchString)) { + console.log(`Found "${searchString}" in the HTML`); + } else { + console.error(`Did not find "${searchString}" in the HTML`); + process.exit(1); + } + }); + }) + .on('error', (err) => { + console.error('Error: ' + err.message); + process.exit(1); + }); + }) + .catch((err) => { + console.error('An error occurred:', err); + process.exit(1); + }); diff --git a/packages/scripts/lockhook.js b/packages/scripts/lockhook.js new file mode 100644 index 0000000000..3574ad8014 --- /dev/null +++ b/packages/scripts/lockhook.js @@ -0,0 +1,138 @@ +#!/usr/bin/env node + +// @flow +import findParentDir from 'find-parent-dir'; +import execa from 'execa'; +import { join } from 'path'; +import fs from 'fs'; + +// environment variables +const { + LOCKHOOK_BYPASS = false, + LOCKHOOK_DEBUG = false, + LOCKHOOK_DRYRUN = false, +} = process.env; + +// supported package managers and lockfile names +const lockfileSpecs = [ + { + checkfile: 'bun.lockb', + lockfile: 'bun.lockb', + command: 'bun', + version: '1', + arguments: ['install', '--frozen-lockfile '], + }, + { + checkfile: '.yarnrc.yml', + lockfile: 'yarn.lock', + command: 'yarn', + version: '2', + arguments: ['install', '--immutable'], + }, + { + checkfile: 'yarn.lock', + lockfile: 'yarn.lock', + command: 'yarn', + version: '1', + arguments: ['install', '--prefer-offline', '--pure-lockfile'], + }, + { + checkfile: 'package-lock.json', + lockfile: 'package-lock.json', + command: 'npm', + version: '>=5', + arguments: ['install', '--prefer-offline', '--no-audit', '--no-save'], + }, + { + checkfile: 'npm-shrinkwrap.json', + lockfile: 'npm-shrinkwrap.json', + command: 'npm', + version: '<5', + arguments: ['install', '--prefer-offline', '--no-audit', '--no-save'], + }, + { + checkfile: 'pnpm-lock.yaml', + lockfile: 'pnpm-lock.yaml', + command: 'pnpm', + version: '>=3', + arguments: [ + 'install', + '--prefer-offline', + '--frozen-lockfile', + '--no-verify-store-integrity', + ], + }, + { + checkfile: 'shrinkwrap.yaml', + lockfile: 'shrinkwrap.yaml', + command: 'pnpm', + version: '<3', + arguments: ['install', '--prefer-offline', '--prefer-frozen-shrinkwrap'], + }, +]; + +function getLockfileSpec(currentDir) { + for (let lockfileSpec of lockfileSpecs) { + const checkfilePath = join(currentDir, lockfileSpec.checkfile); + if (fs.existsSync(checkfilePath)) { + return lockfileSpec; + } + } + + return null; +} + +if (!LOCKHOOK_BYPASS) { + // find directories + const currentDir = process.cwd(); + const gitDir = findParentDir.sync(currentDir, '.git'); + + // check for lockfiles + const lockfileSpec = getLockfileSpec(currentDir); + + if (LOCKHOOK_DEBUG) { + console.log('currentDir:', currentDir); + console.log('gitDir:', gitDir); + console.log('lockfile:', lockfileSpec); + } + + if (lockfileSpec !== null) { + // get the command, arguments and lockfile path + const { lockfile, command, arguments: commandargs } = lockfileSpec; + const lockfilePath = join(currentDir, lockfile); + + // run a git diff on the lockfile + const { stdout: output } = execa.sync( + 'git', + ['diff', 'HEAD@{1}..HEAD@{0}', '--', lockfilePath], + { cwd: gitDir }, + ); + + if (LOCKHOOK_DEBUG) { + console.log(output); + } + + // if diff exists, update dependencies + if (output.length > 0) { + if (LOCKHOOK_DRYRUN) { + console.log( + `Changes to lockfile found, you should run \`${command} install\` if you want to have up-to-date dependencies.`, + ); + } else { + console.log( + `Changes to lockfile found, running \`${command} install\``, + ); + try { + execa.sync(command, commandargs, { stdio: 'inherit' }); + } catch (err) { + console.warn(`Running ${command} ${commandargs.join(' ')} failed`); + } + } + } + } else { + const lockfiles = lockfileSpecs.map((spec) => spec.lockfile).join(', '); + console.warn( + `I can't find a lockfile. Currently supported lockfiles are: ${lockfiles}.`, + ); + } +} diff --git a/packages/scripts/news/5824.feature b/packages/scripts/news/5824.feature new file mode 100644 index 0000000000..10d0e18d00 --- /dev/null +++ b/packages/scripts/news/5824.feature @@ -0,0 +1 @@ +Add `check_deployments.js` script - Test simple deployments setups when Cypress is too much @sneridagh diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 9fd6c869c8..3cef14df40 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -9,7 +9,7 @@ } ], "license": "MIT", - "version": "3.3.2", + "version": "3.4.0", "repository": { "type": "git", "url": "git@github.com:plone/volto.git" @@ -48,12 +48,15 @@ "chalk": "4", "commander": "8.2.0", "comment-json": "^4.2.3", + "execa": "0.6.3", + "find-parent-dir": "^0.3.1", "fs-extra": "10.1.0", "git-url-parse": "^13.1.0", "glob": "7.1.6", "lodash": "4.17.21", "mrs-developer": "^2.1.1", - "pofile": "1.0.10" + "pofile": "1.0.10", + "wait-on": "^7.2.0" }, "devDependencies": { "release-it": "^16.1.3" diff --git a/packages/scripts/prepublish.js b/packages/scripts/prepublish.js index c467b92390..f22f8b99dd 100644 --- a/packages/scripts/prepublish.js +++ b/packages/scripts/prepublish.js @@ -12,7 +12,7 @@ class PrePublishReleaseItPlugin extends Plugin { type: 'confirm', message: (context) => `Are you sure you want to publish ${context.npm.name}${ - context.isPreRelease ? `@${context.preReleaseId}` : 'latest' + context.isPreRelease ? `@${context.preReleaseId}` : '@latest' } to npm?`, default: true, }, diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 2ad0136b0d..d6edab582b 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -8,6 +8,18 @@ +## 1.0.0-alpha.5 (2024-03-01) + +### Feature + +- Upgrade Volto core to use React 18.2.0 @sneridagh [#3221](https://github.com/plone/volto/issues/3221) + +## 1.0.0-alpha.4 (2024-03-01) + +### Feature + +- Support for slots @sneridagh [#5775](https://github.com/plone/volto/issues/5775) + ## 1.0.0-alpha.3 (2024-02-02) ### Bugfix diff --git a/packages/types/package.json b/packages/types/package.json index c8ff7d96d5..bc99040d69 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -9,7 +9,7 @@ ], "funding": "https://github.com/sponsors/plone", "license": "MIT", - "version": "1.0.0-alpha.3", + "version": "1.0.0-alpha.5", "repository": { "type": "git", "url": "https://github.com/plone/volto.git" @@ -50,10 +50,12 @@ "devDependencies": { "@parcel/packager-ts": "2.10.2", "@parcel/transformer-typescript-types": "2.10.2", - "@types/react": "^17.0.52", - "@types/react-dom": "^17", + "@types/react": "^18", + "@types/react-dom": "^18", "history": "^5.3.0", "parcel": "2.10.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-intl": "3.8.0", "release-it": "16.2.1", "tsconfig": "workspace:*", diff --git a/packages/types/src/config/Slots.d.ts b/packages/types/src/config/Slots.d.ts new file mode 100644 index 0000000000..87952c3f7d --- /dev/null +++ b/packages/types/src/config/Slots.d.ts @@ -0,0 +1,25 @@ +import type { Content } from '../content'; + +export type SlotPredicate = (args: any) => boolean; + +export type GetSlotArgs = { + content: Content; + pathname: string; + navRoot?: Content; +}; + +export type GetSlotReturn = + | { component: SlotComponent['component']; name: string }[] + | undefined; + +export type SlotComponent = { + component: React.ComponentType; + predicates?: SlotPredicate[]; +}; + +export type SlotManager = { + slots: string[]; + data: Record; +}; + +export type SlotsConfig = Record; diff --git a/packages/types/src/config/index.d.ts b/packages/types/src/config/index.d.ts index 02df192e63..a62e18c96d 100644 --- a/packages/types/src/config/index.d.ts +++ b/packages/types/src/config/index.d.ts @@ -2,6 +2,7 @@ import type { SettingsConfig } from './Settings'; import type { BlocksConfig } from './Blocks'; import type { ViewsConfig } from './Views'; import type { WidgetsConfig } from './Widgets'; +import type { SlotsConfig } from './Slots'; export type AddonReducersConfig = Record; @@ -11,8 +12,6 @@ export type AddonRoutesConfig = { component: React.ComponentType; }[]; -export type SlotsConfig = Record; - export type ComponentsConfig = Record< string, { component: React.ComponentType } @@ -34,3 +33,4 @@ export type ConfigData = { export { SettingsConfig, BlocksConfig, ViewsConfig, WidgetsConfig }; export * from './Blocks'; +export * from './Slots'; diff --git a/packages/types/src/content/common.d.ts b/packages/types/src/content/common.d.ts index b1b91edf84..de8922c49f 100644 --- a/packages/types/src/content/common.d.ts +++ b/packages/types/src/content/common.d.ts @@ -2,6 +2,7 @@ import type { BreadcrumbsResponse } from '../services/breadcrumbs'; import type { NavigationResponse } from '../services/navigation'; import type { ActionsResponse } from '../services/actions'; import type { GetTypesResponse } from '../services/types'; +import type { GetNavrootResponse } from '../services/navroot'; import type { GetAliasesResponse } from '../services/aliases'; import type { ContextNavigationResponse } from '../services/contextnavigation'; import type { WorkflowResponse } from '../services/workflow'; @@ -13,6 +14,7 @@ export interface Expanders { breadcrumbs: BreadcrumbsResponse; contextnavigation: ContextNavigationResponse; navigation: NavigationResponse; + navroot: GetNavrootResponse; types: GetTypesResponse; workflow: WorkflowResponse; } diff --git a/packages/volto-slate/CHANGELOG.md b/packages/volto-slate/CHANGELOG.md index 756fd90751..62324926d2 100644 --- a/packages/volto-slate/CHANGELOG.md +++ b/packages/volto-slate/CHANGELOG.md @@ -8,6 +8,18 @@ +## 18.0.0-alpha.9 (2024-03-02) + +### Internal + +- Update dependencies @sneridagh [#5815](https://github.com/plone/volto/issues/5815) + +## 18.0.0-alpha.8 (2024-03-01) + +### Feature + +- Upgrade Volto core to use React 18.2.0 @sneridagh [#3221](https://github.com/plone/volto/issues/3221) + ## 18.0.0-alpha.7 (2024-02-22) ### Bugfix diff --git a/packages/volto-slate/package.json b/packages/volto-slate/package.json index 8eadc11fee..1d4311f244 100644 --- a/packages/volto-slate/package.json +++ b/packages/volto-slate/package.json @@ -1,6 +1,6 @@ { "name": "@plone/volto-slate", - "version": "18.0.0-alpha.7", + "version": "18.0.0-alpha.9", "description": "Slate.js integration with Volto", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team", @@ -21,9 +21,9 @@ "image-extensions": "1.1.0", "is-url": "1.2.4", "jsdom": "^16.6.0", - "react": "17.0.2", + "react": "18.2.0", "react-intersection-observer": "9.1.0", - "react-dom": "17.0.2", + "react-dom": "18.2.0", "slate": "0.100.0", "slate-history": "0.100.0", "slate-hyperscript": "0.100.0", diff --git a/packages/volto-slate/src/blocks/Table/TableBlockView.jsx b/packages/volto-slate/src/blocks/Table/TableBlockView.jsx index 8ea9c210aa..4e10c84dcc 100644 --- a/packages/volto-slate/src/blocks/Table/TableBlockView.jsx +++ b/packages/volto-slate/src/blocks/Table/TableBlockView.jsx @@ -101,8 +101,8 @@ const View = ({ data }) => { state.column !== index ? 'ascending' : state.direction === 'ascending' - ? 'descending' - : 'ascending', + ? 'descending' + : 'ascending', }); }} > diff --git a/packages/volto-slate/src/blocks/Text/PluginSidebar.jsx b/packages/volto-slate/src/blocks/Text/PluginSidebar.jsx index 3858e9448f..a5a3185a05 100644 --- a/packages/volto-slate/src/blocks/Text/PluginSidebar.jsx +++ b/packages/volto-slate/src/blocks/Text/PluginSidebar.jsx @@ -1,16 +1,15 @@ import React from 'react'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; const PluginSidebar = ({ children, selected }) => { return ( <> - {selected && ( - - {children} - - )} + {selected && + __CLIENT__ && + createPortal( + <>{children}, + document.getElementById('slate-plugin-sidebar'), + )} ); }; diff --git a/packages/volto-slate/src/blocks/Text/SlashMenu.jsx b/packages/volto-slate/src/blocks/Text/SlashMenu.jsx index de312e3d89..6fd5ed2f30 100644 --- a/packages/volto-slate/src/blocks/Text/SlashMenu.jsx +++ b/packages/volto-slate/src/blocks/Text/SlashMenu.jsx @@ -122,8 +122,8 @@ const PersistentSlashMenu = ({ editor }) => { hasAllowedBlocks ? allowedBlocks.includes(item.id) : typeof item.restricted === 'function' - ? !item.restricted({ properties, block: item }) - : !item.restricted, + ? !item.restricted({ properties, block: item }) + : !item.restricted, ) .filter((block) => Boolean(block.title && block.id)) .filter((block) => { diff --git a/packages/volto-slate/src/blocks/Text/extensions/breakList.js b/packages/volto-slate/src/blocks/Text/extensions/breakList.js index c02e74ec13..b4117a0d8b 100644 --- a/packages/volto-slate/src/blocks/Text/extensions/breakList.js +++ b/packages/volto-slate/src/blocks/Text/extensions/breakList.js @@ -77,8 +77,8 @@ export const breakList = (editor) => { Editor.deleteBackward(editor, { unit: 'line' }); // also account for empty nodes [{text: ''}] if (Editor.isEmpty(editor, parent)) { - Transforms.removeNodes(editor, { at: ref.current }); createAndSelectNewBlockAfter(editor, [createEmptyParagraph()]); + Transforms.removeNodes(editor, { at: ref.current }); return true; } diff --git a/packages/volto-slate/src/blocks/Text/extensions/insertBreak.js b/packages/volto-slate/src/blocks/Text/extensions/insertBreak.js index 258aa7d382..d767c30ab6 100644 --- a/packages/volto-slate/src/blocks/Text/extensions/insertBreak.js +++ b/packages/volto-slate/src/blocks/Text/extensions/insertBreak.js @@ -42,9 +42,9 @@ export const withSplitBlocksOnBreak = (editor) => { // deconstructToVoltoBlocks ReactDOM.unstable_batchedUpdates(() => { const [top, bottom] = splitEditorInTwoFragments(editor); - setEditorContent(editor, top); // ReactEditor.blur(editor); createAndSelectNewBlockAfter(editor, bottom); + setEditorContent(editor, top); }); } return; diff --git a/packages/volto-slate/src/blocks/Text/index.js b/packages/volto-slate/src/blocks/Text/index.js index 562cc58341..37b93ecd02 100644 --- a/packages/volto-slate/src/blocks/Text/index.js +++ b/packages/volto-slate/src/blocks/Text/index.js @@ -128,8 +128,8 @@ export default function applyConfig(config) { return override_toc && level ? [parseInt(level.slice(1)), entry_text] : config.settings.slate.topLevelTargetElements.includes(type) - ? [parseInt(type.slice(1)), plaintext] - : null; + ? [parseInt(type.slice(1)), plaintext] + : null; }, }; diff --git a/packages/volto-slate/src/blocks/Text/keyboard/indentListItems.js b/packages/volto-slate/src/blocks/Text/keyboard/indentListItems.js index 7a17ecd26f..4f9f81cba2 100644 --- a/packages/volto-slate/src/blocks/Text/keyboard/indentListItems.js +++ b/packages/volto-slate/src/blocks/Text/keyboard/indentListItems.js @@ -55,8 +55,8 @@ export function indentListItems({ editor, event }) { ? decreaseMultipleItemsDepth(editor, event) : decreaseItemDepth(editor, event) : event.ctrlKey - ? increaseMultipleItemDepth(editor, event) - : increaseItemDepth(editor, event); + ? increaseMultipleItemDepth(editor, event) + : increaseItemDepth(editor, event); } } diff --git a/packages/volto-slate/src/editor/SlateEditor.jsx b/packages/volto-slate/src/editor/SlateEditor.jsx index d59e8b3f4a..fe8457b191 100644 --- a/packages/volto-slate/src/editor/SlateEditor.jsx +++ b/packages/volto-slate/src/editor/SlateEditor.jsx @@ -166,7 +166,14 @@ class SlateEditor extends Component { ReactEditor.focus(editor); Transforms.select(editor, selection); } else { - Transforms.select(editor, Editor.end(editor, [])); + try { + Transforms.select(editor, Editor.end(editor, [])); + } catch (error) { + // Weird error only happening in Cypress + // Adding a try/catch + // eslint-disable-next-line no-console + console.log(error); + } } this.setState({ diff --git a/packages/volto-slate/src/editor/ui/PositionedToolbar.jsx b/packages/volto-slate/src/editor/ui/PositionedToolbar.jsx index 93ab6d27d2..1ea7ab68ce 100644 --- a/packages/volto-slate/src/editor/ui/PositionedToolbar.jsx +++ b/packages/volto-slate/src/editor/ui/PositionedToolbar.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import BasicToolbar from './BasicToolbar'; @@ -20,12 +20,11 @@ const PositionedToolbar = ({ toggleButton, className, children, position }) => { el.style.left = left; }); - return ( - - - {children} - - + return createPortal( + + {children} + , + document.body, ); }; diff --git a/packages/volto-slate/src/editor/ui/Toolbar.jsx b/packages/volto-slate/src/editor/ui/Toolbar.jsx index 2d7406e12f..4e7afc51de 100644 --- a/packages/volto-slate/src/editor/ui/Toolbar.jsx +++ b/packages/volto-slate/src/editor/ui/Toolbar.jsx @@ -1,11 +1,11 @@ import cx from 'classnames'; import React, { useRef, useEffect } from 'react'; -import { Portal } from 'react-portal'; import { useSlate } from 'slate-react'; import Separator from './Separator'; import BasicToolbar from './BasicToolbar'; import { Editor, Node } from 'slate'; import { ReactEditor } from 'slate-react'; +import { createPortal } from 'react-dom'; const Toolbar = ({ elementType, @@ -82,21 +82,20 @@ const Toolbar = ({ )}px`; }); - return ( - - - {children} - {enableExpando && toggleButton && ( - <> - - {toggleButton} - - )} - - + return createPortal( + + {children} + {enableExpando && toggleButton && ( + <> + + {toggleButton} + + )} + , + document.body, ); }; diff --git a/packages/volto/CHANGELOG.md b/packages/volto/CHANGELOG.md index b148fe9e5c..1273a7286f 100644 --- a/packages/volto/CHANGELOG.md +++ b/packages/volto/CHANGELOG.md @@ -17,6 +17,50 @@ myst: +## 18.0.0-alpha.16 (2024-03-02) + +### Internal + +- Update dependencies + Fix prettier due to new version @sneridagh [#5815](https://github.com/plone/volto/issues/5815) + +### Documentation + +- Linkcheck thinks `README.md` is `http://README.md`. Bad linkcheck, no more 🍺 for you. @stevepiercy [#5816](https://github.com/plone/volto/issues/5816) + +## 18.0.0-alpha.15 (2024-03-01) + +### Breaking + +- Upgrade Volto core to use React 18.2.0 @sneridagh [#3221](https://github.com/plone/volto/issues/3221) + +## 18.0.0-alpha.14 (2024-03-01) + +### Breaking + +- Improved accessibility of logo component. @Molochem [#5776](https://github.com/plone/volto/issues/5776) + +### Feature + +- Support for slots @sneridagh [#5775](https://github.com/plone/volto/issues/5775) + +### Bugfix + +- Fixed toolbar menus not closing when clicking again on the toolbar buttons that show menus. @ichim-david + Add focus-visible rule to toolbar buttons so that it's visible to the user what button is focused when using tab navigation @ichim-david [#5645](https://github.com/plone/volto/issues/5645) +- Enhance findBlocks to check for blocks also in data for add-ons such as @eeacms/volto-tabs-block. @ichim-david [#5796](https://github.com/plone/volto/issues/5796) +- Fixed ArrayWidget sorting items. @giuliaghisini [#5805](https://github.com/plone/volto/issues/5805) + +### Internal + +- New types declarations with @types/react@18 - make tsc happy @sneridagh [#5814](https://github.com/plone/volto/issues/5814) + +### Documentation + +- Added Release Management Notes. @sneridagh @stevepiercy [#5358](https://github.com/plone/volto/issues/5358) +- Delete redundant `developing-a-project.md`. @stevepiercy [#5675](https://github.com/plone/volto/issues/5675) +- Removed Memori and TwinCreator websites from README.md no longer made using Volto and giving 404 error. @ichim-david [#5802](https://github.com/plone/volto/issues/5802) + ## 18.0.0-alpha.13 (2024-02-22) ### Bugfix diff --git a/packages/volto/Makefile b/packages/volto/Makefile index fbb5a192fe..37a3b274ea 100644 --- a/packages/volto/Makefile +++ b/packages/volto/Makefile @@ -67,7 +67,10 @@ cypress-install: .PHONY: build-deps build-deps: - if [ ! -d $$(pwd)/../registry/dist ]; then (cd ../../ && pnpm build:deps); fi + @if [ ! -d $$(pwd)/../registry/dist ] || [ $$(find $$(pwd)/../registry -newer $$(pwd)/../registry/dist -print -quit) ]; then \ + (cd ../../ && pnpm build:deps); \ + fi + ##### Release (it runs the one inside) diff --git a/packages/volto/cypress/tests/core/basic/folder-contents.js b/packages/volto/cypress/tests/core/basic/folder-contents.js index 71dcaf9306..6efb50fe7e 100644 --- a/packages/volto/cypress/tests/core/basic/folder-contents.js +++ b/packages/volto/cypress/tests/core/basic/folder-contents.js @@ -130,25 +130,23 @@ describe('Folder Contents Tests', () => { }); it('Move items to top of folder and bottom of folder', () => { + cy.intercept('GET', `/**/@search*`).as('search'); + cy.intercept('PATCH', `/**/my-folder`).as('reorder'); // creating a Document - cy.createContent({ - contentType: 'Document', - contentId: 'child', - contentTitle: 'My Child', - path: 'my-folder', - }); - - // doing copy paste for dummy data - cy.get('svg[class="icon unchecked"]').click(); - cy.get('svg[class="icon copy"]').click(); var genArr = Array.from({ length: 56 }, (v, k) => k + 1); - cy.wrap(genArr).each((index) => { - cy.get('svg[class="icon paste"]').click({ force: true }); + genArr.forEach((item) => { + cy.createContent({ + contentType: 'Document', + contentTitle: 'My Child', + path: 'my-folder', + }); }); - cy.wait(2000); // just for clearing of toast // after adding 56 page I need to add a final page to move around. // when I add a page + cy.visit('/my-folder'); + cy.wait('@content'); + cy.get('#toolbar-add').click(); cy.get('#toolbar-add-document').click(); cy.getSlateTitle() @@ -159,6 +157,7 @@ describe('Folder Contents Tests', () => { // then a new page has been created cy.get('#toolbar-save').click(); + cy.wait('@content'); cy.url().should( 'eq', Cypress.config().baseUrl + '/my-folder/last-and-first-page', @@ -171,7 +170,7 @@ describe('Folder Contents Tests', () => { ).click(); cy.findByText('Move to top of folder').click(); cy.url().should('eq', Cypress.config().baseUrl + '/my-folder/contents'); - cy.wait(1000); // waiting for settling of odering or search return + cy.wait('@reorder'); // Checking if move to top of folder works or not. cy.get('table tbody tr:first-child a span').findByText( @@ -181,6 +180,7 @@ describe('Folder Contents Tests', () => { 'tr[aria-label="/my-folder/last-and-first-page"] svg[class="icon dropdown-popup-trigger"]', ).click(); cy.findByText('Move to bottom of folder').click(); + cy.wait('@reorder'); // Checking whether moving to bottom of folder works or not. cy.get('.contents-pagination .menu').findByText('2').click(); @@ -195,7 +195,8 @@ describe('Folder Contents Tests', () => { cy.get( 'tr[aria-label="/my-folder/last-and-first-page"] svg[class="icon dropdown-popup-trigger"]', ).click(); - // cy.intercept('GET', '/plone/++api++/my-folder/@search').as('getSearch'); // I don't know proper way to wait + cy.wait('@search'); + cy.findByText('Move to top of folder').click(); cy.get('.search.item button').click(); @@ -207,6 +208,8 @@ describe('Folder Contents Tests', () => { 'tr[aria-label="/my-folder/last-and-first-page"] svg[class="icon dropdown-popup-trigger"]', ).click(); cy.findByText('Move to bottom of folder').click(); + cy.wait('@reorder'); + cy.get('.contents-pagination .menu').findByText('2').click(); // Checking whether moving to bottom of folder works or not. diff --git a/packages/volto/cypress/tests/core/blocks/blocks-autofocus.js b/packages/volto/cypress/tests/core/blocks/blocks-autofocus.js index fed851ad51..ef48d8f8bb 100644 --- a/packages/volto/cypress/tests/core/blocks/blocks-autofocus.js +++ b/packages/volto/cypress/tests/core/blocks/blocks-autofocus.js @@ -1,7 +1,7 @@ describe('New Block Auto Focus Tests', () => { beforeEach(() => { - cy.intercept('GET', `/**/*?expand*`).as('content'); cy.intercept('GET', '/**/my-page').as('content'); + cy.intercept('GET', '/**/Document').as('schema'); cy.intercept('PATCH', '*').as('save'); // given a logged in editor and a page in edit mode @@ -15,12 +15,17 @@ describe('New Block Auto Focus Tests', () => { cy.visit('/my-page'); cy.wait('@content'); + cy.wait(500); + cy.navigate('/my-page/edit'); + cy.wait('@schema'); }); it('Press Enter on a description block adds new autofocused default block', () => { cy.addNewBlock('description'); cy.get('.documentDescription').first().click().type('{enter}'); + cy.get('.block-editor-description + .block-editor-slate'); + cy.wait(500); cy.get('*[class^="block-editor"]') .eq(2) .within(() => { @@ -34,6 +39,8 @@ describe('New Block Auto Focus Tests', () => { cy.get('.blocks-chooser .title').contains('Text').click({ force: true }); cy.get('.blocks-chooser .text').contains('Text').click({ force: true }); cy.get('.text-slate-editor-inner').first().click().type('{enter}'); + cy.wait(500); + cy.get('.block-editor-slate + .block-editor-slate'); cy.get('*[class^="block-editor"]') .eq(2) .within(() => { @@ -43,7 +50,15 @@ describe('New Block Auto Focus Tests', () => { it('Press Enter on a image block adds new autofocused default block', () => { cy.addNewBlock('image'); - cy.get('.block-editor-image').first().click().type('{enter}'); + // Timing issues ahead :( + cy.get('.block-editor-image .no-image-wrapper img') + .should('be.visible') + .and(($img) => { + // "naturalWidth" and "naturalHeight" are set when the image loads + expect($img[0].naturalWidth).to.be.greaterThan(0); + }); + cy.get('.block-editor-image').wait(500).click('topLeft').type('{enter}'); + cy.wait(500); cy.get('*[class^="block-editor"]') .eq(2) .within(() => { @@ -53,7 +68,15 @@ describe('New Block Auto Focus Tests', () => { it('Press Enter on a video block adds new autofocused default block', () => { cy.addNewBlock('video'); + cy.get('.block-editor-video .message img') + .should('be.visible') + .and(($img) => { + // "naturalWidth" and "naturalHeight" are set when the image loads + expect($img[0].naturalWidth).to.be.greaterThan(0); + }); cy.get('.block-editor-video').first().click().type('{enter}'); + cy.get('.block-editor-slate + .block-editor-slate'); + cy.wait(500); cy.get('*[class^="block-editor"]') .eq(2) .within(() => { @@ -64,6 +87,7 @@ describe('New Block Auto Focus Tests', () => { it('Press Enter on a listing block adds new autofocused default block', () => { cy.addNewBlock('listing'); cy.get('.block-editor-listing').first().click().type('{enter}'); + cy.wait(500); cy.get('*[class^="block-editor"]') .eq(2) .within(() => { @@ -74,6 +98,7 @@ describe('New Block Auto Focus Tests', () => { it('Press Enter on a table of contents block adds new autofocused default block', () => { cy.addNewBlock('contents'); cy.get('.block-editor-toc').first().click().type('{enter}'); + cy.wait(500); cy.get('*[class^="block-editor"]') .eq(2) .within(() => { @@ -83,7 +108,14 @@ describe('New Block Auto Focus Tests', () => { it('Press Enter on a maps block adds new autofocused default block', () => { cy.addNewBlock('maps'); + cy.get('.block-editor-maps .message img') + .should('be.visible') + .and(($img) => { + // "naturalWidth" and "naturalHeight" are set when the image loads + expect($img[0].naturalWidth).to.be.greaterThan(0); + }); cy.get('.block-editor-maps').first().click().type('{enter}'); + cy.wait(500); cy.get('*[class^="block-editor"]') .eq(2) .within(() => { @@ -94,6 +126,7 @@ describe('New Block Auto Focus Tests', () => { it('Press Enter on a html block adds new autofocused default block', () => { cy.addNewBlock('html'); cy.get('.block-editor-html').first().click().type('{enter}'); + cy.wait(500); cy.get('*[class^="block-editor"]') .eq(2) .within(() => { @@ -104,6 +137,7 @@ describe('New Block Auto Focus Tests', () => { it('Press Enter on a search block adds new autofocused default block', () => { cy.addNewBlock('search'); cy.get('.block-editor-search').first().click().type('{enter}'); + cy.wait(500); cy.get('*[class^="block-editor"]') .eq(2) .within(() => { diff --git a/packages/volto/cypress/tests/core/blocks/blocks-image.js b/packages/volto/cypress/tests/core/blocks/blocks-image.js index e96bb1dd21..9b76682372 100644 --- a/packages/volto/cypress/tests/core/blocks/blocks-image.js +++ b/packages/volto/cypress/tests/core/blocks/blocks-image.js @@ -15,6 +15,8 @@ describe('Blocks Tests', () => { cy.visit('/my-page'); cy.wait('@content'); + cy.wait(500); + cy.navigate('/my-page/edit'); cy.wait('@schema'); }); @@ -156,7 +158,7 @@ describe('Blocks Tests', () => { }); }); - it('Create an image block and initially alt attr is empty', () => { + it.only('Create an image block and initially alt attr is empty', () => { // when I add an image block via upload cy.get('.content-area .slate-editor [contenteditable=true]', { timeout: 10000, diff --git a/packages/volto/cypress/tests/core/blocks/listing/blocks-listing.js b/packages/volto/cypress/tests/core/blocks/listing/blocks-listing.js index cc4f5e13d8..ab7c82bfcf 100644 --- a/packages/volto/cypress/tests/core/blocks/listing/blocks-listing.js +++ b/packages/volto/cypress/tests/core/blocks/listing/blocks-listing.js @@ -445,7 +445,9 @@ describe('Listing Block Tests', () => { //add listing block cy.scrollTo('bottom'); - cy.addNewBlock('listing', true); + cy.getSlate().click(); + cy.get('.ui.basic.icon.button.block-add-button').click(); + cy.get('.ui.basic.icon.button.listing').contains('Listing').click(); //******** add Page Type criteria filter cy.configureListingWith('Page'); diff --git a/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-layout.js b/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-layout.js index 35a24d1b65..20b1a097e6 100644 --- a/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-layout.js +++ b/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-layout.js @@ -21,7 +21,7 @@ describe('ControlPanel: Dexterity Content-Types Layout', () => { 'Book', ); - cy.visit('/controlpanel/dexterity-types/book/layout'); + cy.navigate('/controlpanel/dexterity-types/book/layout'); cy.get('#page-controlpanel-layout').contains( 'Can not edit Layout for Book', ); diff --git a/packages/volto/cypress/tests/coresandbox/slots.js b/packages/volto/cypress/tests/coresandbox/slots.js new file mode 100644 index 0000000000..4d8abe6e48 --- /dev/null +++ b/packages/volto/cypress/tests/coresandbox/slots.js @@ -0,0 +1,43 @@ +context('Slots', () => { + describe('Block Default View / Edit', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + // given a logged in editor and a page in edit mode + cy.autologin(); + cy.createContent({ + contentType: 'Document', + contentId: 'document', + contentTitle: 'Test document', + }); + cy.createContent({ + contentType: 'Document', + contentId: 'hello', + contentTitle: 'Test document Hello', + }); + + cy.visit('/'); + cy.wait('@content'); + }); + + it('[ContentTypeCondition(["Document"]), RouteCondition("/hello")] only renders when the predicates are true', function () { + cy.get('body').should( + 'not.include.text', + 'This is a test slot component', + ); + + cy.navigate('/document'); + cy.wait('@content'); + + cy.get('body').should( + 'not.include.text', + 'This is a test slot component', + ); + + cy.navigate('/hello'); + cy.wait('@content'); + + cy.get('body').should('include.text', 'This is a test slot component'); + }); + }); +}); diff --git a/packages/volto/jest-setup-afterenv.js b/packages/volto/jest-setup-afterenv.js new file mode 100644 index 0000000000..8c79262657 --- /dev/null +++ b/packages/volto/jest-setup-afterenv.js @@ -0,0 +1,2 @@ +// Jest-crap setup after env T_T +import '@testing-library/jest-dom'; diff --git a/packages/volto/locales/ca/LC_MESSAGES/volto.po b/packages/volto/locales/ca/LC_MESSAGES/volto.po index 124ebbcd01..517af40068 100644 --- a/packages/volto/locales/ca/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ca/LC_MESSAGES/volto.po @@ -3656,6 +3656,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/de/LC_MESSAGES/volto.po b/packages/volto/locales/de/LC_MESSAGES/volto.po index 6011f4fed2..daebb01e75 100644 --- a/packages/volto/locales/de/LC_MESSAGES/volto.po +++ b/packages/volto/locales/de/LC_MESSAGES/volto.po @@ -3655,6 +3655,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/en/LC_MESSAGES/volto.po b/packages/volto/locales/en/LC_MESSAGES/volto.po index 19b258fecb..d54686ff84 100644 --- a/packages/volto/locales/en/LC_MESSAGES/volto.po +++ b/packages/volto/locales/en/LC_MESSAGES/volto.po @@ -3650,6 +3650,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/es/LC_MESSAGES/volto.po b/packages/volto/locales/es/LC_MESSAGES/volto.po index f9f065a686..0a9b7f60cd 100644 --- a/packages/volto/locales/es/LC_MESSAGES/volto.po +++ b/packages/volto/locales/es/LC_MESSAGES/volto.po @@ -3657,6 +3657,12 @@ msgstr "No hay grupos con los criterios buscados." msgid "There are no users with the searched criteria" msgstr "No hay usuarios con los criterios buscados" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/eu/LC_MESSAGES/volto.po b/packages/volto/locales/eu/LC_MESSAGES/volto.po index 7064a7b56c..2afcc9f192 100644 --- a/packages/volto/locales/eu/LC_MESSAGES/volto.po +++ b/packages/volto/locales/eu/LC_MESSAGES/volto.po @@ -3657,6 +3657,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/fi/LC_MESSAGES/volto.po b/packages/volto/locales/fi/LC_MESSAGES/volto.po index 03b2780a8c..ff5107a887 100644 --- a/packages/volto/locales/fi/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fi/LC_MESSAGES/volto.po @@ -3655,6 +3655,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/fr/LC_MESSAGES/volto.po b/packages/volto/locales/fr/LC_MESSAGES/volto.po index 79c1ee9372..fd79824475 100644 --- a/packages/volto/locales/fr/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fr/LC_MESSAGES/volto.po @@ -3657,6 +3657,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/it/LC_MESSAGES/volto.po b/packages/volto/locales/it/LC_MESSAGES/volto.po index ee27a0e94c..6353bffafb 100644 --- a/packages/volto/locales/it/LC_MESSAGES/volto.po +++ b/packages/volto/locales/it/LC_MESSAGES/volto.po @@ -3650,6 +3650,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/ja/LC_MESSAGES/volto.po b/packages/volto/locales/ja/LC_MESSAGES/volto.po index 23c0849a3c..200948bd4a 100644 --- a/packages/volto/locales/ja/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ja/LC_MESSAGES/volto.po @@ -3655,6 +3655,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/nl/LC_MESSAGES/volto.po b/packages/volto/locales/nl/LC_MESSAGES/volto.po index fe21ea8d91..e5bb5478f5 100644 --- a/packages/volto/locales/nl/LC_MESSAGES/volto.po +++ b/packages/volto/locales/nl/LC_MESSAGES/volto.po @@ -3654,6 +3654,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/pt/LC_MESSAGES/volto.po b/packages/volto/locales/pt/LC_MESSAGES/volto.po index c341c5f237..2239501dc2 100644 --- a/packages/volto/locales/pt/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt/LC_MESSAGES/volto.po @@ -3655,6 +3655,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po index 663e5e5ab4..fb355baa90 100644 --- a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po @@ -3656,6 +3656,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "Existem alguns erros." + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/ro/LC_MESSAGES/volto.po b/packages/volto/locales/ro/LC_MESSAGES/volto.po index 2c75e1aa99..0f5512ad88 100644 --- a/packages/volto/locales/ro/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ro/LC_MESSAGES/volto.po @@ -3650,6 +3650,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/volto.pot b/packages/volto/locales/volto.pot index 414d63d872..2791c609c7 100644 --- a/packages/volto/locales/volto.pot +++ b/packages/volto/locales/volto.pot @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Plone\n" -"POT-Creation-Date: 2024-02-26T09:23:35.781Z\n" +"POT-Creation-Date: 2024-02-26T19:56:41.001Z\n" "Last-Translator: Plone i18n \n" "Language-Team: Plone i18n \n" "Content-Type: text/plain; charset=utf-8\n" @@ -3652,6 +3652,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po index 105ead2efc..eb6b8a7ade 100644 --- a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po +++ b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po @@ -3656,6 +3656,12 @@ msgstr "" msgid "There are no users with the searched criteria" msgstr "" +#. Default: "There are some errors." +#: components/manage/Add/Add +#: components/manage/Edit/Edit +msgid "There are some errors." +msgstr "" + #. Default: "There is a configuration problem on the backend" #: components/theme/CorsError/CorsError msgid "There is a configuration problem on the backend" diff --git a/packages/volto/news/1868.bugfix b/packages/volto/news/1868.bugfix new file mode 100644 index 0000000000..f807b6ba1f --- /dev/null +++ b/packages/volto/news/1868.bugfix @@ -0,0 +1 @@ +Show validation error message as string instead of list. @wesleybl diff --git a/packages/volto/news/5358.documentation b/packages/volto/news/5358.documentation deleted file mode 100644 index c954effb8c..0000000000 --- a/packages/volto/news/5358.documentation +++ /dev/null @@ -1 +0,0 @@ -Added Release Management Notes. @sneridagh @stevepiercy diff --git a/packages/volto/news/5645.bugfix b/packages/volto/news/5645.bugfix deleted file mode 100644 index e7c1940261..0000000000 --- a/packages/volto/news/5645.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Fixed toolbar menus not closing when clicking again on the toolbar buttons that show menus. @ichim-david -Add focus-visible rule to toolbar buttons so that it's visible to the user what button is focused when using tab navigation @ichim-david \ No newline at end of file diff --git a/packages/volto/news/5776.breaking b/packages/volto/news/5776.breaking deleted file mode 100644 index 0c25069810..0000000000 --- a/packages/volto/news/5776.breaking +++ /dev/null @@ -1 +0,0 @@ -Improved accessibility of logo component. @Molochem \ No newline at end of file diff --git a/packages/volto/news/5796.bugfix b/packages/volto/news/5796.bugfix deleted file mode 100644 index e5975989d2..0000000000 --- a/packages/volto/news/5796.bugfix +++ /dev/null @@ -1 +0,0 @@ -Enhance findBlocks to check for blocks also in data for add-ons such as @eeacms/volto-tabs-block. @ichim-david \ No newline at end of file diff --git a/packages/volto/news/5802.documentation b/packages/volto/news/5802.documentation deleted file mode 100644 index 8d3c2f16d3..0000000000 --- a/packages/volto/news/5802.documentation +++ /dev/null @@ -1 +0,0 @@ -Removed Memori and TwinCreator websites from README.md no longer made using Volto and giving 404 error. @ichim-david \ No newline at end of file diff --git a/packages/volto/news/5805.bugfix b/packages/volto/news/5805.bugfix deleted file mode 100644 index 68088432fd..0000000000 --- a/packages/volto/news/5805.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed ArrayWidget sorting items. @giuliaghisini diff --git a/packages/volto/news/5825.bugfix b/packages/volto/news/5825.bugfix new file mode 100644 index 0000000000..844a5247b8 --- /dev/null +++ b/packages/volto/news/5825.bugfix @@ -0,0 +1 @@ +Modified build-deps make command to check if registry files are newer than dist to force rebuild. @ichim-david \ No newline at end of file diff --git a/packages/volto/package.json b/packages/volto/package.json index c202aaf0c6..0b1d9e518b 100644 --- a/packages/volto/package.json +++ b/packages/volto/package.json @@ -9,7 +9,7 @@ } ], "license": "MIT", - "version": "18.0.0-alpha.13", + "version": "18.0.0-alpha.16", "repository": { "type": "git", "url": "git@github.com:plone/volto.git" @@ -110,6 +110,9 @@ "./test-setup-config.js" ], "globalSetup": "./global-test-setup.js", + "setupFilesAfterEnv": [ + "/jest-setup-afterenv.js" + ], "globals": { "__DEV__": true }, @@ -189,6 +192,9 @@ "@plone/registry": "workspace:*", "@plone/scripts": "workspace:*", "@plone/volto-slate": "workspace:*", + "@redux-devtools/extension": "^3.3.0", + "@types/react": "^18.2.57", + "@types/react-dom": "^18.2.19", "autoprefixer": "10.4.8", "axe-core": "4.4.2", "babel-plugin-add-module-exports": "0.2.1", @@ -217,13 +223,13 @@ "draft-js-plugins-utils": "2.0.3", "draftjs-filters": "2.3.0", "eslint": "8.49.0", - "eslint-config-prettier": "9.0.0", + "eslint-config-prettier": "9.1.0", "eslint-config-react-app": "7.0.1", "eslint-import-resolver-alias": "1.1.2", "eslint-import-resolver-babel-plugin-root-import": "1.1.1", - "eslint-plugin-import": "2.28.1", + "eslint-plugin-import": "2.29.1", "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-prettier": "5.0.0", + "eslint-plugin-prettier": "5.1.3", "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", "express": "4.17.3", @@ -263,7 +269,7 @@ "postcss-overrides": "3.1.4", "postcss-scss": "4.0.6", "prepend-http": "2", - "prettier": "3.0.3", + "prettier": "3.2.5", "pretty-bytes": "5.3.0", "prismjs": "1.27.0", "process": "^0.11.10", @@ -274,7 +280,7 @@ "razzle-dev-utils": "4.2.18", "razzle-plugin-scss": "4.2.18", "rc-time-picker": "3.7.3", - "react": "17.0.2", + "react": "18.2.0", "react-anchor-link-smooth-scroll": "1.0.12", "react-animate-height": "2.0.17", "react-beautiful-dnd": "13.0.0", @@ -283,17 +289,16 @@ "react-detect-click-outside": "1.1.1", "react-dnd": "5.0.0", "react-dnd-html5-backend": "5.0.1", - "react-dom": "17.0.2", + "react-dom": "18.2.0", "react-dropzone": "11.1.0", "react-fast-compare": "2.0.4", "react-image-gallery": "1.2.7", "react-intersection-observer": "9.1.0", "react-intl": "3.8.0", - "react-intl-redux": "2.2.0", + "react-intl-redux": "2.3.0", "react-medium-image-zoom": "3.0.15", "react-popper": "^2.3.0", - "react-portal": "4.2.1", - "react-redux": "7.2.4", + "react-redux": "8.1.2", "react-router": "5.2.0", "react-router-config": "5.1.1", "react-router-dom": "5.2.0", @@ -301,21 +306,20 @@ "react-select": "4.3.1", "react-select-async-paginate": "0.5.3", "react-share": "2.3.1", - "react-side-effect": "2.1.0", + "react-side-effect": "2.1.2", "react-simple-code-editor": "0.7.1", "react-sortable-hoc": "2.0.0", - "react-test-renderer": "17.0.2", + "react-test-renderer": "18.2.0", "react-toastify": "5.4.1", "react-transition-group": "4.4.5", "react-virtualized": "9.22.3", "redraft": "0.10.2", - "redux": "4.1.0", - "redux-actions": "2.6.5", + "redux": "4.2.1", + "redux-actions": "3.0.0", "redux-connect": "10.0.0", - "redux-devtools-extension": "2.13.8", - "redux-localstorage-simple": "2.3.1", + "redux-localstorage-simple": "2.5.1", "redux-mock-store": "1.5.4", - "redux-thunk": "2.3.0", + "redux-thunk": "2.4.2", "rrule": "2.7.1", "semantic-ui-less": "2.4.1", "semantic-ui-react": "2.1.5", @@ -325,9 +329,9 @@ "slate-react": "0.98.4", "start-server-and-test": "1.14.0", "style-loader": "3.3.1", - "stylelint": "15.10.3", - "stylelint-config-idiomatic-order": "9.0.0", - "stylelint-prettier": "4.0.2", + "stylelint": "16.2.1", + "stylelint-config-idiomatic-order": "10.0.0", + "stylelint-prettier": "5.0.0", "superagent": "3.8.2", "svg-loader": "0.0.2", "svgo-loader": "3.0.3", @@ -343,8 +347,7 @@ "webpack-bundle-analyzer": "4.10.1", "webpack-dev-server": "4.11.1", "webpack-node-externals": "3.0.0", - "xmlrpc": "1.3.2", - "yarnhook": "0.5.1" + "xmlrpc": "1.3.2" }, "devDependencies": { "@jest/globals": "^29.7.0", @@ -360,14 +363,15 @@ "@storybook/manager-webpack5": "^6.5.15", "@storybook/react": "^6.5.15", "@testing-library/cypress": "9.0.0", - "@testing-library/jest-dom": "5.16.4", - "@testing-library/react": "12.1.5", + "@testing-library/jest-dom": "6.4.1", + "@testing-library/react": "14.2.0", "@testing-library/react-hooks": "8.0.1", "@types/jest": "^29.5.8", "@types/lodash": "^4.14.201", - "@types/react": "^17.0.52", - "@types/react-dom": "^17", - "@types/react-test-renderer": "18.0.1", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/react-router-dom": "^5.3.3", + "@types/react-test-renderer": "18.0.7", "@types/uuid": "^9.0.2", "@typescript-eslint/eslint-plugin": "6.7.0", "@typescript-eslint/parser": "6.7.0", @@ -384,7 +388,7 @@ "jsonwebtoken": "9.0.0", "react-docgen-typescript-plugin": "^1.0.5", "react-error-overlay": "6.0.9", - "react-is": "^16.13.1", + "react-is": "^18.2.0", "release-it": "^16.2.1", "semver": "^7.5.4", "tmp": "0.2.1", diff --git a/packages/volto/src/components/index.js b/packages/volto/src/components/index.js index 31f733f367..fc1196ee2b 100644 --- a/packages/volto/src/components/index.js +++ b/packages/volto/src/components/index.js @@ -64,8 +64,8 @@ export { default as FileView } from '@plone/volto/components/theme/View/FileView export { default as ImageView } from '@plone/volto/components/theme/View/ImageView'; export { default as NewsItemView } from '@plone/volto/components/theme/View/NewsItemView'; -export const EventView = loadable(() => - import('@plone/volto/components/theme/View/EventView'), +export const EventView = loadable( + () => import('@plone/volto/components/theme/View/EventView'), ); export { default as ListingView } from '@plone/volto/components/theme/View/ListingView'; @@ -152,13 +152,14 @@ export { default as FormFieldWrapper } from '@plone/volto/components/manage/Widg export { default as ArrayWidget } from '@plone/volto/components/manage/Widgets/ArrayWidget'; export { default as CheckboxWidget } from '@plone/volto/components/manage/Widgets/CheckboxWidget'; -export const DatetimeWidget = loadable(() => - import('@plone/volto/components/manage/Widgets/DatetimeWidget'), +export const DatetimeWidget = loadable( + () => import('@plone/volto/components/manage/Widgets/DatetimeWidget'), ); -export const RecurrenceWidget = loadable(() => - import( - '@plone/volto/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget' - ), +export const RecurrenceWidget = loadable( + () => + import( + '@plone/volto/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget' + ), ); export { default as FileWidget } from '@plone/volto/components/manage/Widgets/FileWidget'; diff --git a/packages/volto/src/components/manage/Add/Add.jsx b/packages/volto/src/components/manage/Add/Add.jsx index d1c14bee17..f5f187d9c5 100644 --- a/packages/volto/src/components/manage/Add/Add.jsx +++ b/packages/volto/src/components/manage/Add/Add.jsx @@ -11,7 +11,7 @@ import { compose } from 'redux'; import { keys, isEmpty } from 'lodash'; import { defineMessages, injectIntl } from 'react-intl'; import { Button, Grid, Menu } from 'semantic-ui-react'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { v4 as uuid } from 'uuid'; import qs from 'query-string'; import { toast } from 'react-toastify'; @@ -39,6 +39,7 @@ import { } from '@plone/volto/helpers'; import { preloadLazyLibs } from '@plone/volto/helpers/Loadable'; +import { tryParseJSON } from '@plone/volto/helpers'; import config from '@plone/volto/registry'; @@ -66,6 +67,10 @@ const messages = defineMessages({ id: 'Translate to {lang}', defaultMessage: 'Translate to {lang}', }, + someErrors: { + id: 'There are some errors.', + defaultMessage: 'There are some errors.', + }, }); /** @@ -168,13 +173,30 @@ class Add extends Component { new DOMParser().parseFromString(message, 'text/html')?.all[0] ?.textContent || message; + const errorsList = tryParseJSON(error); + let erroMessage; + if (Array.isArray(errorsList)) { + const invariantErrors = errorsList + .filter((errorItem) => !('field' in errorItem)) + .map((errorItem) => errorItem['message']); + if (invariantErrors.length > 0) { + // Plone invariant validation message. + erroMessage = invariantErrors.join(' - '); + } else { + // Error in specific field. + erroMessage = this.props.intl.formatMessage(messages.someErrors); + } + } else { + erroMessage = error; + } + this.setState({ error: error }); toast.error( , ); } @@ -366,8 +388,8 @@ class Add extends Component { }} global /> - {this.state.isClient && ( - + {this.state.isClient && + createPortal( } - /> - - )} - {visual && this.state.isClient && ( - - - - )} + />, + document.getElementById('toolbar'), + )} + {visual && + this.state.isClient && + createPortal(, document.getElementById('sidebar'))} ); diff --git a/packages/volto/src/components/manage/Add/Add.test.jsx b/packages/volto/src/components/manage/Add/Add.test.jsx index 2a1ee56192..4d6500e885 100644 --- a/packages/volto/src/components/manage/Add/Add.test.jsx +++ b/packages/volto/src/components/manage/Add/Add.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; import config from '@plone/volto/registry'; @@ -16,9 +16,8 @@ beforeAll(() => { config.settings.loadables = {}; }); -jest.mock('react-portal', () => ({ - Portal: jest.fn(() =>
), -})); +jest.mock('../Toolbar/Toolbar', () => jest.fn(() =>
)); + jest.mock('../Form/Form', () => jest.fn(() =>
)); describe('Add', () => { @@ -39,13 +38,12 @@ describe('Add', () => { messages: {}, }, }); - const component = renderer.create( + const { container } = render( , ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); it('renders an add component', () => { @@ -67,13 +65,13 @@ describe('Add', () => { messages: {}, }, }); - const component = renderer.create( + const { container } = render( , ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); it('renders an add component with schema', () => { @@ -106,12 +104,12 @@ describe('Add', () => { messages: {}, }, }); - const component = renderer.create( + const { container } = render( , ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/volto/src/components/manage/Add/__snapshots__/Add.test.jsx.snap b/packages/volto/src/components/manage/Add/__snapshots__/Add.test.jsx.snap index 00ad89f369..e0b4e6d306 100644 --- a/packages/volto/src/components/manage/Add/__snapshots__/Add.test.jsx.snap +++ b/packages/volto/src/components/manage/Add/__snapshots__/Add.test.jsx.snap @@ -1,7 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Add renders an add component 1`] = `
`; +exports[`Add renders an add component 1`] = ` +
+
+
+`; -exports[`Add renders an add component with schema 1`] = `
`; +exports[`Add renders an add component with schema 1`] = ` +
+
+
+`; -exports[`Add renders an empty add component 1`] = `
`; +exports[`Add renders an empty add component 1`] = ` +
+
+
+`; diff --git a/packages/volto/src/components/manage/Aliases/Aliases.jsx b/packages/volto/src/components/manage/Aliases/Aliases.jsx index 7554a9f20f..15af7a0326 100644 --- a/packages/volto/src/components/manage/Aliases/Aliases.jsx +++ b/packages/volto/src/components/manage/Aliases/Aliases.jsx @@ -8,7 +8,7 @@ import { Helmet } from '@plone/volto/helpers'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { Link } from 'react-router-dom'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { Button, Checkbox, @@ -317,8 +317,8 @@ class Aliases extends Component { - {this.state.isClient && ( - + {this.state.isClient && + createPortal( } - /> - - )} + />, + document.getElementById('toolbar'), + )} ); } diff --git a/packages/volto/src/components/manage/Aliases/Aliases.test.jsx b/packages/volto/src/components/manage/Aliases/Aliases.test.jsx index 31350c8d94..3ded78f739 100644 --- a/packages/volto/src/components/manage/Aliases/Aliases.test.jsx +++ b/packages/volto/src/components/manage/Aliases/Aliases.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react'; import { Provider } from 'react-intl-redux'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; @@ -9,9 +9,8 @@ import Aliases from './Aliases'; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); -jest.mock('react-portal', () => ({ - Portal: jest.fn(() =>
), -})); +jest.mock('../Toolbar/Toolbar', () => jest.fn(() =>
)); + jest.mock('../Toolbar/More', () => jest.fn(() =>
)); describe('Aliases', () => { @@ -45,12 +44,13 @@ describe('Aliases', () => { messages: {}, }, }); - const component = renderer.create( + const { container } = render( +
, ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/volto/src/components/manage/Aliases/__snapshots__/Aliases.test.jsx.snap b/packages/volto/src/components/manage/Aliases/__snapshots__/Aliases.test.jsx.snap index 85b8d5c307..5bfa3e94a7 100644 --- a/packages/volto/src/components/manage/Aliases/__snapshots__/Aliases.test.jsx.snap +++ b/packages/volto/src/components/manage/Aliases/__snapshots__/Aliases.test.jsx.snap @@ -1,96 +1,97 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Aliases renders aliases object control 1`] = ` -
+
- URL Management for - - Blog - -
-
- Using this form, you can manage alternative urls for an item. This is an easy way to make an item available under two different URLs. -
-
+ URL Management for + + Blog + +
+
+ Using this form, you can manage alternative urls for an item. This is an easy way to make an item available under two different URLs. +
+
- Add a new alternative url -
-

- Enter the absolute path where the alternative url should exist. The path must start with '/'. Only urls that result in a 404 not found page will result in a redirect occurring. -

-
- + Add a new alternative url
+

+ Enter the absolute path where the alternative url should exist. The path must start with '/'. Only urls that result in a 404 not found page will result in a redirect occurring. +

+
+
+ +
+
+
- -
- -
-
+
- Existing alternative urls for this item +
+ Existing alternative urls for this item +
+
- -
-
+ +
+ id="toolbar" + > +
+
`; diff --git a/packages/volto/src/components/manage/BlockChooser/BlockChooser.test.jsx b/packages/volto/src/components/manage/BlockChooser/BlockChooser.test.jsx index 22fd5b5f48..447c20f61d 100644 --- a/packages/volto/src/components/manage/BlockChooser/BlockChooser.test.jsx +++ b/packages/volto/src/components/manage/BlockChooser/BlockChooser.test.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import '@testing-library/jest-dom/extend-expect'; import { render, screen } from '@testing-library/react'; import { Provider } from 'react-intl-redux'; import configureStore from 'redux-mock-store'; diff --git a/packages/volto/src/components/manage/BlockChooser/BlockChooserButton.jsx b/packages/volto/src/components/manage/BlockChooser/BlockChooserButton.jsx index d52389492d..136b413b70 100644 --- a/packages/volto/src/components/manage/BlockChooser/BlockChooserButton.jsx +++ b/packages/volto/src/components/manage/BlockChooser/BlockChooserButton.jsx @@ -7,7 +7,7 @@ import config from '@plone/volto/registry'; import { Button, Ref } from 'semantic-ui-react'; import { defineMessages, useIntl } from 'react-intl'; import { usePopper } from 'react-popper'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; const messages = defineMessages({ addBlock: { @@ -115,8 +115,8 @@ const BlockChooserButton = (props) => { /> )} - {addNewBlockOpened && ( - + {addNewBlockOpened && + createPortal(
{ navRoot={navRoot} contentType={contentType} /> -
-
- )} +
, + document.body, + )} ); }; diff --git a/packages/volto/src/components/manage/BlockChooser/BlockChooserSearch.test.jsx b/packages/volto/src/components/manage/BlockChooser/BlockChooserSearch.test.jsx index 7176bdda06..2bc70ced70 100644 --- a/packages/volto/src/components/manage/BlockChooser/BlockChooserSearch.test.jsx +++ b/packages/volto/src/components/manage/BlockChooser/BlockChooserSearch.test.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import '@testing-library/jest-dom/extend-expect'; import { render } from '@testing-library/react'; import BlockChooserSearch from './BlockChooserSearch'; import { Provider } from 'react-intl-redux'; diff --git a/packages/volto/src/components/manage/BlockChooser/__snapshots__/BlockChooser.test.jsx.snap b/packages/volto/src/components/manage/BlockChooser/__snapshots__/BlockChooser.test.jsx.snap index d61323c068..b0f6bc9a54 100644 --- a/packages/volto/src/components/manage/BlockChooser/__snapshots__/BlockChooser.test.jsx.snap +++ b/packages/volto/src/components/manage/BlockChooser/__snapshots__/BlockChooser.test.jsx.snap @@ -26,7 +26,6 @@ exports[`BlocksChooser Fallback BlockChooser component onMutateBlock 1`] = ` value="" />
-
Image -
Listing -
Video -
@@ -142,7 +138,6 @@ exports[`BlocksChooser Fallback BlockChooser component onMutateBlock 1`] = ` style="height: 36px; width: auto; fill: currentColor;" /> Text -
@@ -186,7 +181,6 @@ exports[`BlocksChooser Fallback BlockChooser component onMutateBlock 1`] = ` style="height: 36px; width: auto; fill: currentColor;" /> Image -
Lead Image Field -
Video -
@@ -258,7 +250,6 @@ exports[`BlocksChooser Fallback BlockChooser component onMutateBlock 1`] = ` style="height: 36px; width: auto; fill: currentColor;" /> Listing -
Table of Contents -
Hero -
Maps -
HTML -
Table -
@@ -365,7 +351,6 @@ exports[`BlocksChooser renders a BlockChooser component 1`] = ` value="" />
-
Image -
Listing -
Video -
@@ -481,7 +463,6 @@ exports[`BlocksChooser renders a BlockChooser component 1`] = ` style="height: 36px; width: auto; fill: currentColor;" /> Text - @@ -525,7 +506,6 @@ exports[`BlocksChooser renders a BlockChooser component 1`] = ` style="height: 36px; width: auto; fill: currentColor;" /> Image -
Lead Image Field -
Video -
@@ -597,7 +575,6 @@ exports[`BlocksChooser renders a BlockChooser component 1`] = ` style="height: 36px; width: auto; fill: currentColor;" /> Listing -
Table of Contents -
Hero -
Maps -
HTML -
Table -
diff --git a/packages/volto/src/components/manage/Blocks/Block/Settings.test.jsx b/packages/volto/src/components/manage/Blocks/Block/Settings.test.jsx index 57b7ea90ca..8cc2a7f301 100644 --- a/packages/volto/src/components/manage/Blocks/Block/Settings.test.jsx +++ b/packages/volto/src/components/manage/Blocks/Block/Settings.test.jsx @@ -1,7 +1,6 @@ import React from 'react'; import Settings from './Settings'; import { render } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; import configureStore from 'redux-mock-store'; import config from '@plone/volto/registry'; import { Provider } from 'react-intl-redux'; diff --git a/packages/volto/src/components/manage/Blocks/Container/NewBlockAddButton.jsx b/packages/volto/src/components/manage/Blocks/Container/NewBlockAddButton.jsx index 89076a0560..713f518e22 100644 --- a/packages/volto/src/components/manage/Blocks/Container/NewBlockAddButton.jsx +++ b/packages/volto/src/components/manage/Blocks/Container/NewBlockAddButton.jsx @@ -5,7 +5,7 @@ import { BlockChooser, Icon } from '@plone/volto/components'; import { useDetectClickOutside } from '@plone/volto/helpers'; import addSVG from '@plone/volto/icons/add.svg'; import { usePopper } from 'react-popper'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; const messages = defineMessages({ addBlock: { @@ -59,24 +59,25 @@ const NewBlockAddButton = (props) => { - {isOpenMenu ? ( - -
- -
-
- ) : null} + {isOpenMenu + ? createPortal( +
+ +
, + document.body, + ) + : null} ); }; diff --git a/packages/volto/src/components/manage/Blocks/Image/Edit.jsx b/packages/volto/src/components/manage/Blocks/Image/Edit.jsx index 4e272d53f9..37d2c04be6 100644 --- a/packages/volto/src/components/manage/Blocks/Image/Edit.jsx +++ b/packages/volto/src/components/manage/Blocks/Image/Edit.jsx @@ -283,19 +283,19 @@ class Edit extends Component { data.image_scales ? undefined : isInternalURL(data.url) - ? // Backwards compat in the case that the block is storing the full server URL - (() => { - if (data.size === 'l') + ? // Backwards compat in the case that the block is storing the full server URL + (() => { + if (data.size === 'l') + return `${flattenToAppURL(data.url)}/@@images/image`; + if (data.size === 'm') + return `${flattenToAppURL( + data.url, + )}/@@images/image/preview`; + if (data.size === 's') + return `${flattenToAppURL(data.url)}/@@images/image/mini`; return `${flattenToAppURL(data.url)}/@@images/image`; - if (data.size === 'm') - return `${flattenToAppURL( - data.url, - )}/@@images/image/preview`; - if (data.size === 's') - return `${flattenToAppURL(data.url)}/@@images/image/mini`; - return `${flattenToAppURL(data.url)}/@@images/image`; - })() - : data.url + })() + : data.url } sizes={config.blocks.blocksConfig.image.getSizes(data)} alt={data.alt || ''} diff --git a/packages/volto/src/components/manage/Blocks/Image/ImageSidebar.jsx b/packages/volto/src/components/manage/Blocks/Image/ImageSidebar.jsx index 02c18552a2..2df5c64190 100644 --- a/packages/volto/src/components/manage/Blocks/Image/ImageSidebar.jsx +++ b/packages/volto/src/components/manage/Blocks/Image/ImageSidebar.jsx @@ -61,9 +61,9 @@ const ImageSidebar = (props) => { data.image_scales ? undefined : isInternalURL(data.url) - ? // Backwards compat in the case that the block is storing the full server URL - `${flattenToAppURL(data.url)}/@@images/image/preview` - : data.url + ? // Backwards compat in the case that the block is storing the full server URL + `${flattenToAppURL(data.url)}/@@images/image/preview` + : data.url } sizes="188px" alt={intl.formatMessage(messages.preview)} diff --git a/packages/volto/src/components/manage/Blocks/Image/View.jsx b/packages/volto/src/components/manage/Blocks/Image/View.jsx index 6d0964ca90..e84dc64c14 100644 --- a/packages/volto/src/components/manage/Blocks/Image/View.jsx +++ b/packages/volto/src/components/manage/Blocks/Image/View.jsx @@ -51,21 +51,21 @@ export const View = ({ className, data, detached, properties, style }) => { data.image_scales ? undefined : isInternalURL(data.url) - ? // Backwards compat in the case that the block is storing the full server URL - (() => { - if (data.size === 'l') + ? // Backwards compat in the case that the block is storing the full server URL + (() => { + if (data.size === 'l') + return `${flattenToAppURL(data.url)}/@@images/image`; + if (data.size === 'm') + return `${flattenToAppURL( + data.url, + )}/@@images/image/preview`; + if (data.size === 's') + return `${flattenToAppURL( + data.url, + )}/@@images/image/mini`; return `${flattenToAppURL(data.url)}/@@images/image`; - if (data.size === 'm') - return `${flattenToAppURL( - data.url, - )}/@@images/image/preview`; - if (data.size === 's') - return `${flattenToAppURL( - data.url, - )}/@@images/image/mini`; - return `${flattenToAppURL(data.url)}/@@images/image`; - })() - : data.url + })() + : data.url } sizes={config.blocks.blocksConfig.image.getSizes(data)} alt={data.alt || ''} diff --git a/packages/volto/src/components/manage/Blocks/Image/View.test.jsx b/packages/volto/src/components/manage/Blocks/Image/View.test.jsx index fb073c2f21..81aac59bfd 100644 --- a/packages/volto/src/components/manage/Blocks/Image/View.test.jsx +++ b/packages/volto/src/components/manage/Blocks/Image/View.test.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import '@testing-library/jest-dom/extend-expect'; import { render } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { Provider } from 'react-intl-redux'; diff --git a/packages/volto/src/components/manage/Blocks/Listing/withQuerystringResults.jsx b/packages/volto/src/components/manage/Blocks/Listing/withQuerystringResults.jsx index 6dd025769b..08c7bdeb6b 100644 --- a/packages/volto/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +++ b/packages/volto/src/components/manage/Blocks/Listing/withQuerystringResults.jsx @@ -67,19 +67,19 @@ export default function withQuerystringResults(WrappedComponent) { const totalPages = showAsFolderListing ? Math.ceil(content.items_total / b_size) : showAsQueryListing - ? Math.ceil(querystringResults[subrequestID].total / b_size) - : 0; + ? Math.ceil(querystringResults[subrequestID].total / b_size) + : 0; const prevBatch = showAsFolderListing ? content.batching?.prev : showAsQueryListing - ? querystringResults[subrequestID].batching?.prev - : null; + ? querystringResults[subrequestID].batching?.prev + : null; const nextBatch = showAsFolderListing ? content.batching?.next : showAsQueryListing - ? querystringResults[subrequestID].batching?.next - : null; + ? querystringResults[subrequestID].batching?.next + : null; const isImageGallery = (!data.variation && data.template === 'imageGallery') || diff --git a/packages/volto/src/components/manage/Blocks/Search/SelectStyling.jsx b/packages/volto/src/components/manage/Blocks/Search/SelectStyling.jsx index 7aa2153c05..ec9d37e738 100644 --- a/packages/volto/src/components/manage/Blocks/Search/SelectStyling.jsx +++ b/packages/volto/src/components/manage/Blocks/Search/SelectStyling.jsx @@ -48,8 +48,8 @@ export const customSelectStyles = { color: state.isSelected ? '#007bc1' : state.isFocused - ? '#4a4a4a' - : 'inherit', + ? '#4a4a4a' + : 'inherit', ':active': { backgroundColor: null, }, diff --git a/packages/volto/src/components/manage/Blocks/Search/components/CheckboxFacet.jsx b/packages/volto/src/components/manage/Blocks/Search/components/CheckboxFacet.jsx index f41acdd048..c4b86c1147 100644 --- a/packages/volto/src/components/manage/Blocks/Search/components/CheckboxFacet.jsx +++ b/packages/volto/src/components/manage/Blocks/Search/components/CheckboxFacet.jsx @@ -40,8 +40,8 @@ const CheckboxFacet = (props) => { ...(checked ? [value] : []), ] : checked - ? value - : null, + ? value + : null, ) } /> diff --git a/packages/volto/src/components/manage/Blocks/Search/components/Facets.jsx b/packages/volto/src/components/manage/Blocks/Search/components/Facets.jsx index d19ea0ad7e..28ecc0c636 100644 --- a/packages/volto/src/components/manage/Blocks/Search/components/Facets.jsx +++ b/packages/volto/src/components/manage/Blocks/Search/components/Facets.jsx @@ -144,8 +144,8 @@ const Facets = (props) => { ? intl.formatMessage(messages.showFilters) : intl.formatMessage(messages.moreFilters) : advancedFilters === 2 - ? intl.formatMessage(messages.hideFilters) - : intl.formatMessage(messages.lessFilters)} + ? intl.formatMessage(messages.hideFilters) + : intl.formatMessage(messages.lessFilters)} )} diff --git a/packages/volto/src/components/manage/Blocks/Search/components/SelectStyling.jsx b/packages/volto/src/components/manage/Blocks/Search/components/SelectStyling.jsx index b2b1215710..bfc279b06f 100644 --- a/packages/volto/src/components/manage/Blocks/Search/components/SelectStyling.jsx +++ b/packages/volto/src/components/manage/Blocks/Search/components/SelectStyling.jsx @@ -48,8 +48,8 @@ export const customSelectStyles = { color: state.isSelected ? '#007bc1' : state.isFocused - ? '#4a4a4a' - : 'inherit', + ? '#4a4a4a' + : 'inherit', ':active': { backgroundColor: null, }, @@ -98,8 +98,8 @@ export const sortOnSelectStyles = { color: state.isSelected ? '#007bc1' : state.isFocused - ? '#4a4a4a' - : 'inherit', + ? '#4a4a4a' + : 'inherit', ':active': { backgroundColor: null, }, diff --git a/packages/volto/src/components/manage/Blocks/Table/Edit.jsx b/packages/volto/src/components/manage/Blocks/Table/Edit.jsx index dee0234118..2f00b4ba77 100644 --- a/packages/volto/src/components/manage/Blocks/Table/Edit.jsx +++ b/packages/volto/src/components/manage/Blocks/Table/Edit.jsx @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; import { compose } from 'redux'; import { map, remove } from 'lodash'; import { Button, Segment, Table, Form } from 'semantic-ui-react'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import cx from 'classnames'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; @@ -670,8 +670,9 @@ class Edit extends Component { )} - {this.props.selected && this.state.isClient && ( - + {this.props.selected && + this.state.isClient && + createPortal(
{ @@ -736,9 +737,9 @@ class Edit extends Component { onChange={this.toggleCellType} /> -
-
- )} + , + document.getElementById('sidebar-properties'), + )} ); } diff --git a/packages/volto/src/components/manage/Blocks/ToC/variations/__snapshots__/DefaultTocRenderer.test.jsx.snap b/packages/volto/src/components/manage/Blocks/ToC/variations/__snapshots__/DefaultTocRenderer.test.jsx.snap index 59f2bde9df..b1037d169f 100644 --- a/packages/volto/src/components/manage/Blocks/ToC/variations/__snapshots__/DefaultTocRenderer.test.jsx.snap +++ b/packages/volto/src/components/manage/Blocks/ToC/variations/__snapshots__/DefaultTocRenderer.test.jsx.snap @@ -1,47 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders a default toc renderer component 1`] = ` -Array [ - "", - + + `; diff --git a/packages/volto/src/components/manage/Contents/Contents.jsx b/packages/volto/src/components/manage/Contents/Contents.jsx index c462592977..4641b74405 100644 --- a/packages/volto/src/components/manage/Contents/Contents.jsx +++ b/packages/volto/src/components/manage/Contents/Contents.jsx @@ -7,7 +7,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { compose } from 'redux'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { Link } from 'react-router-dom'; import { Button, @@ -1989,9 +1989,9 @@ class Contents extends Component { this.state.selected.length === 0 ? checkboxUncheckedSVG : this.state.selected.length === - this.state.items.length - ? checkboxCheckedSVG - : checkboxIndeterminateSVG + this.state.items.length + ? checkboxCheckedSVG + : checkboxIndeterminateSVG } color={ this.state.selected.length > 0 @@ -2159,8 +2159,8 @@ class Contents extends Component { - {this.state.isClient && ( - + {this.state.isClient && + createPortal( } - /> - - )} + />, + document.getElementById('toolbar'), + )} ) : ( diff --git a/packages/volto/src/components/manage/Contents/Contents.test.jsx b/packages/volto/src/components/manage/Contents/Contents.test.jsx index 48f01c1562..87982d36d4 100644 --- a/packages/volto/src/components/manage/Contents/Contents.test.jsx +++ b/packages/volto/src/components/manage/Contents/Contents.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; import { MemoryRouter } from 'react-router-dom'; @@ -14,9 +14,8 @@ beforeAll( await require('@plone/volto/helpers/Loadable/Loadable').__setLoadables(), ); -jest.mock('react-portal', () => ({ - Portal: jest.fn(() =>
), -})); +jest.mock('../Toolbar/Toolbar', () => jest.fn(() =>
)); + jest.mock('../../theme/Pagination/Pagination', () => jest.fn(() =>
), ); @@ -91,14 +90,15 @@ describe('Contents', () => { messages: {}, }, }); - const component = renderer.create( + const { container } = render( +
, ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/volto/src/components/manage/Contents/ContentsPropertiesModal.test.jsx b/packages/volto/src/components/manage/Contents/ContentsPropertiesModal.test.jsx index dd4a2b65bf..126e695026 100644 --- a/packages/volto/src/components/manage/Contents/ContentsPropertiesModal.test.jsx +++ b/packages/volto/src/components/manage/Contents/ContentsPropertiesModal.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; @@ -23,7 +23,7 @@ describe('ContentsPropertiesModal', () => { messages: {}, }, }); - const component = renderer.create( + const { container } = render( { /> , ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/volto/src/components/manage/Contents/__snapshots__/Contents.test.jsx.snap b/packages/volto/src/components/manage/Contents/__snapshots__/Contents.test.jsx.snap index a348d4c3db..2c6dd5c65f 100644 --- a/packages/volto/src/components/manage/Contents/__snapshots__/Contents.test.jsx.snap +++ b/packages/volto/src/components/manage/Contents/__snapshots__/Contents.test.jsx.snap @@ -1,1067 +1,600 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Contents renders a folder contents view component 1`] = ` -
+
- Loading +
+ Loading +
-
-
-
-
-
+
- -
-
- - - - -
-
- - - - -
- -
-
- - Home - + + + + +
- +
-
-
- - - - - - - - - - -
- - - - - Title - - Actions -
-
-
+ + + + + + + + Title + + + Actions + + + + + +
+ class="contents-pagination" + > +
-
-
-
+ + +
+
+
diff --git a/packages/volto/src/components/manage/Contents/__snapshots__/ContentsPropertiesModal.test.jsx.snap b/packages/volto/src/components/manage/Contents/__snapshots__/ContentsPropertiesModal.test.jsx.snap index 3c382ace86..c460a95329 100644 --- a/packages/volto/src/components/manage/Contents/__snapshots__/ContentsPropertiesModal.test.jsx.snap +++ b/packages/volto/src/components/manage/Contents/__snapshots__/ContentsPropertiesModal.test.jsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ContentsPropertiesModal renders a contents properties modal component 1`] = ` -
+
+
+
`; diff --git a/packages/volto/src/components/manage/Controlpanels/AddonsControlpanel.jsx b/packages/volto/src/components/manage/Controlpanels/AddonsControlpanel.jsx index 1248c90cbf..66e1bc7a06 100644 --- a/packages/volto/src/components/manage/Controlpanels/AddonsControlpanel.jsx +++ b/packages/volto/src/components/manage/Controlpanels/AddonsControlpanel.jsx @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { Link } from 'react-router-dom'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { Accordion, Button, @@ -550,8 +550,8 @@ class AddonsControlpanel extends Component { )} - {this.state.isClient && ( - + {this.state.isClient && + createPortal( } - /> - - )} + />, + document.getElementById('toolbar'), + )} ); } diff --git a/packages/volto/src/components/manage/Controlpanels/AddonsControlpanel.test.jsx b/packages/volto/src/components/manage/Controlpanels/AddonsControlpanel.test.jsx index 460b69f85e..67ce84f2ac 100644 --- a/packages/volto/src/components/manage/Controlpanels/AddonsControlpanel.test.jsx +++ b/packages/volto/src/components/manage/Controlpanels/AddonsControlpanel.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; @@ -7,9 +7,7 @@ import AddonsControlpanel from './AddonsControlpanel'; const mockStore = configureStore(); -jest.mock('react-portal', () => ({ - Portal: jest.fn(() =>
), -})); +jest.mock('../Toolbar/Toolbar', () => jest.fn(() =>
)); describe('AddonsControlpanel', () => { it('renders an addon control component', () => { @@ -57,12 +55,13 @@ describe('AddonsControlpanel', () => { messages: {}, }, }); - const component = renderer.create( + const { container } = render( +
, ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/volto/src/components/manage/Controlpanels/Aliases.jsx b/packages/volto/src/components/manage/Controlpanels/Aliases.jsx index 6a927d7ae5..6b985ea3d3 100644 --- a/packages/volto/src/components/manage/Controlpanels/Aliases.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Aliases.jsx @@ -10,7 +10,7 @@ import { compose } from 'redux'; import { Link } from 'react-router-dom'; import { getBaseUrl, getParentUrl, Helmet } from '@plone/volto/helpers'; import { removeAliases, addAliases, getAliases } from '@plone/volto/actions'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { Container, Button, @@ -652,8 +652,8 @@ class Aliases extends Component { - {this.state.isClient && ( - + {this.state.isClient && + createPortal( } - /> - - )} + />, + document.getElementById('toolbar'), + )}
); } diff --git a/packages/volto/src/components/manage/Controlpanels/Aliases.test.jsx b/packages/volto/src/components/manage/Controlpanels/Aliases.test.jsx index 661611753f..c6a9a18010 100644 --- a/packages/volto/src/components/manage/Controlpanels/Aliases.test.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Aliases.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { Provider } from 'react-intl-redux'; @@ -10,9 +10,7 @@ import { MemoryRouter } from 'react-router'; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); -jest.mock('react-portal', () => ({ - Portal: jest.fn(() =>
), -})); +jest.mock('../Toolbar/Toolbar', () => jest.fn(() =>
)); describe('Aliases', () => { it('renders an aliases control component', () => { @@ -59,14 +57,15 @@ describe('Aliases', () => { messages: {}, }, }); - const component = renderer.create( + const { container } = render( +
, ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/volto/src/components/manage/Controlpanels/ContentType.jsx b/packages/volto/src/components/manage/Controlpanels/ContentType.jsx index 097dd488ff..6b8edbb9e6 100644 --- a/packages/volto/src/components/manage/Controlpanels/ContentType.jsx +++ b/packages/volto/src/components/manage/Controlpanels/ContentType.jsx @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { getParentUrl } from '@plone/volto/helpers'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { Button, Header } from 'semantic-ui-react'; import { defineMessages, injectIntl } from 'react-intl'; import { toast } from 'react-toastify'; @@ -210,8 +210,8 @@ class ContentType extends Component { hideActions loading={this.props.cpanelRequest.update.loading} /> - {this.state.isClient && ( - + {this.state.isClient && + createPortal( } - /> - - )} + />, + document.getElementById('toolbar'), + )}
); } diff --git a/packages/volto/src/components/manage/Controlpanels/ContentType.test.jsx b/packages/volto/src/components/manage/Controlpanels/ContentType.test.jsx index 63aa9fcd68..159c303936 100644 --- a/packages/volto/src/components/manage/Controlpanels/ContentType.test.jsx +++ b/packages/volto/src/components/manage/Controlpanels/ContentType.test.jsx @@ -1,16 +1,14 @@ import React from 'react'; -import renderer from 'react-test-renderer'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; import { MemoryRouter, Route } from 'react-router-dom'; +import { render } from '@testing-library/react'; import ContentType from './ContentType'; const mockStore = configureStore(); -jest.mock('react-portal', () => ({ - Portal: jest.fn(() =>
), -})); +jest.mock('../Toolbar/Toolbar', () => jest.fn(() =>
)); jest.mock('../Form/Form', () => jest.fn(() =>
)); describe('ContentType', () => { @@ -35,7 +33,8 @@ describe('ContentType', () => { messages: {}, }, }); - const component = renderer.create( + + const { container } = render( { path={'/controlpanel/dexterity-types/:id'} component={ContentType} /> +
, ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/volto/src/components/manage/Controlpanels/ContentTypeLayout.jsx b/packages/volto/src/components/manage/Controlpanels/ContentTypeLayout.jsx index b2754d90ba..3e5cd1a388 100644 --- a/packages/volto/src/components/manage/Controlpanels/ContentTypeLayout.jsx +++ b/packages/volto/src/components/manage/Controlpanels/ContentTypeLayout.jsx @@ -14,7 +14,7 @@ import { getBlocksFieldname, getBlocksLayoutFieldname, } from '@plone/volto/helpers'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { Button, Segment } from 'semantic-ui-react'; import { toast } from 'react-toastify'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -343,26 +343,30 @@ class ContentTypeLayout extends Component { content={this.props.intl.formatMessage(messages.enable)} /> - - - this.onCancel()}> - - - - } - /> - + {this.state.isClient && + createPortal( + + this.onCancel()} + > + + + + } + />, + document.getElementById('toolbar'), + )} ); } @@ -392,26 +396,30 @@ class ContentTypeLayout extends Component { content={this.props.intl.formatMessage(messages.enable)} /> - - - this.onCancel()}> - - - - } - /> - + {this.state.isClient && + createPortal( + + this.onCancel()} + > + + + + } + />, + document.getElementById('toolbar'), + )} ); } @@ -449,50 +457,50 @@ class ContentTypeLayout extends Component { visual={this.state.visual} hideActions /> - - - - - - - - - } - /> - + {this.state.isClient && + createPortal( + , + document.getElementById('sidebar'), + )} + {this.state.isClient && + createPortal( + + + + + } + />, + document.getElementById('toolbar'), + )}
); } diff --git a/packages/volto/src/components/manage/Controlpanels/ContentTypeLayout.test.jsx b/packages/volto/src/components/manage/Controlpanels/ContentTypeLayout.test.jsx index 70127b8477..aee7d78ec6 100644 --- a/packages/volto/src/components/manage/Controlpanels/ContentTypeLayout.test.jsx +++ b/packages/volto/src/components/manage/Controlpanels/ContentTypeLayout.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; import { MemoryRouter, Route } from 'react-router-dom'; @@ -8,9 +8,8 @@ import ContentTypeLayout from './ContentTypeLayout'; const mockStore = configureStore(); -jest.mock('react-portal', () => ({ - Portal: jest.fn(() =>
), -})); +jest.mock('../Toolbar/Toolbar', () => jest.fn(() =>
)); + jest.mock('../Form/Form', () => jest.fn(() =>
)); describe('ContentTypeLayout', () => { @@ -38,7 +37,7 @@ describe('ContentTypeLayout', () => { schema: null, }, }); - const component = renderer.create( + const { container } = render( { path={'/controlpanel/dexterity-types/:id/layout'} component={ContentTypeLayout} /> +
, ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/volto/src/components/manage/Controlpanels/ContentTypeSchema.jsx b/packages/volto/src/components/manage/Controlpanels/ContentTypeSchema.jsx index b8ebf1dea0..ab6888140b 100644 --- a/packages/volto/src/components/manage/Controlpanels/ContentTypeSchema.jsx +++ b/packages/volto/src/components/manage/Controlpanels/ContentTypeSchema.jsx @@ -12,7 +12,7 @@ import saveSVG from '@plone/volto/icons/save.svg'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { defineMessages, injectIntl } from 'react-intl'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { connect } from 'react-redux'; import { toast } from 'react-toastify'; import { compose } from 'redux'; @@ -283,8 +283,8 @@ class ContentTypeSchema extends Component { onCancel={this.onCancel} hideActions /> - {this.state.isClient && ( - + {this.state.isClient && + createPortal( } - /> - - )} + />, + document.getElementById('toolbar'), + )}
); } diff --git a/packages/volto/src/components/manage/Controlpanels/ContentTypes.jsx b/packages/volto/src/components/manage/Controlpanels/ContentTypes.jsx index 4c386c4bce..4759ed6fb2 100644 --- a/packages/volto/src/components/manage/Controlpanels/ContentTypes.jsx +++ b/packages/volto/src/components/manage/Controlpanels/ContentTypes.jsx @@ -9,7 +9,7 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { Link } from 'react-router-dom'; import { getParentUrl } from '@plone/volto/helpers'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { last } from 'lodash'; import { Confirm, Container, Table, Button, Header } from 'semantic-ui-react'; import { toast } from 'react-toastify'; @@ -457,8 +457,8 @@ class ContentTypes extends Component { - {this.state.isClient && ( - + {this.state.isClient && + createPortal( } - /> - - )} + />, + document.getElementById('toolbar'), + )} ); } diff --git a/packages/volto/src/components/manage/Controlpanels/ContentTypes.test.jsx b/packages/volto/src/components/manage/Controlpanels/ContentTypes.test.jsx index 876aed77df..5289835b93 100644 --- a/packages/volto/src/components/manage/Controlpanels/ContentTypes.test.jsx +++ b/packages/volto/src/components/manage/Controlpanels/ContentTypes.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; import { MemoryRouter, Route } from 'react-router-dom'; @@ -8,9 +8,8 @@ import ContentTypes from './ContentTypes'; const mockStore = configureStore(); -jest.mock('react-portal', () => ({ - Portal: jest.fn(() =>
), -})); +jest.mock('../Toolbar/Toolbar', () => jest.fn(() =>
)); + jest.mock('../Form/Form', () => jest.fn(() =>
)); describe('ContentTypes', () => { @@ -60,14 +59,15 @@ describe('ContentTypes', () => { messages: {}, }, }); - const component = renderer.create( + const { container } = render( +
, ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/volto/src/components/manage/Controlpanels/Controlpanel.jsx b/packages/volto/src/components/manage/Controlpanels/Controlpanel.jsx index a6fc83740f..a55477d787 100644 --- a/packages/volto/src/components/manage/Controlpanels/Controlpanel.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Controlpanel.jsx @@ -9,7 +9,7 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { withRouter } from 'react-router-dom'; import { Helmet } from '@plone/volto/helpers'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { Button, Container } from 'semantic-ui-react'; import { defineMessages, injectIntl } from 'react-intl'; import { toast } from 'react-toastify'; @@ -167,8 +167,8 @@ class Controlpanel extends Component { loading={this.props.updateRequest.loading} /> - {this.state.isClient && ( - + {this.state.isClient && + createPortal( } - /> - - )} + />, + document.getElementById('toolbar'), + )}
); } diff --git a/packages/volto/src/components/manage/Controlpanels/Controlpanel.test.jsx b/packages/volto/src/components/manage/Controlpanels/Controlpanel.test.jsx index d03d87e2d6..1ee6a98c11 100644 --- a/packages/volto/src/components/manage/Controlpanels/Controlpanel.test.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Controlpanel.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; import { MemoryRouter, Route } from 'react-router-dom'; @@ -8,9 +8,8 @@ import Controlpanel from './Controlpanel'; const mockStore = configureStore(); -jest.mock('react-portal', () => ({ - Portal: jest.fn(() =>
), -})); +jest.mock('../Toolbar/Toolbar', () => jest.fn(() =>
)); + jest.mock('../Form/Form', () => jest.fn(() =>
)); describe('Controlpanel', () => { @@ -36,14 +35,15 @@ describe('Controlpanel', () => { messages: {}, }, }); - const component = renderer.create( + const { container } = render( +
, ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/volto/src/components/manage/Controlpanels/Controlpanels.jsx b/packages/volto/src/components/manage/Controlpanels/Controlpanels.jsx index 263be3b02f..d2751c0142 100644 --- a/packages/volto/src/components/manage/Controlpanels/Controlpanels.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Controlpanels.jsx @@ -8,7 +8,7 @@ import { concat, filter, last, map, sortBy, uniqBy } from 'lodash'; import PropTypes from 'prop-types'; import { useEffect, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { compose } from 'redux'; @@ -260,8 +260,8 @@ function Controlpanels({ - {isClient && ( - + {isClient && + createPortal( } - /> - - )} + />, + document.getElementById('toolbar'), + )}
); } diff --git a/packages/volto/src/components/manage/Controlpanels/Controlpanels.test.jsx b/packages/volto/src/components/manage/Controlpanels/Controlpanels.test.jsx index 23c9d83392..10ce403043 100644 --- a/packages/volto/src/components/manage/Controlpanels/Controlpanels.test.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Controlpanels.test.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { Provider } from 'react-intl-redux'; import { MemoryRouter } from 'react-router-dom'; -import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react'; import configureStore from 'redux-mock-store'; import config from '@plone/volto/registry'; @@ -9,9 +9,7 @@ import Controlpanels from './Controlpanels'; const mockStore = configureStore(); -jest.mock('react-portal', () => ({ - Portal: jest.fn(() =>
), -})); +jest.mock('../Toolbar/Toolbar', () => jest.fn(() =>
)); jest.mock('./VersionOverview', () => jest.fn(() =>
), @@ -73,15 +71,16 @@ describe('Controlpanels', () => { messages: {}, }, }); - const component = renderer.create( + const { container } = render( +
, ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); it('renders an additional control panel', () => { @@ -127,14 +126,15 @@ describe('Controlpanels', () => { component: FooComponent, }, ]; - const component = renderer.create( + const { container } = render( +
, ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/volto/src/components/manage/Controlpanels/DatabaseInformation.jsx b/packages/volto/src/components/manage/Controlpanels/DatabaseInformation.jsx index ec17c9b8ce..04b29655ad 100644 --- a/packages/volto/src/components/manage/Controlpanels/DatabaseInformation.jsx +++ b/packages/volto/src/components/manage/Controlpanels/DatabaseInformation.jsx @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { Link } from 'react-router-dom'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { Container, Divider, Message, Segment, Table } from 'semantic-ui-react'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; @@ -216,8 +216,8 @@ class DatabaseInformation extends Component { - {this.state.isClient && ( - + {this.state.isClient && + createPortal( } - /> - - )} + />, + document.getElementById('toolbar'), + )} ) : null; } diff --git a/packages/volto/src/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx b/packages/volto/src/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx index fb4a4aac0e..97fcb5cb27 100644 --- a/packages/volto/src/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Groups/GroupsControlpanel.jsx @@ -37,7 +37,7 @@ import { find, map, pull } from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; -import { Portal } from 'react-portal'; +import { createPortal } from 'react-dom'; import { connect } from 'react-redux'; import { toast } from 'react-toastify'; @@ -279,16 +279,16 @@ class GroupsControlpanel extends Component { entry.id === name && !entry.roles.includes(value) ? [...entry.roles, value] : entry.id !== name - ? entry.roles - : pull(entry.roles, value), + ? entry.roles + : pull(entry.roles, value), })), authenticatedRole: name === 'AuthenticatedUsers' && !prevState.authenticatedRole.includes(value) ? [...prevState.authenticatedRole, value] : name !== 'AuthenticatedUsers' - ? prevState.authenticatedRole - : pull(prevState.authenticatedRole, value), + ? prevState.authenticatedRole + : pull(prevState.authenticatedRole, value), })); } /** @@ -598,8 +598,8 @@ class GroupsControlpanel extends Component {
- {this.state.isClient && ( - + {this.state.isClient && + createPortal( } - /> - - )} + />, + document.getElementById('toolbar'), + )} ); } diff --git a/packages/volto/src/components/manage/Controlpanels/Groups/GroupsControlpanel.test.jsx b/packages/volto/src/components/manage/Controlpanels/Groups/GroupsControlpanel.test.jsx index f520c37a07..faf5c4f04e 100644 --- a/packages/volto/src/components/manage/Controlpanels/Groups/GroupsControlpanel.test.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Groups/GroupsControlpanel.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { render } from '@testing-library/react'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; import jwt from 'jsonwebtoken'; @@ -7,9 +7,8 @@ import jwt from 'jsonwebtoken'; import GroupsControlpanel from './GroupsControlpanel'; const mockStore = configureStore(); -jest.mock('react-portal', () => ({ - Portal: jest.fn(() =>
), -})); +jest.mock('../../Toolbar/Toolbar', () => jest.fn(() =>
)); + describe('UsersControlpanel', () => { it('renders a user control component', () => { const store = mockStore({ @@ -37,12 +36,13 @@ describe('UsersControlpanel', () => { messages: {}, }, }); - const component = renderer.create( + const { container } = render( +
, ); - const json = component.toJSON(); - expect(json).toMatchSnapshot(); + + expect(container).toMatchSnapshot(); }); }); diff --git a/packages/volto/src/components/manage/Controlpanels/Groups/__snapshots__/GroupsControlpanel.test.jsx.snap b/packages/volto/src/components/manage/Controlpanels/Groups/__snapshots__/GroupsControlpanel.test.jsx.snap index 6a7e41b64b..36ba594690 100644 --- a/packages/volto/src/components/manage/Controlpanels/Groups/__snapshots__/GroupsControlpanel.test.jsx.snap +++ b/packages/volto/src/components/manage/Controlpanels/Groups/__snapshots__/GroupsControlpanel.test.jsx.snap @@ -1,128 +1,121 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UsersControlpanel renders a user control component 1`] = ` -
+
-
- Groups -
-
- Groups are logical collections of users, such as departments and business units. Groups are not directly related to permissions on a global level, you normally use Roles for that - and let certain Groups have a particular role. The symbol - plone-svgundefined", - } - } - onClick={null} - style={ - Object { - "fill": "#007EB1", - "height": "20px", - "width": "auto", - } - } - viewBox="" - xmlns="" - /> - indicates a role inherited from membership in another group. -
+ class="container" + />
-
-
+
+ Groups are logical collections of users, such as departments and business units. Groups are not directly related to permissions on a global level, you normally use Roles for that - and let certain Groups have a particular role. The symbol + + + plone-svg + + undefined + + indicates a role inherited from membership in another group. +
+
+
- -