diff --git a/.circleci/config.yml b/.circleci/config.yml index 9569a4fea6..41f32f1b68 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,34 +3,34 @@ version: 2.1 aliases: - &workspace_root ~/neos-ui-workspace - &store_yarn_package_cache - key: yarn-cache-v{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }} + key: yarn-cache-v{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ checksum "yarn.lock" }} paths: - ~/.cache/yarn - &restore_yarn_package_cache keys: - - yarn-cache-v{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }} + - yarn-cache-v{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ checksum "yarn.lock" }} - &store_app_cache - key: full-app-cache-v{{ .Environment.CACHE_VERSION }}-{{ arch }}-{{ checksum "Tests/IntegrationTests/TestDistribution/composer.json" }}-{{ checksum "Tests/IntegrationTests/TestDistribution/Configuration/Settings.yaml" }} + key: full-app-cache-v{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ arch }}-{{ checksum "Tests/IntegrationTests/TestDistribution/composer.json" }}-{{ checksum "Tests/IntegrationTests/TestDistribution/Configuration/Settings.yaml" }} paths: - /home/circleci/app - &restore_app_cache keys: - - full-app-cache-v{{ .Environment.CACHE_VERSION }}-{{ arch }}-{{ checksum "Tests/IntegrationTests/TestDistribution/composer.json" }}-{{ checksum "Tests/IntegrationTests/TestDistribution/Configuration/Settings.yaml" }} - - full-app-cache-v{{ .Environment.CACHE_VERSION }}-{{ arch }}-{{ checksum "Tests/IntegrationTests/TestDistribution/composer.json" }}- - - full-app-cache-v{{ .Environment.CACHE_VERSION }}-{{ arch }}- + - full-app-cache-v{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ arch }}-{{ checksum "Tests/IntegrationTests/TestDistribution/composer.json" }}-{{ checksum "Tests/IntegrationTests/TestDistribution/Configuration/Settings.yaml" }} + - full-app-cache-v{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ arch }}-{{ checksum "Tests/IntegrationTests/TestDistribution/composer.json" }}- + - full-app-cache-v{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ arch }}- - &save_composer_cache - key: composer-cache-v{{ .Environment.CACHE_VERSION }}-{{ arch }}-{{ checksum "Tests/IntegrationTests/TestDistribution/composer.json" }} + key: composer-cache-v{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ arch }}-{{ checksum "Tests/IntegrationTests/TestDistribution/composer.json" }} paths: - /home/circleci/composer/cache-dir - &restore_composer_cache keys: - - composer-cache-v{{ .Environment.CACHE_VERSION }}-{{ arch }}-{{ checksum "Tests/IntegrationTests/TestDistribution/composer.json" }} - - composer-cache-v{{ .Environment.CACHE_VERSION }}-{{ arch }}- + - composer-cache-v{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ arch }}-{{ checksum "Tests/IntegrationTests/TestDistribution/composer.json" }} + - composer-cache-v{{ .Environment.CIRCLE_WORKFLOW_ID }}-{{ arch }}- - &attach_workspace at: *workspace_root @@ -84,6 +84,7 @@ jobs: e2e: environment: FLOW_CONTEXT: Production + DB_HOST: 127.0.0.1 docker: - image: cimg/php:8.2-node - image: cimg/mariadb:10.6 @@ -113,7 +114,8 @@ jobs: ./flow flow:cache:flush ./flow flow:cache:warmup ./flow doctrine:migrate - ./flow user:create --username=admin --password=password --first-name=John --last-name=Doe --roles=Administrator + ./flow user:create --username=admin --password=admin --first-name=Admin --last-name=Admington --roles=Administrator + ./flow user:create --username=editor --password=editor --first-name=Editor --last-name=McEditworth --roles=Editor - run: name: Start flow server command: /home/circleci/app/flow server:run --port 8081 @@ -150,6 +152,8 @@ jobs: cd /home/circleci/app/Packages/Application/Neos.Neos.Ui nvm install nvm use + echo 127.0.0.1 onedimension.localhost | sudo tee -a /etc/hosts + echo 127.0.0.1 twodimensions.localhost | sudo tee -a /etc/hosts make test-e2e-saucelabs - store_artifacts: path: /home/circleci/app/Data/Logs @@ -178,6 +182,20 @@ jobs: cd /home/circleci/app/ bin/phpunit -c Build/BuildEssentials/PhpUnit/UnitTests.xml Packages/Application/Neos.Neos.Ui/Tests/Unit + php-linting: + docker: + - image: cimg/php:8.2-node + working_directory: *workspace_root + steps: + - attach_workspace: *attach_workspace + - restore_cache: *restore_app_cache + + - run: rm -rf /home/circleci/app/Packages/Application/Neos.Neos.Ui + - run: cd /home/circleci/app/Packages/Application && mv ~/neos-ui-workspace Neos.Neos.Ui + - run: | + cd /home/circleci/app/Packages/Application/Neos.Neos.Ui + composer run lint:phpstan + workflows: version: 2 build_and_test: @@ -199,3 +217,6 @@ workflows: - php-unittests: requires: - build_flow_app + - php-linting: + requires: + - build_flow_app diff --git a/.editorconfig b/.editorconfig index d67e637dc1..4679fd309f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,7 @@ trim_trailing_whitespace = true indent_style = space indent_size = 4 -[*.{yml,yaml,json}] +[*.{yml,yaml,json,xlf}] indent_size = 2 [*.md] diff --git a/.eslintrc.js b/.eslintrc.js index cfff665808..b25c1fd719 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,6 +30,9 @@ module.exports = { 'default-case': 'off', 'no-mixed-operators': 'off', 'no-negated-condition': 'off', - 'complexity': 'off' + 'complexity': 'off', + + // This rule would prevent us from implementing meaningful value objects + 'no-useless-constructor': 'off' }, } diff --git a/.github/workflows/add-pr-labels.yml b/.github/workflows/add-pr-labels.yml index 1c492a3dd0..68add971b3 100644 --- a/.github/workflows/add-pr-labels.yml +++ b/.github/workflows/add-pr-labels.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Maybe remove base branch label diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 0e7ba9da77..9761c1120e 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -12,8 +12,8 @@ jobs: runs-on: ubuntu-latest name: 'Code style' steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'yarn' @@ -26,8 +26,8 @@ jobs: runs-on: ubuntu-latest name: 'Unit tests' steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'yarn' diff --git a/.sauce/config1Dimension.yml b/.sauce/config.yml similarity index 63% rename from .sauce/config1Dimension.yml rename to .sauce/config.yml index 48bd751dd2..5d836840ea 100644 --- a/.sauce/config1Dimension.yml +++ b/.sauce/config.yml @@ -18,19 +18,19 @@ testcafe: # Controls what files are available in the context of a test run (unless explicitly excluded by .sauceignore). rootDir: ./ suites: - - name: "One dimensional Tests in Firefox on Windows" + - name: "Tests in Firefox on Windows" browserName: "firefox" src: - - "Tests/IntegrationTests/Fixtures/1Dimension/*.e2e.js" + - "Tests/IntegrationTests/Fixtures/*/*.e2e.js" platformName: "Windows 10" screenResolution: "1280x1024" - - name: "One dimensional Tests in Firefox on MacOS" - # todo use chrome here and fix ci https://github.com/neos/neos-ui/issues/3591 - browserName: "firefox" - src: - - "Tests/IntegrationTests/Fixtures/1Dimension/*.e2e.js" - platformName: "macOS 13" - screenResolution: "1440x900" + # todo use chrome here and fix ci https://github.com/neos/neos-ui/issues/3591 (but even firefox fails in ci) + # - name: "Tests in Firefox on MacOS" + # browserName: "firefox" + # src: + # - "Tests/IntegrationTests/Fixtures/*/*.e2e.js" + # platformName: "macOS 13" + # screenResolution: "1440x900" npm: dependencies: - testcafe-react-selectors @@ -39,7 +39,7 @@ npm: artifacts: download: match: - - neosui-two-dimensional-test-report.json + - neosui-test-report.json - console.log - sauce-test-report.json when: always @@ -49,4 +49,4 @@ artifacts: reporters: json: enabled: true - filename: neosui-one-dimensional-test-report.json + filename: neosui-test-report.json diff --git a/.sauce/config2Dimension.yml b/.sauce/config2Dimension.yml deleted file mode 100644 index 80ebb74ad7..0000000000 --- a/.sauce/config2Dimension.yml +++ /dev/null @@ -1,49 +0,0 @@ -apiVersion: v1alpha -kind: testcafe -sauce: - region: us-west-1 - concurrency: 1 # Controls how many suites are executed at the same time. - retries: 0 - metadata: - tags: - - e2e - - $TARGET_BRANCH - build: $TARGET_BRANCH - tunnel: - name: "circleci-tunnel" -testcafe: - version: 3.6.2 -# Controls what files are available in the context of a test run (unless explicitly excluded by .sauceignore). -rootDir: ./ -suites: - - name: "Two dimensional Tests in Firefox on Windows" - browserName: "firefox" - src: - - "Tests/IntegrationTests/Fixtures/2Dimension/*.e2e.js" - platformName: "Windows 10" - screenResolution: "1280x1024" - - name: "Two dimensional Tests in Chrome on MacOS" - browserName: "chrome" - src: - - "Tests/IntegrationTests/Fixtures/2Dimension/*.e2e.js" - platformName: "macOS 13" - screenResolution: "1440x900" -npm: - dependencies: - - testcafe-react-selectors - -# Controls what artifacts to fetch when the suites have finished. -artifacts: - download: - match: - - neosui-two-dimensional-test-report.json - - console.log - - sauce-test-report.json - when: always - allAttempts: true - directory: ../../Data/Logs/saucelabs-artifacts/ - -reporters: - json: - enabled: true - filename: neosui-two-dimensional-test-report.json diff --git a/.yarn/cache/@ckeditor-ckeditor5-adapter-ckfinder-npm-44.0.0-06e6bdb665-a7152c7f11.zip b/.yarn/cache/@ckeditor-ckeditor5-adapter-ckfinder-npm-44.0.0-06e6bdb665-a7152c7f11.zip new file mode 100644 index 0000000000..23a5256b5c Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-adapter-ckfinder-npm-44.0.0-06e6bdb665-a7152c7f11.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-alignment-npm-16.0.0-c065b19ff9-e7e792f431.zip b/.yarn/cache/@ckeditor-ckeditor5-alignment-npm-16.0.0-c065b19ff9-e7e792f431.zip deleted file mode 100644 index c01d4745a7..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-alignment-npm-16.0.0-c065b19ff9-e7e792f431.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-alignment-npm-44.0.0-1466eb6b78-d8ae68918c.zip b/.yarn/cache/@ckeditor-ckeditor5-alignment-npm-44.0.0-1466eb6b78-d8ae68918c.zip new file mode 100644 index 0000000000..3f929b1f7f Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-alignment-npm-44.0.0-1466eb6b78-d8ae68918c.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-autoformat-npm-44.0.0-af7fbce98a-9b531d4d20.zip b/.yarn/cache/@ckeditor-ckeditor5-autoformat-npm-44.0.0-af7fbce98a-9b531d4d20.zip new file mode 100644 index 0000000000..6a24e6995d Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-autoformat-npm-44.0.0-af7fbce98a-9b531d4d20.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-autosave-npm-44.0.0-018978c1c2-aef6b9b483.zip b/.yarn/cache/@ckeditor-ckeditor5-autosave-npm-44.0.0-018978c1c2-aef6b9b483.zip new file mode 100644 index 0000000000..cc7fbd379f Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-autosave-npm-44.0.0-018978c1c2-aef6b9b483.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-basic-styles-npm-16.0.0-24cf5bb434-570c104c7c.zip b/.yarn/cache/@ckeditor-ckeditor5-basic-styles-npm-16.0.0-24cf5bb434-570c104c7c.zip deleted file mode 100644 index 4d89bd9399..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-basic-styles-npm-16.0.0-24cf5bb434-570c104c7c.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-basic-styles-npm-44.0.0-5fa1ba8b7f-5d8de19262.zip b/.yarn/cache/@ckeditor-ckeditor5-basic-styles-npm-44.0.0-5fa1ba8b7f-5d8de19262.zip new file mode 100644 index 0000000000..ddba83b8f3 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-basic-styles-npm-44.0.0-5fa1ba8b7f-5d8de19262.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-block-quote-npm-44.0.0-e923f8a2bd-5359e78317.zip b/.yarn/cache/@ckeditor-ckeditor5-block-quote-npm-44.0.0-e923f8a2bd-5359e78317.zip new file mode 100644 index 0000000000..b5f6208582 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-block-quote-npm-44.0.0-e923f8a2bd-5359e78317.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-bookmark-npm-44.0.0-0cbb5555b0-8946df8297.zip b/.yarn/cache/@ckeditor-ckeditor5-bookmark-npm-44.0.0-0cbb5555b0-8946df8297.zip new file mode 100644 index 0000000000..9cff3d5d63 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-bookmark-npm-44.0.0-0cbb5555b0-8946df8297.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-ckbox-npm-44.0.0-398eb95df8-6dcfe9936a.zip b/.yarn/cache/@ckeditor-ckeditor5-ckbox-npm-44.0.0-398eb95df8-6dcfe9936a.zip new file mode 100644 index 0000000000..d562b6a859 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-ckbox-npm-44.0.0-398eb95df8-6dcfe9936a.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-ckfinder-npm-44.0.0-0ca7a6bf52-098a117ced.zip b/.yarn/cache/@ckeditor-ckeditor5-ckfinder-npm-44.0.0-0ca7a6bf52-098a117ced.zip new file mode 100644 index 0000000000..581dff2e22 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-ckfinder-npm-44.0.0-0ca7a6bf52-098a117ced.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-clipboard-npm-16.0.0-523adcb299-f42e5bc235.zip b/.yarn/cache/@ckeditor-ckeditor5-clipboard-npm-16.0.0-523adcb299-f42e5bc235.zip deleted file mode 100644 index 120ee56261..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-clipboard-npm-16.0.0-523adcb299-f42e5bc235.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-clipboard-npm-44.0.0-bc736f7695-519da043c8.zip b/.yarn/cache/@ckeditor-ckeditor5-clipboard-npm-44.0.0-bc736f7695-519da043c8.zip new file mode 100644 index 0000000000..82e1b5e95f Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-clipboard-npm-44.0.0-bc736f7695-519da043c8.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-cloud-services-npm-44.0.0-92be699ee0-95554228d0.zip b/.yarn/cache/@ckeditor-ckeditor5-cloud-services-npm-44.0.0-92be699ee0-95554228d0.zip new file mode 100644 index 0000000000..1e61cf73ef Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-cloud-services-npm-44.0.0-92be699ee0-95554228d0.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-code-block-npm-44.0.0-3ab798922b-b294468a98.zip b/.yarn/cache/@ckeditor-ckeditor5-code-block-npm-44.0.0-3ab798922b-b294468a98.zip new file mode 100644 index 0000000000..2bb39cdabf Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-code-block-npm-44.0.0-3ab798922b-b294468a98.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-core-npm-16.0.0-6573ba5935-588634f0e2.zip b/.yarn/cache/@ckeditor-ckeditor5-core-npm-16.0.0-6573ba5935-588634f0e2.zip deleted file mode 100644 index 91ea4b1b1d..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-core-npm-16.0.0-6573ba5935-588634f0e2.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-core-npm-44.0.0-a277165cd5-f995ef7713.zip b/.yarn/cache/@ckeditor-ckeditor5-core-npm-44.0.0-a277165cd5-f995ef7713.zip new file mode 100644 index 0000000000..18366d1f34 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-core-npm-44.0.0-a277165cd5-f995ef7713.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-easy-image-npm-44.0.0-cd84bf737e-3ca8ec0871.zip b/.yarn/cache/@ckeditor-ckeditor5-easy-image-npm-44.0.0-cd84bf737e-3ca8ec0871.zip new file mode 100644 index 0000000000..857fff5da4 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-easy-image-npm-44.0.0-cd84bf737e-3ca8ec0871.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-editor-balloon-npm-44.0.0-e1b8642ccd-6031e44b01.zip b/.yarn/cache/@ckeditor-ckeditor5-editor-balloon-npm-44.0.0-e1b8642ccd-6031e44b01.zip new file mode 100644 index 0000000000..e52292b48d Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-editor-balloon-npm-44.0.0-e1b8642ccd-6031e44b01.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-editor-classic-npm-44.0.0-288eefc34e-3b9fc1b6eb.zip b/.yarn/cache/@ckeditor-ckeditor5-editor-classic-npm-44.0.0-288eefc34e-3b9fc1b6eb.zip new file mode 100644 index 0000000000..cdf6c439f8 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-editor-classic-npm-44.0.0-288eefc34e-3b9fc1b6eb.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-editor-decoupled-npm-16.0.0-cd3a0321b9-c8f4aa63b5.zip b/.yarn/cache/@ckeditor-ckeditor5-editor-decoupled-npm-16.0.0-cd3a0321b9-c8f4aa63b5.zip deleted file mode 100644 index 4d03ec2fe8..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-editor-decoupled-npm-16.0.0-cd3a0321b9-c8f4aa63b5.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-editor-decoupled-npm-44.0.0-cfae0546fd-43eb4565eb.zip b/.yarn/cache/@ckeditor-ckeditor5-editor-decoupled-npm-44.0.0-cfae0546fd-43eb4565eb.zip new file mode 100644 index 0000000000..4a2c45aa61 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-editor-decoupled-npm-44.0.0-cfae0546fd-43eb4565eb.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-editor-inline-npm-44.0.0-646553a1ee-de225a0170.zip b/.yarn/cache/@ckeditor-ckeditor5-editor-inline-npm-44.0.0-646553a1ee-de225a0170.zip new file mode 100644 index 0000000000..20aedae1c8 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-editor-inline-npm-44.0.0-646553a1ee-de225a0170.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-editor-multi-root-npm-44.0.0-ccf88420cb-29acd69963.zip b/.yarn/cache/@ckeditor-ckeditor5-editor-multi-root-npm-44.0.0-ccf88420cb-29acd69963.zip new file mode 100644 index 0000000000..6444d3d5ad Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-editor-multi-root-npm-44.0.0-ccf88420cb-29acd69963.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-engine-npm-16.0.0-4b05aefe15-cabbd74051.zip b/.yarn/cache/@ckeditor-ckeditor5-engine-npm-16.0.0-4b05aefe15-cabbd74051.zip deleted file mode 100644 index 2533ae40f4..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-engine-npm-16.0.0-4b05aefe15-cabbd74051.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-engine-npm-44.0.0-b1d8052981-dc390f3bb2.zip b/.yarn/cache/@ckeditor-ckeditor5-engine-npm-44.0.0-b1d8052981-dc390f3bb2.zip new file mode 100644 index 0000000000..9703bbb84f Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-engine-npm-44.0.0-b1d8052981-dc390f3bb2.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-engine-patch-dfc8c266c9-0ca241d6d2.zip b/.yarn/cache/@ckeditor-ckeditor5-engine-patch-dfc8c266c9-0ca241d6d2.zip deleted file mode 100644 index 303f1a6d5e..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-engine-patch-dfc8c266c9-0ca241d6d2.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-enter-npm-16.0.0-ca8ebb6106-81c440cc6b.zip b/.yarn/cache/@ckeditor-ckeditor5-enter-npm-16.0.0-ca8ebb6106-81c440cc6b.zip deleted file mode 100644 index 993ece72be..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-enter-npm-16.0.0-ca8ebb6106-81c440cc6b.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-enter-npm-44.0.0-0092e6b801-b1d74d438d.zip b/.yarn/cache/@ckeditor-ckeditor5-enter-npm-44.0.0-0092e6b801-b1d74d438d.zip new file mode 100644 index 0000000000..cb70ed9a5a Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-enter-npm-44.0.0-0092e6b801-b1d74d438d.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-essentials-npm-16.0.0-d4010e91c7-9aa445f70a.zip b/.yarn/cache/@ckeditor-ckeditor5-essentials-npm-16.0.0-d4010e91c7-9aa445f70a.zip deleted file mode 100644 index 4b4d0f52cf..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-essentials-npm-16.0.0-d4010e91c7-9aa445f70a.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-essentials-npm-44.0.0-fefced513a-5474a35743.zip b/.yarn/cache/@ckeditor-ckeditor5-essentials-npm-44.0.0-fefced513a-5474a35743.zip new file mode 100644 index 0000000000..27d0798028 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-essentials-npm-44.0.0-fefced513a-5474a35743.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-find-and-replace-npm-44.0.0-0ebd3fe9ca-00695b6884.zip b/.yarn/cache/@ckeditor-ckeditor5-find-and-replace-npm-44.0.0-0ebd3fe9ca-00695b6884.zip new file mode 100644 index 0000000000..e8bec202ae Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-find-and-replace-npm-44.0.0-0ebd3fe9ca-00695b6884.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-font-npm-44.0.0-eade3ec6dc-012071bf3c.zip b/.yarn/cache/@ckeditor-ckeditor5-font-npm-44.0.0-eade3ec6dc-012071bf3c.zip new file mode 100644 index 0000000000..2a86d5ab4d Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-font-npm-44.0.0-eade3ec6dc-012071bf3c.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-heading-npm-16.0.0-f1390fdeeb-e2ed0bc9e2.zip b/.yarn/cache/@ckeditor-ckeditor5-heading-npm-16.0.0-f1390fdeeb-e2ed0bc9e2.zip deleted file mode 100644 index a794f70dbc..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-heading-npm-16.0.0-f1390fdeeb-e2ed0bc9e2.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-heading-npm-44.0.0-f0f3fe8235-df5b30520d.zip b/.yarn/cache/@ckeditor-ckeditor5-heading-npm-44.0.0-f0f3fe8235-df5b30520d.zip new file mode 100644 index 0000000000..7ded9a06b9 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-heading-npm-44.0.0-f0f3fe8235-df5b30520d.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-highlight-npm-16.0.0-bbd766a72b-6b1fcfa52d.zip b/.yarn/cache/@ckeditor-ckeditor5-highlight-npm-16.0.0-bbd766a72b-6b1fcfa52d.zip deleted file mode 100644 index 0d7a13c1b8..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-highlight-npm-16.0.0-bbd766a72b-6b1fcfa52d.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-highlight-npm-44.0.0-57d1e7a6c2-837acbf7ac.zip b/.yarn/cache/@ckeditor-ckeditor5-highlight-npm-44.0.0-57d1e7a6c2-837acbf7ac.zip new file mode 100644 index 0000000000..cdc49c76a2 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-highlight-npm-44.0.0-57d1e7a6c2-837acbf7ac.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-horizontal-line-npm-44.0.0-f622256307-579b119bb7.zip b/.yarn/cache/@ckeditor-ckeditor5-horizontal-line-npm-44.0.0-f622256307-579b119bb7.zip new file mode 100644 index 0000000000..06b2b10aba Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-horizontal-line-npm-44.0.0-f622256307-579b119bb7.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-html-embed-npm-44.0.0-8fdc5a4cf7-2df621b0ab.zip b/.yarn/cache/@ckeditor-ckeditor5-html-embed-npm-44.0.0-8fdc5a4cf7-2df621b0ab.zip new file mode 100644 index 0000000000..3c38733383 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-html-embed-npm-44.0.0-8fdc5a4cf7-2df621b0ab.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-html-support-npm-44.0.0-dcff546e9f-ed4a5cade2.zip b/.yarn/cache/@ckeditor-ckeditor5-html-support-npm-44.0.0-dcff546e9f-ed4a5cade2.zip new file mode 100644 index 0000000000..2b70684741 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-html-support-npm-44.0.0-dcff546e9f-ed4a5cade2.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-image-npm-44.0.0-b2fbf99cd5-b6bd38314e.zip b/.yarn/cache/@ckeditor-ckeditor5-image-npm-44.0.0-b2fbf99cd5-b6bd38314e.zip new file mode 100644 index 0000000000..5a2d7dfb10 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-image-npm-44.0.0-b2fbf99cd5-b6bd38314e.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-indent-npm-44.0.0-66802edb3b-8509afec86.zip b/.yarn/cache/@ckeditor-ckeditor5-indent-npm-44.0.0-66802edb3b-8509afec86.zip new file mode 100644 index 0000000000..274b0a4841 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-indent-npm-44.0.0-66802edb3b-8509afec86.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-language-npm-44.0.0-a324fec302-c38d5003b2.zip b/.yarn/cache/@ckeditor-ckeditor5-language-npm-44.0.0-a324fec302-c38d5003b2.zip new file mode 100644 index 0000000000..9961a2472c Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-language-npm-44.0.0-a324fec302-c38d5003b2.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-link-npm-16.0.0-0ccecccfc8-cb9bc3aa6d.zip b/.yarn/cache/@ckeditor-ckeditor5-link-npm-16.0.0-0ccecccfc8-cb9bc3aa6d.zip deleted file mode 100644 index 4fd27b0395..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-link-npm-16.0.0-0ccecccfc8-cb9bc3aa6d.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-link-npm-44.0.0-f5931795e4-7442bb38b2.zip b/.yarn/cache/@ckeditor-ckeditor5-link-npm-44.0.0-f5931795e4-7442bb38b2.zip new file mode 100644 index 0000000000..204f6a2a71 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-link-npm-44.0.0-f5931795e4-7442bb38b2.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-list-npm-16.0.0-25266495d8-2f5de21fe1.zip b/.yarn/cache/@ckeditor-ckeditor5-list-npm-16.0.0-25266495d8-2f5de21fe1.zip deleted file mode 100644 index 6878148449..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-list-npm-16.0.0-25266495d8-2f5de21fe1.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-list-npm-44.0.0-74cefda3c5-8884017ef4.zip b/.yarn/cache/@ckeditor-ckeditor5-list-npm-44.0.0-74cefda3c5-8884017ef4.zip new file mode 100644 index 0000000000..2ae0b312ee Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-list-npm-44.0.0-74cefda3c5-8884017ef4.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-markdown-gfm-npm-44.0.0-50d395eb14-a9942258b8.zip b/.yarn/cache/@ckeditor-ckeditor5-markdown-gfm-npm-44.0.0-50d395eb14-a9942258b8.zip new file mode 100644 index 0000000000..b4150c9804 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-markdown-gfm-npm-44.0.0-50d395eb14-a9942258b8.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-media-embed-npm-44.0.0-f83276917d-1caaf1a482.zip b/.yarn/cache/@ckeditor-ckeditor5-media-embed-npm-44.0.0-f83276917d-1caaf1a482.zip new file mode 100644 index 0000000000..cd4c5c3ca4 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-media-embed-npm-44.0.0-f83276917d-1caaf1a482.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-mention-npm-44.0.0-300e461066-83b7563002.zip b/.yarn/cache/@ckeditor-ckeditor5-mention-npm-44.0.0-300e461066-83b7563002.zip new file mode 100644 index 0000000000..c5408508ea Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-mention-npm-44.0.0-300e461066-83b7563002.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-minimap-npm-44.0.0-d49243d9e8-36a328dc55.zip b/.yarn/cache/@ckeditor-ckeditor5-minimap-npm-44.0.0-d49243d9e8-36a328dc55.zip new file mode 100644 index 0000000000..84a7abd7ea Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-minimap-npm-44.0.0-d49243d9e8-36a328dc55.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-page-break-npm-44.0.0-6de4476717-1e6ccdaf2a.zip b/.yarn/cache/@ckeditor-ckeditor5-page-break-npm-44.0.0-6de4476717-1e6ccdaf2a.zip new file mode 100644 index 0000000000..7711802dfb Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-page-break-npm-44.0.0-6de4476717-1e6ccdaf2a.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-paragraph-npm-16.0.0-111e77648f-ba0e81e429.zip b/.yarn/cache/@ckeditor-ckeditor5-paragraph-npm-16.0.0-111e77648f-ba0e81e429.zip deleted file mode 100644 index 9031cbbf96..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-paragraph-npm-16.0.0-111e77648f-ba0e81e429.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-paragraph-npm-44.0.0-95e655df0b-7ba641e214.zip b/.yarn/cache/@ckeditor-ckeditor5-paragraph-npm-44.0.0-95e655df0b-7ba641e214.zip new file mode 100644 index 0000000000..c97cb6a589 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-paragraph-npm-44.0.0-95e655df0b-7ba641e214.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-paste-from-office-npm-44.0.0-cf9c16e8c6-2b7e59cd86.zip b/.yarn/cache/@ckeditor-ckeditor5-paste-from-office-npm-44.0.0-cf9c16e8c6-2b7e59cd86.zip new file mode 100644 index 0000000000..98b06f4ff3 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-paste-from-office-npm-44.0.0-cf9c16e8c6-2b7e59cd86.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-remove-format-npm-16.0.0-26d007494b-4eab5c1115.zip b/.yarn/cache/@ckeditor-ckeditor5-remove-format-npm-16.0.0-26d007494b-4eab5c1115.zip deleted file mode 100644 index 0be993ebee..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-remove-format-npm-16.0.0-26d007494b-4eab5c1115.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-remove-format-npm-44.0.0-6f3038ce15-e1e13a5812.zip b/.yarn/cache/@ckeditor-ckeditor5-remove-format-npm-44.0.0-6f3038ce15-e1e13a5812.zip new file mode 100644 index 0000000000..0bd46ae92e Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-remove-format-npm-44.0.0-6f3038ce15-e1e13a5812.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-restricted-editing-npm-44.0.0-d7763f8176-9113642f36.zip b/.yarn/cache/@ckeditor-ckeditor5-restricted-editing-npm-44.0.0-d7763f8176-9113642f36.zip new file mode 100644 index 0000000000..8bf07d3778 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-restricted-editing-npm-44.0.0-d7763f8176-9113642f36.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-select-all-npm-44.0.0-954ae702fb-74785ddd6c.zip b/.yarn/cache/@ckeditor-ckeditor5-select-all-npm-44.0.0-954ae702fb-74785ddd6c.zip new file mode 100644 index 0000000000..19d7dc7e34 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-select-all-npm-44.0.0-954ae702fb-74785ddd6c.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-show-blocks-npm-44.0.0-03c59fd46e-7f9554fa34.zip b/.yarn/cache/@ckeditor-ckeditor5-show-blocks-npm-44.0.0-03c59fd46e-7f9554fa34.zip new file mode 100644 index 0000000000..f5a0d2a3af Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-show-blocks-npm-44.0.0-03c59fd46e-7f9554fa34.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-source-editing-npm-44.0.0-e8706fd42a-d9706147e7.zip b/.yarn/cache/@ckeditor-ckeditor5-source-editing-npm-44.0.0-e8706fd42a-d9706147e7.zip new file mode 100644 index 0000000000..81ed4229cc Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-source-editing-npm-44.0.0-e8706fd42a-d9706147e7.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-special-characters-npm-44.0.0-8fc2af7927-8c6da91d2a.zip b/.yarn/cache/@ckeditor-ckeditor5-special-characters-npm-44.0.0-8fc2af7927-8c6da91d2a.zip new file mode 100644 index 0000000000..04c9462d10 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-special-characters-npm-44.0.0-8fc2af7927-8c6da91d2a.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-style-npm-44.0.0-482c093e94-f0e48b9ed6.zip b/.yarn/cache/@ckeditor-ckeditor5-style-npm-44.0.0-482c093e94-f0e48b9ed6.zip new file mode 100644 index 0000000000..0bb13b6e0d Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-style-npm-44.0.0-482c093e94-f0e48b9ed6.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-table-npm-16.0.0-c8773a0e8f-bcc9844d36.zip b/.yarn/cache/@ckeditor-ckeditor5-table-npm-16.0.0-c8773a0e8f-bcc9844d36.zip deleted file mode 100644 index 526e650b41..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-table-npm-16.0.0-c8773a0e8f-bcc9844d36.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-table-npm-44.0.0-7ee559783d-c9fcfc361b.zip b/.yarn/cache/@ckeditor-ckeditor5-table-npm-44.0.0-7ee559783d-c9fcfc361b.zip new file mode 100644 index 0000000000..5eaffbfe13 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-table-npm-44.0.0-7ee559783d-c9fcfc361b.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-theme-lark-npm-44.0.0-a19a8fe4ef-881efb5572.zip b/.yarn/cache/@ckeditor-ckeditor5-theme-lark-npm-44.0.0-a19a8fe4ef-881efb5572.zip new file mode 100644 index 0000000000..fbdf04348c Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-theme-lark-npm-44.0.0-a19a8fe4ef-881efb5572.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-typing-npm-16.0.0-ade680e35b-bc7e8f9414.zip b/.yarn/cache/@ckeditor-ckeditor5-typing-npm-16.0.0-ade680e35b-bc7e8f9414.zip deleted file mode 100644 index 6b1f71cffa..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-typing-npm-16.0.0-ade680e35b-bc7e8f9414.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-typing-npm-44.0.0-b7c12d84ce-9bc93cadb1.zip b/.yarn/cache/@ckeditor-ckeditor5-typing-npm-44.0.0-b7c12d84ce-9bc93cadb1.zip new file mode 100644 index 0000000000..5de4c0109d Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-typing-npm-44.0.0-b7c12d84ce-9bc93cadb1.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-ui-npm-16.0.0-14310916bb-8b9bd6c059.zip b/.yarn/cache/@ckeditor-ckeditor5-ui-npm-16.0.0-14310916bb-8b9bd6c059.zip deleted file mode 100644 index 9ae1918c89..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-ui-npm-16.0.0-14310916bb-8b9bd6c059.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-ui-npm-44.0.0-c25f3d8204-08dac36ecb.zip b/.yarn/cache/@ckeditor-ckeditor5-ui-npm-44.0.0-c25f3d8204-08dac36ecb.zip new file mode 100644 index 0000000000..f788ee93ee Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-ui-npm-44.0.0-c25f3d8204-08dac36ecb.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-undo-npm-16.0.0-78776f4610-f74fd79809.zip b/.yarn/cache/@ckeditor-ckeditor5-undo-npm-16.0.0-78776f4610-f74fd79809.zip deleted file mode 100644 index 8b9d3ca82b..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-undo-npm-16.0.0-78776f4610-f74fd79809.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-undo-npm-44.0.0-8ba315bf1c-85701d2f56.zip b/.yarn/cache/@ckeditor-ckeditor5-undo-npm-44.0.0-8ba315bf1c-85701d2f56.zip new file mode 100644 index 0000000000..0a28e8ffe6 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-undo-npm-44.0.0-8ba315bf1c-85701d2f56.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-upload-npm-44.0.0-636a121e5f-a55fa3dd27.zip b/.yarn/cache/@ckeditor-ckeditor5-upload-npm-44.0.0-636a121e5f-a55fa3dd27.zip new file mode 100644 index 0000000000..e1cf479b05 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-upload-npm-44.0.0-636a121e5f-a55fa3dd27.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-utils-npm-16.0.0-dfecda191f-d35f1a98cc.zip b/.yarn/cache/@ckeditor-ckeditor5-utils-npm-16.0.0-dfecda191f-d35f1a98cc.zip deleted file mode 100644 index d94caaa1eb..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-utils-npm-16.0.0-dfecda191f-d35f1a98cc.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-utils-npm-44.0.0-f570b179e7-65282690fb.zip b/.yarn/cache/@ckeditor-ckeditor5-utils-npm-44.0.0-f570b179e7-65282690fb.zip new file mode 100644 index 0000000000..941f657614 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-utils-npm-44.0.0-f570b179e7-65282690fb.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-watchdog-npm-44.0.0-02a6a95e5c-d9ef810fcd.zip b/.yarn/cache/@ckeditor-ckeditor5-watchdog-npm-44.0.0-02a6a95e5c-d9ef810fcd.zip new file mode 100644 index 0000000000..42e161522c Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-watchdog-npm-44.0.0-02a6a95e5c-d9ef810fcd.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-widget-npm-16.0.0-e28dca433f-4802720cf8.zip b/.yarn/cache/@ckeditor-ckeditor5-widget-npm-16.0.0-e28dca433f-4802720cf8.zip deleted file mode 100644 index 6a550cd1fd..0000000000 Binary files a/.yarn/cache/@ckeditor-ckeditor5-widget-npm-16.0.0-e28dca433f-4802720cf8.zip and /dev/null differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-widget-npm-44.0.0-88f2e97636-8851dc0612.zip b/.yarn/cache/@ckeditor-ckeditor5-widget-npm-44.0.0-88f2e97636-8851dc0612.zip new file mode 100644 index 0000000000..34c368a517 Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-widget-npm-44.0.0-88f2e97636-8851dc0612.zip differ diff --git a/.yarn/cache/@ckeditor-ckeditor5-word-count-npm-44.0.0-2f1f41c158-12f01f06ea.zip b/.yarn/cache/@ckeditor-ckeditor5-word-count-npm-44.0.0-2f1f41c158-12f01f06ea.zip new file mode 100644 index 0000000000..52673d57ca Binary files /dev/null and b/.yarn/cache/@ckeditor-ckeditor5-word-count-npm-44.0.0-2f1f41c158-12f01f06ea.zip differ diff --git a/.yarn/cache/@mixmark-io-domino-npm-2.2.0-4e1d58551b-547af4a8f7.zip b/.yarn/cache/@mixmark-io-domino-npm-2.2.0-4e1d58551b-547af4a8f7.zip new file mode 100644 index 0000000000..60a67ca975 Binary files /dev/null and b/.yarn/cache/@mixmark-io-domino-npm-2.2.0-4e1d58551b-547af4a8f7.zip differ diff --git a/.yarn/cache/blurhash-npm-2.0.5-7648719b71-aa4d6855bb.zip b/.yarn/cache/blurhash-npm-2.0.5-7648719b71-aa4d6855bb.zip new file mode 100644 index 0000000000..899ca53a2c Binary files /dev/null and b/.yarn/cache/blurhash-npm-2.0.5-7648719b71-aa4d6855bb.zip differ diff --git a/.yarn/cache/ckeditor5-npm-44.0.0-6764714b6f-79db23a04e.zip b/.yarn/cache/ckeditor5-npm-44.0.0-6764714b6f-79db23a04e.zip new file mode 100644 index 0000000000..9981cd224e Binary files /dev/null and b/.yarn/cache/ckeditor5-npm-44.0.0-6764714b6f-79db23a04e.zip differ diff --git a/.yarn/cache/color-parse-npm-1.4.2-bd4a4dff72-3ed5916f87.zip b/.yarn/cache/color-parse-npm-1.4.2-bd4a4dff72-3ed5916f87.zip new file mode 100644 index 0000000000..467eed369c Binary files /dev/null and b/.yarn/cache/color-parse-npm-1.4.2-bd4a4dff72-3ed5916f87.zip differ diff --git a/.yarn/cache/cross-fetch-npm-4.0.0-9c67668db4-ecca4f37ff.zip b/.yarn/cache/cross-fetch-npm-4.0.0-9c67668db4-ecca4f37ff.zip new file mode 100644 index 0000000000..bddbef91a2 Binary files /dev/null and b/.yarn/cache/cross-fetch-npm-4.0.0-9c67668db4-ecca4f37ff.zip differ diff --git a/.yarn/cache/eslint-plugin-ckeditor5-rules-npm-0.0.2-7dd587c3b6-847b03300f.zip b/.yarn/cache/eslint-plugin-ckeditor5-rules-npm-0.0.2-7dd587c3b6-847b03300f.zip deleted file mode 100644 index 08f7452586..0000000000 Binary files a/.yarn/cache/eslint-plugin-ckeditor5-rules-npm-0.0.2-7dd587c3b6-847b03300f.zip and /dev/null differ diff --git a/.yarn/cache/immutable-npm-3.8.2-8bba11f18f-41909b3869.zip b/.yarn/cache/immutable-npm-3.8.2-8bba11f18f-41909b3869.zip deleted file mode 100644 index 045b0dc704..0000000000 Binary files a/.yarn/cache/immutable-npm-3.8.2-8bba11f18f-41909b3869.zip and /dev/null differ diff --git a/.yarn/cache/lodash.get-npm-4.4.2-7bda64ed87-e403047ddb.zip b/.yarn/cache/lodash.get-npm-4.4.2-7bda64ed87-e403047ddb.zip new file mode 100644 index 0000000000..63cd7ccfc2 Binary files /dev/null and b/.yarn/cache/lodash.get-npm-4.4.2-7bda64ed87-e403047ddb.zip differ diff --git a/.yarn/cache/marked-npm-4.0.12-1fc6e0ed31-7575117f85.zip b/.yarn/cache/marked-npm-4.0.12-1fc6e0ed31-7575117f85.zip new file mode 100644 index 0000000000..f149f56e58 Binary files /dev/null and b/.yarn/cache/marked-npm-4.0.12-1fc6e0ed31-7575117f85.zip differ diff --git a/.yarn/cache/node-fetch-npm-2.7.0-587d57004e-d76d2f5edb.zip b/.yarn/cache/node-fetch-npm-2.7.0-587d57004e-d76d2f5edb.zip new file mode 100644 index 0000000000..a067dc7b1c Binary files /dev/null and b/.yarn/cache/node-fetch-npm-2.7.0-587d57004e-d76d2f5edb.zip differ diff --git a/.yarn/cache/plow-js-npm-3.0.0-3448dc4b49-ad59f0014b.zip b/.yarn/cache/plow-js-npm-3.0.0-3448dc4b49-ad59f0014b.zip deleted file mode 100644 index abc1ae433c..0000000000 Binary files a/.yarn/cache/plow-js-npm-3.0.0-3448dc4b49-ad59f0014b.zip and /dev/null differ diff --git a/.yarn/cache/turndown-npm-7.2.0-35c69df4d5-a0bdf2fcc8.zip b/.yarn/cache/turndown-npm-7.2.0-35c69df4d5-a0bdf2fcc8.zip new file mode 100644 index 0000000000..d44b2c6b1c Binary files /dev/null and b/.yarn/cache/turndown-npm-7.2.0-35c69df4d5-a0bdf2fcc8.zip differ diff --git a/.yarn/cache/turndown-plugin-gfm-npm-1.0.2-73465b88af-18191dc18d.zip b/.yarn/cache/turndown-plugin-gfm-npm-1.0.2-73465b88af-18191dc18d.zip new file mode 100644 index 0000000000..86b6e7941f Binary files /dev/null and b/.yarn/cache/turndown-plugin-gfm-npm-1.0.2-73465b88af-18191dc18d.zip differ diff --git a/.yarn/cache/vanilla-colorful-npm-0.7.2-e7027ae79f-02bcdc1c8c.zip b/.yarn/cache/vanilla-colorful-npm-0.7.2-e7027ae79f-02bcdc1c8c.zip new file mode 100644 index 0000000000..2f6340b3fc Binary files /dev/null and b/.yarn/cache/vanilla-colorful-npm-0.7.2-e7027ae79f-02bcdc1c8c.zip differ diff --git a/Build/Jenkins/release-neos-ui.sh b/Build/Jenkins/release-neos-ui.sh index 60005eebf8..8cfbea8f96 100755 --- a/Build/Jenkins/release-neos-ui.sh +++ b/Build/Jenkins/release-neos-ui.sh @@ -41,15 +41,11 @@ nvm use # install dependencies and login to npm make install -# acutal release process +# actual release process # build make build-subpackages -# code quality -make lint -make test - # publishing VERSION=$VERSION make bump-version VERSION=$VERSION NPM_TOKEN=$NPM_TOKEN make publish-npm diff --git a/Build/Jenkins/update-neos-ui-compiled.sh b/Build/Jenkins/update-neos-ui-compiled.sh index 543ddddb18..d6af06bcc5 100755 --- a/Build/Jenkins/update-neos-ui-compiled.sh +++ b/Build/Jenkins/update-neos-ui-compiled.sh @@ -48,7 +48,7 @@ cd tmp_compiled_pkg git add Resources/Public/ git commit -m "Compile Neos UI - $GIT_SHA1" || true -if [[ "$GIT_BRANCH" == "origin/7.3" || "$GIT_BRANCH" == "origin/8.0" || "$GIT_BRANCH" == "origin/8.1" || "$GIT_BRANCH" == "origin/8.2" || "$GIT_BRANCH" == "origin/8.3" || "$GIT_BRANCH" == "origin/8.4" ]]; then +if [[ "$GIT_BRANCH" == "origin/7.3" || "$GIT_BRANCH" == "origin/8.0" || "$GIT_BRANCH" == "origin/8.1" || "$GIT_BRANCH" == "origin/8.2" || "$GIT_BRANCH" == "origin/8.3" || "$GIT_BRANCH" == "origin/8.4" || "$GIT_BRANCH" == "origin/9.0" ]]; then echo "Git branch $GIT_BRANCH found, pushing to this branch." git push origin HEAD:${GIT_BRANCH#*/} fi diff --git a/Classes/Application/ChangeTargetWorkspace.php b/Classes/Application/ChangeTargetWorkspace.php new file mode 100644 index 0000000000..07fd682fe7 --- /dev/null +++ b/Classes/Application/ChangeTargetWorkspace.php @@ -0,0 +1,37 @@ + $values + */ + public static function fromArray(array $values): self + { + return new self( + ContentRepositoryId::fromString($values['contentRepositoryId']), + WorkspaceName::fromString($values['workspaceName']), + ); + } +} diff --git a/Classes/Application/DiscardChangesInDocument.php b/Classes/Application/DiscardChangesInDocument.php new file mode 100644 index 0000000000..3a7fc07013 --- /dev/null +++ b/Classes/Application/DiscardChangesInDocument.php @@ -0,0 +1,48 @@ + $values + */ + public static function fromArray(array $values): self + { + return new self( + ContentRepositoryId::fromString($values['contentRepositoryId']), + WorkspaceName::fromString($values['workspaceName']), + NodeAggregateId::fromString($values['documentId']), + ); + } +} diff --git a/Classes/Application/DiscardChangesInSite.php b/Classes/Application/DiscardChangesInSite.php new file mode 100644 index 0000000000..749bd61e6e --- /dev/null +++ b/Classes/Application/DiscardChangesInSite.php @@ -0,0 +1,48 @@ + $values + */ + public static function fromArray(array $values): self + { + return new self( + ContentRepositoryId::fromString($values['contentRepositoryId']), + WorkspaceName::fromString($values['workspaceName']), + NodeAggregateId::fromString($values['siteId']), + ); + } +} diff --git a/Classes/Application/PublishChangesInDocument/PublishChangesInDocumentCommand.php b/Classes/Application/PublishChangesInDocument/PublishChangesInDocumentCommand.php new file mode 100644 index 0000000000..f5c06a6616 --- /dev/null +++ b/Classes/Application/PublishChangesInDocument/PublishChangesInDocumentCommand.php @@ -0,0 +1,54 @@ +} $values + */ + public static function fromArray(array $values): self + { + return new self( + ContentRepositoryId::fromString($values['contentRepositoryId']), + WorkspaceName::fromString($values['workspaceName']), + NodeAggregateId::fromString($values['documentId']), + isset($values['preferredDimensionSpacePoint']) && !empty($values['preferredDimensionSpacePoint']) + ? DimensionSpacePoint::fromLegacyDimensionArray($values['preferredDimensionSpacePoint']) + : null, + ); + } +} diff --git a/Classes/Application/PublishChangesInDocument/PublishChangesInDocumentCommandHandler.php b/Classes/Application/PublishChangesInDocument/PublishChangesInDocumentCommandHandler.php new file mode 100644 index 0000000000..d090fdb815 --- /dev/null +++ b/Classes/Application/PublishChangesInDocument/PublishChangesInDocumentCommandHandler.php @@ -0,0 +1,96 @@ +workspacePublishingService->publishChangesInDocument( + $command->contentRepositoryId, + $command->workspaceName, + $command->documentId + ); + + $workspace = $this->contentRepositoryRegistry->get($command->contentRepositoryId)->findWorkspaceByName( + $command->workspaceName + ); + + return new PublishSucceeded( + numberOfAffectedChanges: $publishingResult->numberOfPublishedChanges, + baseWorkspaceName: $workspace?->baseWorkspaceName?->value + ); + } catch (NodeAggregateCurrentlyDoesNotExist $e) { + throw new \RuntimeException( + $this->getLabel('NodeNotPublishedMissingParentNode'), + 1705053430, + $e + ); + } catch (NodeAggregateDoesCurrentlyNotCoverDimensionSpacePoint $e) { + throw new \RuntimeException( + $this->getLabel('NodeNotPublishedParentNodeNotInCurrentDimension'), + 1705053432, + $e + ); + } catch (WorkspaceRebaseFailed $e) { + $conflictsFactory = new ConflictsFactory( + contentRepository: $this->contentRepositoryRegistry + ->get($command->contentRepositoryId), + nodeLabelGenerator: $this->nodeLabelGenerator, + workspaceName: $command->workspaceName, + preferredDimensionSpacePoint: $command->preferredDimensionSpacePoint + ); + + return new ConflictsOccurred( + conflicts: $conflictsFactory->fromWorkspaceRebaseFailed($e) + ); + } + } +} diff --git a/Classes/Application/PublishChangesInSite/PublishChangesInSiteCommand.php b/Classes/Application/PublishChangesInSite/PublishChangesInSiteCommand.php new file mode 100644 index 0000000000..f177482c41 --- /dev/null +++ b/Classes/Application/PublishChangesInSite/PublishChangesInSiteCommand.php @@ -0,0 +1,54 @@ +} $values + */ + public static function fromArray(array $values): self + { + return new self( + ContentRepositoryId::fromString($values['contentRepositoryId']), + WorkspaceName::fromString($values['workspaceName']), + NodeAggregateId::fromString($values['siteId']), + isset($values['preferredDimensionSpacePoint']) && !empty($values['preferredDimensionSpacePoint']) + ? DimensionSpacePoint::fromLegacyDimensionArray($values['preferredDimensionSpacePoint']) + : null, + ); + } +} diff --git a/Classes/Application/PublishChangesInSite/PublishChangesInSiteCommandHandler.php b/Classes/Application/PublishChangesInSite/PublishChangesInSiteCommandHandler.php new file mode 100644 index 0000000000..2326a5cadb --- /dev/null +++ b/Classes/Application/PublishChangesInSite/PublishChangesInSiteCommandHandler.php @@ -0,0 +1,76 @@ +workspacePublishingService->publishChangesInSite( + $command->contentRepositoryId, + $command->workspaceName, + $command->siteId + ); + + $workspace = $this->contentRepositoryRegistry->get($command->contentRepositoryId)->findWorkspaceByName( + $command->workspaceName + ); + + return new PublishSucceeded( + numberOfAffectedChanges: $publishingResult->numberOfPublishedChanges, + baseWorkspaceName: $workspace?->baseWorkspaceName?->value + ); + } catch (WorkspaceRebaseFailed $e) { + $conflictsFactory = new ConflictsFactory( + contentRepository: $this->contentRepositoryRegistry + ->get($command->contentRepositoryId), + nodeLabelGenerator: $this->nodeLabelGenerator, + workspaceName: $command->workspaceName, + preferredDimensionSpacePoint: $command->preferredDimensionSpacePoint + ); + + return new ConflictsOccurred( + conflicts: $conflictsFactory->fromWorkspaceRebaseFailed($e) + ); + } + } +} diff --git a/Classes/Application/ReloadNodes/MinimalNodeForTree.php b/Classes/Application/ReloadNodes/MinimalNodeForTree.php new file mode 100644 index 0000000000..cdbba3584a --- /dev/null +++ b/Classes/Application/ReloadNodes/MinimalNodeForTree.php @@ -0,0 +1,65 @@ + $data + */ + private function __construct(private array $data) + { + } + + public static function tryFromNode( + Node $node, + NodeInfoHelper $nodeInfoHelper, + ActionRequest $actionRequest + ): ?self { + /** @var null|(array{contextPath:string}&array) $data */ + $data = $nodeInfoHelper + ->renderNodeWithMinimalPropertiesAndChildrenInformation( + node: $node, + actionRequest: $actionRequest + ); + + return $data ? new self($data) : null; + } + + public function getNodeAddressAsString(): string + { + return $this->data['contextPath']; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->data; + } +} diff --git a/Classes/Application/ReloadNodes/NoDocumentNodeWasFound.php b/Classes/Application/ReloadNodes/NoDocumentNodeWasFound.php new file mode 100644 index 0000000000..4d91738a3d --- /dev/null +++ b/Classes/Application/ReloadNodes/NoDocumentNodeWasFound.php @@ -0,0 +1,22 @@ +items = $items; + } + + /** + * @param class-string $nodeRepresentationClass + */ + public static function builder( + string $nodeRepresentationClass, + NodeInfoHelper $nodeInfoHelper, + ActionRequest $actionRequest + ): NodeMapBuilder { + return new NodeMapBuilder( + nodeRepresentationClass: $nodeRepresentationClass, + nodeInfoHelper: $nodeInfoHelper, + actionRequest: $actionRequest, + ); + } + + /** + * @return \stdClass|(array) + */ + public function jsonSerialize(): mixed + { + $result = []; + foreach ($this->items as $item) { + $result[$item->getNodeAddressAsString()] = $item; + } + + return $result ? $result : new \stdClass; + } +} diff --git a/Classes/Application/ReloadNodes/NodeMapBuilder.php b/Classes/Application/ReloadNodes/NodeMapBuilder.php new file mode 100644 index 0000000000..497e02a977 --- /dev/null +++ b/Classes/Application/ReloadNodes/NodeMapBuilder.php @@ -0,0 +1,60 @@ + $nodeRepresentationClass + */ + public function __construct( + private readonly string $nodeRepresentationClass, + private readonly NodeInfoHelper $nodeInfoHelper, + private readonly ActionRequest $actionRequest + ) { + } + + public function addNode(Node $node): void + { + $item = $this->nodeRepresentationClass::tryFromNode( + node: $node, + nodeInfoHelper: $this->nodeInfoHelper, + actionRequest: $this->actionRequest + ); + + if ($item !== null) { + $this->items[] = $item; + } + } + + public function build(): NodeMap + { + return new NodeMap(...$this->items); + } +} diff --git a/Classes/Application/ReloadNodes/ReloadNodesQuery.php b/Classes/Application/ReloadNodes/ReloadNodesQuery.php new file mode 100644 index 0000000000..9db531301d --- /dev/null +++ b/Classes/Application/ReloadNodes/ReloadNodesQuery.php @@ -0,0 +1,61 @@ + $values + */ + public static function fromArray(array $values): self + { + return new self( + contentRepositoryId: ContentRepositoryId::fromString($values['contentRepositoryId']), + workspaceName: WorkspaceName::fromString($values['workspaceName']), + dimensionSpacePoint: DimensionSpacePoint::fromLegacyDimensionArray($values['dimensionSpacePoint']), + siteId: NodeAggregateId::fromString($values['siteId']), + documentId: NodeAggregateId::fromString($values['documentId']), + ancestorsOfDocumentIds: NodeAggregateIds::fromArray($values['ancestorsOfDocumentIds']), + toggledNodesIds: NodeAggregateIds::fromArray($values['toggledNodesIds']), + clipboardNodesIds: NodeAggregateIds::fromArray($values['clipboardNodesIds']) + ); + } +} diff --git a/Classes/Application/ReloadNodes/ReloadNodesQueryHandler.php b/Classes/Application/ReloadNodes/ReloadNodesQueryHandler.php new file mode 100644 index 0000000000..b43606c7f6 --- /dev/null +++ b/Classes/Application/ReloadNodes/ReloadNodesQueryHandler.php @@ -0,0 +1,152 @@ +contentRepositoryRegistry + ->get($query->contentRepositoryId); + $subgraph = $contentRepository->getContentSubgraph($query->workspaceName, $query->dimensionSpacePoint); + $baseNodeTypeConstraints = NodeTypeCriteria::fromFilterString($this->baseNodeType); + + $documentNode = $subgraph->findNodeById($query->documentId); + if ($documentNode === null) { + foreach ($query->ancestorsOfDocumentIds as $ancestorOfDocumentId) { + $documentNode = $subgraph->findNodeById($ancestorOfDocumentId); + if ($documentNode) { + break; + } + } + } + + if ($documentNode === null) { + throw new NoDocumentNodeWasFound( + sprintf( + 'The document node with NodeAddress "%s" was not found.', + $query->documentId->value + ), + 1712584572 + ); + } + + $siteNode = $subgraph->findNodeById($query->siteId); + if ($siteNode === null) { + throw new NoSiteNodeWasFound( + sprintf( + 'The site node with NodeAddress "%s" was not found.', + $query->siteId->value + ), + 1712584589 + ); + } + + $ancestors = $subgraph->findAncestorNodes( + $documentNode->aggregateId, + FindAncestorNodesFilter::create( + NodeTypeCriteria::fromFilterString(NodeTypeNameFactory::NAME_DOCUMENT) + ) + ); + + $nodeMapBuilder = NodeMap::builder( + MinimalNodeForTree::class, + $this->nodeInfoHelper, + $actionRequest + ); + $nodeMapBuilder->addNode($siteNode); + + $gatherNodesRecursively = function ( + &$nodeMapBuilder, + Node $baseNode, + $level = 0 + ) use ( + &$gatherNodesRecursively, + $baseNodeTypeConstraints, + $query, + $ancestors, + $subgraph + ) { + if ($level < $this->loadingDepth || // load all nodes within loadingDepth + $this->loadingDepth === 0 || // unlimited loadingDepth + // load toggled nodes + $query->toggledNodesIds->contain($baseNode->aggregateId) || + // load children of all parents of documentNode + in_array($baseNode->aggregateId->value, array_map( + fn (Node $node): string => $node->aggregateId->value, + iterator_to_array($ancestors) + )) + ) { + foreach ($subgraph->findChildNodes( + $baseNode->aggregateId, + FindChildNodesFilter::create(nodeTypes: $baseNodeTypeConstraints) + ) as $childNode) { + $nodeMapBuilder->addNode($childNode); + $gatherNodesRecursively($nodeMapBuilder, $childNode, $level + 1); + } + } + }; + $gatherNodesRecursively($nodeMapBuilder, $siteNode); + + $nodeMapBuilder->addNode($documentNode); + + foreach ($query->clipboardNodesIds as $clipboardNodeId) { + // TODO: does not work across multiple CRs yet. + $clipboardNode = $subgraph->findNodeById($clipboardNodeId); + if ($clipboardNode) { + $nodeMapBuilder->addNode($clipboardNode); + } + } + + /* TODO: we might use the Subtree as this may be more efficient + - but the logic above mirrors the old behavior better. + https://github.com/neos/neos-ui/issues/3517#issuecomment-2070274053 */ + + return new ReloadNodesQueryResult( + documentId: NodeAddress::fromNode($documentNode), + nodes: $nodeMapBuilder->build() + ); + } +} diff --git a/Classes/Application/ReloadNodes/ReloadNodesQueryResult.php b/Classes/Application/ReloadNodes/ReloadNodesQueryResult.php new file mode 100644 index 0000000000..69fa39cc3f --- /dev/null +++ b/Classes/Application/ReloadNodes/ReloadNodesQueryResult.php @@ -0,0 +1,45 @@ + $this->documentId->toJson(), + 'nodes' => $this->nodes + ]; + } +} diff --git a/Classes/Application/Shared/Conflict.php b/Classes/Application/Shared/Conflict.php new file mode 100644 index 0000000000..faf13b690a --- /dev/null +++ b/Classes/Application/Shared/Conflict.php @@ -0,0 +1,42 @@ +items = array_values($items); + } + + public function jsonSerialize(): mixed + { + return $this->items; + } + + public function count(): int + { + return count($this->items); + } +} diff --git a/Classes/Application/Shared/ConflictsOccurred.php b/Classes/Application/Shared/ConflictsOccurred.php new file mode 100644 index 0000000000..fcf1817f5a --- /dev/null +++ b/Classes/Application/Shared/ConflictsOccurred.php @@ -0,0 +1,34 @@ + get_object_vars($this) + ]; + } +} diff --git a/Classes/Application/Shared/ReasonForConflict.php b/Classes/Application/Shared/ReasonForConflict.php new file mode 100644 index 0000000000..3979852e28 --- /dev/null +++ b/Classes/Application/Shared/ReasonForConflict.php @@ -0,0 +1,28 @@ +value; + } +} diff --git a/Classes/Application/Shared/TypeOfChange.php b/Classes/Application/Shared/TypeOfChange.php new file mode 100644 index 0000000000..cd474d30a5 --- /dev/null +++ b/Classes/Application/Shared/TypeOfChange.php @@ -0,0 +1,31 @@ +value; + } +} diff --git a/Classes/Application/SyncWorkspace/SyncWorkspaceCommand.php b/Classes/Application/SyncWorkspace/SyncWorkspaceCommand.php new file mode 100644 index 0000000000..f80dac1709 --- /dev/null +++ b/Classes/Application/SyncWorkspace/SyncWorkspaceCommand.php @@ -0,0 +1,39 @@ +workspacePublishingService->rebaseWorkspace( + $command->contentRepositoryId, + $command->workspaceName, + $command->rebaseErrorHandlingStrategy + ); + return new SyncingSucceeded(); + } catch (WorkspaceRebaseFailed $e) { + $conflictsFactory = new ConflictsFactory( + contentRepository: $this->contentRepositoryRegistry + ->get($command->contentRepositoryId), + nodeLabelGenerator: $this->nodeLabelGenerator, + workspaceName: $command->workspaceName, + preferredDimensionSpacePoint: $command->preferredDimensionSpacePoint + ); + + return new ConflictsOccurred( + conflicts: $conflictsFactory->fromWorkspaceRebaseFailed($e) + ); + } + } +} diff --git a/Classes/Application/SyncWorkspace/SyncingSucceeded.php b/Classes/Application/SyncWorkspace/SyncingSucceeded.php new file mode 100644 index 0000000000..52f5a0b288 --- /dev/null +++ b/Classes/Application/SyncWorkspace/SyncingSucceeded.php @@ -0,0 +1,32 @@ + true]; + } +} diff --git a/Classes/Aspects/AugmentationAspect.php b/Classes/Aspects/AugmentationAspect.php deleted file mode 100644 index 7b67248c31..0000000000 --- a/Classes/Aspects/AugmentationAspect.php +++ /dev/null @@ -1,175 +0,0 @@ -evaluate())") - * @param JoinPointInterface $joinPoint - * @return mixed - */ - public function setControllerContextFromContentElementWrappingImplementation(JoinPointInterface $joinPoint) - { - /** @var \Neos\Neos\Fusion\ContentElementWrappingImplementation $proxy */ - $proxy = $joinPoint->getProxy(); - $runtime = $proxy->getRuntime(); - $this->controllerContext = $runtime->getControllerContext(); - } - - /** - * @Flow\Before("method(Neos\Neos\ViewHelpers\ContentElement\WrapViewHelper->setRenderingContext())") - * @param JoinPointInterface $joinPoint - * @return mixed - */ - public function setControllerContextFromWrapViewHelper(JoinPointInterface $joinPoint) - { - $renderingContext = $joinPoint->getMethodArgument('renderingContext'); - if ($renderingContext instanceof FlowAwareRenderingContextInterface) { - $this->controllerContext = $renderingContext->getControllerContext(); - } - } - - /** - * Hooks into standard content element wrapping to render those attributes needed for the package to identify - * nodes and Fusion paths - * - * @Flow\Around("method(Neos\Neos\Service\ContentElementWrappingService->wrapContentObject())") - * @param JoinPointInterface $joinPoint the join point - * @return mixed - * @throws IllegalObjectTypeException - */ - public function contentElementAugmentation(JoinPointInterface $joinPoint) - { - /** @var NodeInterface $node */ - $node = $joinPoint->getMethodArgument('node'); - - $content = $joinPoint->getMethodArgument('content'); - - // Stay compatible with Neos 3.0. When we remove this compatibility, we can convert everything to "fusionPath"). - $fusionPath = ($joinPoint->isMethodArgument('typoScriptPath') ? $joinPoint->getMethodArgument('typoScriptPath') : $joinPoint->getMethodArgument('fusionPath')); - - if (!$this->needsMetadata($node, false)) { - return $content; - } - - $attributes = $joinPoint->isMethodArgument('additionalAttributes') ? $joinPoint->getMethodArgument('additionalAttributes') : []; - $attributes['data-__neos-node-contextpath'] = $node->getContextPath(); - $attributes['data-__neos-fusion-path'] = $fusionPath; - - // Define all attribute names as exclusive via the `exclusiveAttributes` parameter, to prevent the data of - // two different nodes to be concatenated into the attributes of a single html node. - // This way an outer div is added, if the wrapped content already has node related data-attributes set. - return $this->htmlAugmenter->addAttributes( - $content, - $attributes, - 'div', - array_keys($attributes) - ); - } - - /** - * Hooks into the editable viewhelper to render those attributes needed for the package's inline editing - * - * @Flow\Around("method(Neos\Neos\Service\ContentElementEditableService->wrapContentProperty())") - * @param JoinPointInterface $joinPoint the join point - * @return mixed - * @throws IllegalObjectTypeException - */ - public function editableElementAugmentation(JoinPointInterface $joinPoint) - { - $property = $joinPoint->getMethodArgument('property'); - $node = $joinPoint->getMethodArgument('node'); - $content = $joinPoint->getMethodArgument('content'); - - /** @var ContentContext $contentContext */ - $contentContext = $node->getContext(); - if (!$contentContext->isInBackend()) { - return $content; - } - - if ($this->nodeAuthorizationService->isGrantedToEditNode($node) === false) { - return $content; - } - - $content = $joinPoint->getAdviceChain()->proceed($joinPoint); - - $attributes = [ - 'data-__neos-property' => $property, - 'data-__neos-editable-node-contextpath' => $node->getContextPath() - ]; - - return $this->htmlAugmenter->addAttributes($content, $attributes, 'span'); - } - - /** - * @param NodeInterface $node - * @param boolean $renderCurrentDocumentMetadata - * @return boolean - * @throws IllegalObjectTypeException - */ - protected function needsMetadata(NodeInterface $node, $renderCurrentDocumentMetadata) - { - /** @var $contentContext ContentContext */ - $contentContext = $node->getContext(); - - return $contentContext->isInBackend() === true || $renderCurrentDocumentMetadata === true; - } -} diff --git a/Classes/ContentRepository/Service/NeosUiNodeService.php b/Classes/ContentRepository/Service/NeosUiNodeService.php new file mode 100644 index 0000000000..63fdad72e5 --- /dev/null +++ b/Classes/ContentRepository/Service/NeosUiNodeService.php @@ -0,0 +1,37 @@ +contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); + + $subgraph = $contentRepository->getContentSubgraph($nodeAddress->workspaceName, $nodeAddress->dimensionSpacePoint); + return $subgraph->findNodeById($nodeAddress->aggregateId); + } +} diff --git a/Classes/ContentRepository/Service/NodeService.php b/Classes/ContentRepository/Service/NodeService.php deleted file mode 100644 index 866797e8d7..0000000000 --- a/Classes/ContentRepository/Service/NodeService.php +++ /dev/null @@ -1,194 +0,0 @@ - - */ - protected array $contextCache = []; - - /** - * Helper method to retrieve the closest document for a node - * - * @param NodeInterface $node - * @return NodeInterface - */ - public function getClosestDocument(NodeInterface $node) - { - if ($node->getNodeType()->isOfType('Neos.Neos:Document')) { - return $node; - } - - $flowQuery = new FlowQuery([$node]); - - return $flowQuery->closest('[instanceof Neos.Neos:Document]')->get(0); - } - - /** - * Helper method to check if a given node is a document node. - * - * @param NodeInterface $node The node to check - * @return boolean A boolean which indicates if the given node is a document node. - */ - public function isDocument(NodeInterface $node) - { - return ($this->getClosestDocument($node) === $node); - } - - /** - * Converts a given context path to a node object - * - * @param string $contextPath - * @return NodeInterface|Error - */ - public function getNodeFromContextPath($contextPath, ?Site $site = null, ?Domain $domain = null, $includeAll = false) - { - $nodePathAndContext = NodePaths::explodeContextPath($contextPath); - $nodePath = $nodePathAndContext['nodePath']; - $workspaceName = $nodePathAndContext['workspaceName']; - $dimensions = $nodePathAndContext['dimensions']; - $siteNodeName = $site ? $site->getNodeName() : explode('/', $nodePath)[2]; - - // Prevent reloading the same context multiple times - $contextHash = md5(implode('|', [$siteNodeName, $workspaceName, json_encode($dimensions), $includeAll])); - if (isset($this->contextCache[$contextHash])) { - $context = $this->contextCache[$contextHash]; - } else { - $contextProperties = $this->prepareContextProperties($workspaceName, $dimensions); - - if ($site === null) { - $site = $this->siteRepository->findOneByNodeName($siteNodeName); - } - - if ($domain === null) { - $domain = $this->domainRepository->findOneBySite($site); - } - - $contextProperties['currentSite'] = $site; - $contextProperties['currentDomain'] = $domain; - if ($includeAll === true) { - $contextProperties['invisibleContentShown'] = true; - $contextProperties['removedContentShown'] = true; - $contextProperties['inaccessibleContentShown'] = true; - } - - $context = $this->contextFactory->create( - $contextProperties - ); - - $workspace = $context->getWorkspace(false); - if (!$workspace) { - return new Error( - sprintf('Could not convert the given source to Node object because the workspace "%s" as specified in the context node path does not exist.', $workspaceName), - 1451392329 - ); - } - $this->contextCache[$contextHash] = $context; - } - - return $context->getNode($nodePath); - } - - /** - * Checks if the given node exists in the given workspace - * - * @param NodeInterface $node - * @param Workspace $workspace - * @return boolean - */ - public function nodeExistsInWorkspace(NodeInterface $node, Workspace $workspace) - { - return $this->getNodeInWorkspace($node, $workspace) !== null; - } - - /** - * Get the variant of the given node in the given workspace - * - * @param NodeInterface $node - * @param Workspace $workspace - * @return NodeInterface|null - */ - public function getNodeInWorkspace(NodeInterface $node, Workspace $workspace): ?NodeInterface - { - $context = ['workspaceName' => $workspace->getName()]; - $flowQuery = new FlowQuery([$node]); - - $result = $flowQuery->context($context); - if ($result->count() > 0) { - return $result->get(0); - } else { - return null; - } - } - - /** - * Prepares the context properties for the nodes based on the given workspace and dimensions - * - * @param string $workspaceName - * @param ?array $dimensions - * @return array - */ - protected function prepareContextProperties($workspaceName, ?array $dimensions = null) - { - $contextProperties = [ - 'workspaceName' => $workspaceName, - 'invisibleContentShown' => false, - 'removedContentShown' => false - ]; - - if ($workspaceName !== 'live') { - $contextProperties['invisibleContentShown'] = true; - } - - if ($dimensions !== null) { - $contextProperties['dimensions'] = $dimensions; - } - - return $contextProperties; - } -} diff --git a/Classes/ContentRepository/Service/WorkspaceService.php b/Classes/ContentRepository/Service/WorkspaceService.php index efd0801ee4..99535e7b71 100644 --- a/Classes/ContentRepository/Service/WorkspaceService.php +++ b/Classes/ContentRepository/Service/WorkspaceService.php @@ -1,4 +1,5 @@ publishingService->getUnpublishedNodes($workspace); - - $publishableNodes = array_map(function ($node) { - if ($documentNode = $this->nodeService->getClosestDocument($node)) { - return [ - 'contextPath' => $node->getContextPath(), - 'documentContextPath' => $documentNode->getContextPath() + $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); + $contentGraph = $contentRepository->getContentGraph($workspaceName); + $pendingChanges = $this->workspacePublishingService->pendingWorkspaceChanges($contentRepositoryId, $workspaceName); + /** @var array{contextPath:string,documentContextPath:string,typeOfChange:int}[] $unpublishedNodes */ + $unpublishedNodes = []; + foreach ($pendingChanges as $change) { + if (method_exists($change, 'getLegacyRemovalAttachmentPoint') && $change->getLegacyRemovalAttachmentPoint() && $change->originDimensionSpacePoint !== null) { + // deprecated LegacyRemovalAttachmentPoint handling + $nodeAddress = NodeAddress::create( + $contentRepositoryId, + $workspaceName, + $change->originDimensionSpacePoint->toDimensionSpacePoint(), + $change->nodeAggregateId + ); + + /** + * See {@see Change::getLegacyRemovalAttachmentPoint()} -> Removal Attachment Point == closest document node. + */ + $documentNodeAddress = NodeAddress::create( + $contentRepositoryId, + $workspaceName, + $change->originDimensionSpacePoint->toDimensionSpacePoint(), + $change->getLegacyRemovalAttachmentPoint() + ); + + $unpublishedNodes[] = [ + 'contextPath' => $nodeAddress->toJson(), + 'documentContextPath' => $documentNodeAddress->toJson(), + 'typeOfChange' => $this->getTypeOfChange($change) ]; + } else { + if ($change->originDimensionSpacePoint !== null) { + $originDimensionSpacePoints = [$change->originDimensionSpacePoint]; + } else { + // If originDimensionSpacePoint is null, we have a change to the nodeAggregate. All nodes in the + // occupied dimensionspacepoints shall be marked as changed. + $originDimensionSpacePoints = $contentGraph + ->findNodeAggregateById($change->nodeAggregateId) + ?->occupiedDimensionSpacePoints ?: []; + } + + $contentGraph = $contentRepository->getContentGraph($workspaceName); + foreach ($originDimensionSpacePoints as $originDimensionSpacePoint) { + $subgraph = $contentGraph->getSubgraph($originDimensionSpacePoint->toDimensionSpacePoint(), VisibilityConstraints::createEmpty()); + $node = $subgraph->findNodeById($change->nodeAggregateId); + if ($node instanceof Node) { + $documentNode = $subgraph->findClosestNode($node->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_DOCUMENT)); + if ($documentNode instanceof Node) { + $unpublishedNodes[] = [ + 'contextPath' => NodeAddress::fromNode($node)->toJson(), + 'documentContextPath' => NodeAddress::fromNode($documentNode)->toJson(), + 'typeOfChange' => $this->getTypeOfChange($change) + ]; + } + } + } } - }, $publishableNodes); + } - return array_values(array_filter($publishableNodes, function ($item) { - return (bool)$item; - })); + return $unpublishedNodes; } - /** - * Get allowed target workspaces for current user - * - * @return array - */ - public function getAllowedTargetWorkspaces() + // todo remove for now lol :D + private function getTypeOfChange(Change $change): int { - $user = $this->domainUserService->getCurrentUser(); - - $workspacesArray = []; - /** @var Workspace $workspace */ - foreach ($this->workspaceRepository->findAll() as $workspace) { - // FIXME: This check should be implemented through a specialized Workspace Privilege or something similar - // Skip workspace not owned by current user - if ($workspace->getOwner() !== null && $workspace->getOwner() !== $user) { - continue; - } - // Skip own personal workspace - if ($workspace === $this->userService->getPersonalWorkspace()) { - continue; - } + $result = 0; - if ($workspace->isPersonalWorkspace()) { - // Skip other personal workspaces - continue; - } + if ($change->created) { + $result = $result | self::NODE_HAS_BEEN_CREATED; + } - $workspaceArray = [ - 'name' => $workspace->getName(), - 'title' => $workspace->getTitle(), - 'description' => $workspace->getDescription(), - 'readonly' => !$this->domainUserService->currentUserCanPublishToWorkspace($workspace) - ]; - $workspacesArray[$workspace->getName()] = $workspaceArray; + if ($change->changed) { + $result = $result | self::NODE_HAS_BEEN_CHANGED; } - return $workspacesArray; - } + if ($change->moved) { + $result = $result | self::NODE_HAS_BEEN_MOVED; + } - /** - * Sets base workspace of current user workspace - * - * @param Workspace $workspace - * @return void - */ - public function setBaseWorkspace(Workspace $workspace) - { - $userWorkspace = $this->userService->getPersonalWorkspace(); - $userWorkspace->setBaseWorkspace($workspace); + if ($change->deleted) { + $result = $result | self::NODE_HAS_BEEN_DELETED; + } + + return $result; } } diff --git a/Classes/Controller/BackendController.php b/Classes/Controller/BackendController.php index 841aaf5302..bb3019a65f 100644 --- a/Classes/Controller/BackendController.php +++ b/Classes/Controller/BackendController.php @@ -1,6 +1,5 @@ setFusionPath('backend'); - } + protected $nodeUriBuilderFactory; + + /** + * @Flow\Inject + * @var WorkspaceService + */ + protected $workspaceService; + + /** + * @Flow\Inject + * @var WorkspacePublishingService + */ + protected $workspacePublishingService; + + /** + * @Flow\InjectConfiguration(path="autoSyncPersonalWorkspaces") + * @var bool + */ + protected $autoSyncPersonalWorkspaces; /** * Displays the backend interface * - * @Flow\IgnoreValidation("$node") - * @param NodeInterface|null $node The node that will be displayed on the first tab + * @param string|null $node The node that will be displayed on the first tab * @return void - * @throws StopActionException - * @throws UnsupportedRequestTypeException - * @throws MissingActionNameException - * @throws \ReflectionException - * @throws \Neos\Flow\Http\Exception */ - public function indexAction(?NodeInterface $node = null): void + public function indexAction(?string $node = null) { + $siteDetectionResult = SiteDetectionResult::fromRequest($this->request->getHttpRequest()); + $contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId); + + $nodeAddress = $node !== null ? NodeAddress::fromJsonString($node) : null; $user = $this->userService->getBackendUser(); if ($user === null) { $this->redirectToUri($this->uriBuilder->uriFor('index', [], 'Login', 'Neos.Neos')); } - if ($node === null) { - $node = $this->findNodeToEdit(); + $this->workspaceService->createPersonalWorkspaceForUserIfMissing($siteDetectionResult->contentRepositoryId, $user); + $workspace = $this->workspaceService->getPersonalWorkspaceForUser($siteDetectionResult->contentRepositoryId, $user->getId()); + if ( + $this->autoSyncPersonalWorkspaces + && $workspace->status === WorkspaceStatus::OUTDATED + && !$workspace->hasPublishableChanges() + ) { + try { + $this->workspacePublishingService->rebaseWorkspace($siteDetectionResult->contentRepositoryId, $workspace->workspaceName); + } catch (WorkspaceRebaseFailed) { + // currently we don't have a way to provide this rebase error directly to the neos ui and have it solved. + // instead we ignore it and have the editor trigger it again via the sync button. + } } - $siteNode = $node->getContext()->getCurrentSiteNode(); + $contentGraph = $contentRepository->getContentGraph($workspace->workspaceName); - $this->view->assign('user', $user); - $this->view->assign('documentNode', $node); - $this->view->assign('site', $siteNode); - $this->view->assign('clipboardNodes', $this->clipboard->getNodeContextPaths()); - $this->view->assign('clipboardMode', $this->clipboard->getMode()); - $this->view->assign('headScripts', $this->styleAndJavascriptInclusionService->getHeadScripts()); - $this->view->assign('headStylesheets', $this->styleAndJavascriptInclusionService->getHeadStylesheets()); - $this->view->assign('splashScreenPartial', $this->settings['splashScreen']['partial']); - $this->view->assign('sitesForMenu', $this->menuHelper->buildSiteList($this->getControllerContext())); - $this->view->assign('modulesForMenu', $this->menuHelper->buildModuleList($this->getControllerContext())); + $rootDimensionSpacePoints = $contentRepository->getVariationGraph()->getRootGeneralizations(); + $arbitraryRootDimensionSpacePoint = array_shift($rootDimensionSpacePoints); - $this->view->assign('interfaceLanguage', $this->userService->getInterfaceLanguage()); - } + $subgraph = $contentRepository->getContentSubgraph( + $workspace->workspaceName, + $nodeAddress->dimensionSpacePoint ?? $arbitraryRootDimensionSpacePoint, + ); - /** - * Allow invisible nodes to be redirected to - * - * @return void - * @throws NoSuchArgumentException - */ - protected function initializeRedirectToAction(): void - { - // use this constant only if available (became available with patch level releases in Neos 4.0 and up) - if (defined(NodeConverter::class . '::INVISIBLE_CONTENT_SHOWN')) { - $this->arguments->getArgument('node')->getPropertyMappingConfiguration()->setTypeConverterOption(NodeConverter::class, NodeConverter::INVISIBLE_CONTENT_SHOWN, true); + // we assume that the ROOT node is always stored in the CR as "physical" node; so it is safe + // to call the contentGraph here directly. + $rootNodeAggregate = $contentGraph->findRootNodeAggregateByType( + NodeTypeNameFactory::forSites() + ); + if (!$rootNodeAggregate) { + throw new \RuntimeException(sprintf('No sites root node found in content repository "%s", while fetching site node "%s"', $contentRepository->id->value, $siteDetectionResult->siteNodeName->value), 1724849303); + } + + $siteNode = $subgraph->findNodeByPath( + $siteDetectionResult->siteNodeName->toNodeName(), + $rootNodeAggregate->nodeAggregateId + ); + + if (!$nodeAddress) { + $node = $siteNode; + } else { + $node = $subgraph->findNodeById($nodeAddress->aggregateId); } + + $this->view->setOption('title', 'Neos CMS'); + $this->view->assign('initialData', [ + 'configuration' => + $this->configurationProvider->getConfiguration( + contentRepository: $contentRepository, + uriBuilder: $this->controllerContext->getUriBuilder(), + ), + 'routes' => + $this->routesProvider->getRoutes( + uriBuilder: $this->controllerContext->getUriBuilder() + ), + 'frontendConfiguration' => + $this->frontendConfigurationProvider->getFrontendConfiguration( + actionRequest: $this->request, + ), + 'nodeTypes' => + $this->nodeTypeGroupsAndRolesProvider->getNodeTypes(), + 'menu' => + $this->menuProvider->getMenu( + actionRequest: $this->request, + ), + 'initialState' => + $this->initialStateProvider->getInitialState( + actionRequest: $this->request, + documentNode: $node, + site: $siteNode, + user: $user, + ), + ]); } /** - * @param NodeInterface $node - * @param ?string $presetBaseNodeType - * @throws MissingActionNameException - * @throws StopActionException - * @throws UnsupportedRequestTypeException - * @throws \Neos\Flow\Http\Exception - * @throws \Neos\Flow\Persistence\Exception\IllegalObjectTypeException - * @throws \Neos\Flow\Property\Exception - * @throws \Neos\Flow\Security\Exception - * @throws \Neos\Neos\Exception + * @throws \Neos\Flow\Mvc\Exception\StopActionException */ - public function redirectToAction(NodeInterface $node, ?string $presetBaseNodeType = null): void + public function redirectToAction(string $node): void { $this->response->setHttpHeader('Cache-Control', [ 'no-cache', 'no-store' ]); - $this->redirectToUri($this->linkingService->createNodeUri($this->controllerContext, $node, null, null, false, ['presetBaseNodeType' => $presetBaseNodeType])); - } - /** - * @return NodeInterface|null - */ - protected function getSiteNodeForLoggedInUser(): ?NodeInterface - { - $user = $this->userService->getBackendUser(); - if ($user === null) { - return null; - } + $nodeAddress = NodeAddress::fromJsonString($node); - $workspaceName = $this->userService->getPersonalWorkspaceName(); - return $this->createContext($workspaceName)->getCurrentSiteNode(); - } + $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); - /** - * @return NodeInterface|null - * @throws \ReflectionException - */ - protected function findNodeToEdit(): ?NodeInterface - { - $siteNode = $this->getSiteNodeForLoggedInUser(); - if (!$siteNode) { - throw new \RuntimeException('Could not find site node for current user.', 1697707361); - } - $reflectionMethod = new \ReflectionMethod($this->backendRedirectionService, 'getLastVisitedNode'); - $reflectionMethod->setAccessible(true); - $node = $reflectionMethod->invoke($this->backendRedirectionService, $siteNode->getContext()->getWorkspaceName()); + $nodeInstance = $contentRepository->getContentSubgraph( + $nodeAddress->workspaceName, + $nodeAddress->dimensionSpacePoint + )->findNodeById($nodeAddress->aggregateId); - if ($node === null || !str_starts_with($node->getPath(), $siteNode->getPath())) { - $node = $siteNode; - } + $workspace = $contentRepository->findWorkspaceByName($nodeAddress->workspaceName); - return $node; - } + // we always want to redirect to the node in the base workspace unless we are on a root workspace in which case we stay on that (currently that will not happen) + $nodeAddressInBaseWorkspace = NodeAddress::create( + $nodeAddress->contentRepositoryId, + $workspace->baseWorkspaceName ?? $nodeAddress->workspaceName, + $nodeAddress->dimensionSpacePoint, + $nodeAddress->aggregateId + ); - /** - * Create a ContentContext to be used for the backend redirects. - * - * @param string $workspaceName - * @return ContentContext - */ - protected function createContext(string $workspaceName): ?ContentContext - { - $contextProperties = [ - 'workspaceName' => $workspaceName, - 'invisibleContentShown' => true, - 'inaccessibleContentShown' => true - ]; - - $currentDomain = $this->domainRepository->findOneByActiveRequest(); - - if ($currentDomain !== null) { - $contextProperties['currentSite'] = $currentDomain->getSite(); - $contextProperties['currentDomain'] = $currentDomain; - } else { - $contextProperties['currentSite'] = $this->siteRepository->findFirstOnline(); - } + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($this->request); - return $this->contextFactory->create($contextProperties); + $this->redirectToUri( + !$nodeInstance || $nodeInstance->tags->contain(NeosSubtreeTag::disabled()) + ? $nodeUriBuilder->previewUriFor($nodeAddressInBaseWorkspace) + : $nodeUriBuilder->uriFor($nodeAddressInBaseWorkspace) + ); } } diff --git a/Classes/Controller/BackendServiceController.php b/Classes/Controller/BackendServiceController.php index cae2de946d..616e18b641 100644 --- a/Classes/Controller/BackendServiceController.php +++ b/Classes/Controller/BackendServiceController.php @@ -1,4 +1,5 @@ */ protected $supportedMediaTypes = ['application/json']; @@ -81,149 +86,135 @@ class BackendServiceController extends ActionController /** * @Flow\Inject - * @var PersistenceManagerInterface + * @var NeosUiNodeService */ - protected $persistenceManager; + protected $nodeService; /** * @Flow\Inject - * @var PublishingService + * @var UserService */ - protected $publishingService; + protected $userService; /** * @Flow\Inject - * @var NodeService + * @var ChangeCollectionConverter */ - protected $nodeService; + protected $changeCollectionConverter; /** * @Flow\Inject - * @var WorkspaceRepository + * @var NodeClipboard */ - protected $workspaceRepository; + protected $clipboard; /** * @Flow\Inject - * @var WorkspaceService + * @var PropertyMapper */ - protected $workspaceService; + protected $propertyMapper; /** * @Flow\Inject - * @var UserService + * @var Context */ - protected $userService; + protected $securityContext; /** * @Flow\Inject - * @var NodePolicyService + * @var ContentRepositoryRegistry */ - protected $nodePolicyService; + protected $contentRepositoryRegistry; /** * @Flow\Inject - * @var NodeClipboard + * @var NodeUriPathSegmentGenerator */ - protected $clipboard; + protected $nodeUriPathSegmentGenerator; /** * @Flow\Inject - * @var Service + * @var WorkspaceService */ - protected $localizationService; + protected $workspaceService; /** * @Flow\Inject - * @var PropertyMapper + * @var WorkspacePublishingService */ - protected $propertyMapper; + protected $workspacePublishingService; /** * @Flow\Inject - * @var ContentDimensionPresetSourceInterface + * @var PublishChangesInSiteCommandHandler */ - protected $contentDimensionsPresetSource; + protected $publishChangesInSiteCommandHandler; /** * @Flow\Inject - * @var Translator + * @var PublishChangesInDocumentCommandHandler */ - protected $translator; + protected $publishChangesInDocumentCommandHandler; /** * @Flow\Inject - * @var NodeUriPathSegmentGenerator + * @var SyncWorkspaceCommandHandler */ - protected $nodeUriPathSegmentGenerator; + protected $syncWorkspaceCommandHandler; /** - * Set the controller context on the feedback collection after the controller - * has been initialized - * - * @param ActionRequest $request - * @param ActionResponse $response - * @return void - * @throws UnsupportedRequestTypeException + * @Flow\Inject + * @var ReloadNodesQueryHandler */ - protected function initializeController(ActionRequest $request, ActionResponse $response) - { - parent::initializeController($request, $response); - $this->feedbackCollection->setControllerContext($this->getControllerContext()); - - try { - $this->localizationService->getConfiguration()->setCurrentLocale(new Locale($this->userService->getInterfaceLanguage())); - } catch (InvalidLocaleIdentifierException $e) { - // Do nothing, stay in the default locale - } - } + protected $reloadNodesQueryHandler; /** - * Helper method to inform the client, that new workspace information is available + * Cant be named here $throwableStorage see https://github.com/neos/flow-development-collection/issues/2928 * - * @param string $documentNodeContextPath - * @return void - * @throws IllegalObjectTypeException + * @Flow\Inject + * @var ThrowableStorageInterface */ - protected function updateWorkspaceInfo(string $documentNodeContextPath): void - { - $updateWorkspaceInfo = new UpdateWorkspaceInfo(); - $documentNode = $this->nodeService->getNodeFromContextPath($documentNodeContextPath, null, null, true); - if ($documentNode === null) { - $error = new Error(); - $error->setMessage(sprintf('Could not find node for document node context path "%s"', $documentNodeContextPath)); + protected $throwableStorage2; - $this->feedbackCollection->add($error); - } else { - $updateWorkspaceInfo->setWorkspace( - $documentNode->getContext()->getWorkspace() - ); + /** + * @Flow\Inject + * @var ContentRepositoryAuthorizationService + */ + protected $contentRepositoryAuthorizationService; - $this->feedbackCollection->add($updateWorkspaceInfo); - } + /** + * Set the controller context on the feedback collection after the controller + * has been initialized + */ + protected function initializeController(ActionRequest $request, ActionResponse $response): void + { + parent::initializeController($request, $response); + $this->feedbackCollection->setControllerContext($this->getControllerContext()); } /** * Apply a set of changes to the system - * - * @param ChangeCollection $changes - * @return void + * @phpstan-param list> $changes */ - public function changeAction(ChangeCollection $changes): void + public function changeAction(array $changes): void { + $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; + + $changeCollection = $this->changeCollectionConverter->convert($changes, $contentRepositoryId); try { - $count = $changes->count(); - $changes->apply(); + $count = $changeCollection->count(); + $changeCollection->apply(); $success = new Info(); - $success->setMessage($this->translator->translateById('changesApplied', [$count], $count, null, 'Main', 'Neos.Neos.Ui')); + $success->setMessage( + $this->getLabel('changesApplied', [$count], $count) + ); $this->feedbackCollection->add($success); - $this->persistenceManager->persistAll(); - } catch (Exception $e) { + } catch (\Exception $e) { + $this->throwableStorage2->logThrowable($e); $error = new Error(); $error->setMessage($e->getMessage()); - $this->feedbackCollection->add($error); } @@ -231,208 +222,311 @@ public function changeAction(ChangeCollection $changes): void } /** - * Publish nodes + * Publish all changes in the current site * - * @param array $nodeContextPaths - * @param string $targetWorkspaceName - * @return void + * @phpstan-param array{workspaceName:string,siteId:string,preferredDimensionSpacePoint?:array} $command */ - public function publishAction(array $nodeContextPaths, string $targetWorkspaceName): void + public function publishChangesInSiteAction(array $command): void { try { - $targetWorkspace = $this->workspaceRepository->findOneByName($targetWorkspaceName); - - foreach ($nodeContextPaths as $contextPath) { - $node = $this->nodeService->getNodeFromContextPath($contextPath, null, null, true); - if ($node === null) { - $error = new Info(); - $error->setMessage(sprintf('Could not find node for context path "%s"', $contextPath)); - - $this->feedbackCollection->add($error); - - continue; - } - $this->publishingService->publishNode($node, $targetWorkspace); - - if ($node->getNodeType()->isAggregate()) { - $updateNodePreviewUrl = new UpdateNodePreviewUrl(); - $updateNodePreviewUrl->setNode($node); - $this->feedbackCollection->add($updateNodePreviewUrl); - } - } - - $count = count($nodeContextPaths); - - $success = new Success(); - $success->setMessage($this->translator->translateById('changesPublished', [$count, $targetWorkspace->getTitle()], $count, null, 'Main', 'Neos.Neos.Ui')); - - $this->updateWorkspaceInfo($nodeContextPaths[0]); - $this->feedbackCollection->add($success); - - $this->persistenceManager->persistAll(); - } catch (Exception $e) { - $error = new Error(); - $error->setMessage($e->getMessage()); - - $this->feedbackCollection->add($error); + /** @todo send from UI */ + $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; + $command['contentRepositoryId'] = $contentRepositoryId->value; + $command['siteId'] = NodeAddress::fromJsonString( + $command['siteId'] + )->aggregateId->value; + $command = PublishChangesInSiteCommand::fromArray($command); + + $result = $this->publishChangesInSiteCommandHandler + ->handle($command); + + $this->view->assign('value', $result); + } catch (\Exception $e) { + $this->throwableStorage2->logThrowable($e); + $this->view->assign('value', [ + 'error' => [ + 'class' => $e::class, + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ] + ]); } + } - $this->view->assign('value', $this->feedbackCollection); + /** + * Publish all changes in the current document + * + * @phpstan-param array{workspaceName:string,documentId:string,preferredDimensionSpacePoint?:array} $command + */ + public function publishChangesInDocumentAction(array $command): void + { + try { + /** @todo send from UI */ + $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; + $command['contentRepositoryId'] = $contentRepositoryId->value; + $command['documentId'] = NodeAddress::fromJsonString( + $command['documentId'] + )->aggregateId->value; + $command = PublishChangesInDocumentCommand::fromArray($command); + + $result = $this->publishChangesInDocumentCommandHandler + ->handle($command); + + $this->view->assign('value', $result); + } catch (\Exception $e) { + $this->throwableStorage2->logThrowable($e); + $this->view->assign('value', [ + 'error' => [ + 'class' => $e::class, + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ] + ]); + } } /** - * Discard nodes + * Discard all changes in the user's personal workspace * - * @param array $nodeContextPaths - * @return void + * @phpstan-param array $command */ - public function discardAction(array $nodeContextPaths): void + public function discardAllChangesAction(array $command): void { try { - foreach ($nodeContextPaths as $contextPath) { - $node = $this->nodeService->getNodeFromContextPath($contextPath, null, null, true); - if (!$node) { - $error = new Error(); - $error->setMessage(sprintf('Could not find node for context path "%s"', $contextPath)); - - $this->feedbackCollection->add($error); - continue; - } - - if ($node->isRemoved() === true) { - // When discarding node removal we should re-create it - $updateNodeInfo = new UpdateNodeInfo(); - $updateNodeInfo->setNode($node); - $updateNodeInfo->recursive(); - $this->feedbackCollection->add($updateNodeInfo); - - // handle parent node, if needed - $parentNode = $node->getParent(); - if ($parentNode instanceof NodeInterface) { - $updateParentNodeInfo = new UpdateNodeInfo(); - $updateParentNodeInfo->setNode($parentNode); - $this->feedbackCollection->add($updateParentNodeInfo); - } - - // Reload document for content node changes - // (as we can't RenderContentOutOfBand from here, we don't know dom addresses) - if (!$this->nodeService->isDocument($node)) { - $reloadDocument = new ReloadDocument(); - $this->feedbackCollection->add($reloadDocument); - } - } elseif ($nodeInBaseWorkspace = $this->nodeService->getNodeInWorkspace($node, $node->getWorkSpace()->getBaseWorkspace())) { - $nodeHasBeenMoved = $node->getPath() !== $nodeInBaseWorkspace->getPath(); - if ($nodeHasBeenMoved) { - $removeNode = new RemoveNode(); - $removeNode->setNode($node); - $this->feedbackCollection->add($removeNode); - - $updateNodeInfo = new UpdateNodeInfo(); - $updateNodeInfo->setNode($nodeInBaseWorkspace); - $updateNodeInfo->recursive(); - $this->feedbackCollection->add($updateNodeInfo); - } - } else { - // If the node doesn't exist in the target workspace, tell the UI to remove it - $removeNode = new RemoveNode(); - $removeNode->setNode($node); - $this->feedbackCollection->add($removeNode); - } - - $this->publishingService->discardNode($node); - } + /** @todo send from UI */ + $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; + $command['contentRepositoryId'] = $contentRepositoryId->value; + $command = DiscardAllChanges::fromArray($command); + + $discardingResult = $this->workspacePublishingService->discardAllWorkspaceChanges( + $command->contentRepositoryId, + $command->workspaceName + ); - $count = count($nodeContextPaths); + $this->view->assign('value', [ + 'success' => [ + 'numberOfAffectedChanges' => $discardingResult->numberOfDiscardedChanges + ] + ]); + } catch (\Exception $e) { + $this->throwableStorage2->logThrowable($e); + $this->view->assign('value', [ + 'error' => [ + 'class' => $e::class, + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ] + ]); + } + } - $success = new Success(); - $success->setMessage($this->translator->translateById('changesDiscarded', [$count], $count, null, 'Main', 'Neos.Neos.Ui')); + /** + * Discard all changes in the given site + * + * @phpstan-param array $command + */ + public function discardChangesInSiteAction(array $command): void + { + try { + /** @todo send from UI */ + $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; + $command['contentRepositoryId'] = $contentRepositoryId->value; + $command['siteId'] = NodeAddress::fromJsonString( + $command['siteId'] + )->aggregateId->value; + $command = DiscardChangesInSite::fromArray($command); + + $discardingResult = $this->workspacePublishingService->discardChangesInSite( + $command->contentRepositoryId, + $command->workspaceName, + $command->siteId + ); - $this->updateWorkspaceInfo($nodeContextPaths[0]); - $this->feedbackCollection->add($success); + $this->view->assign('value', [ + 'success' => [ + 'numberOfAffectedChanges' => $discardingResult->numberOfDiscardedChanges + ] + ]); + } catch (\Exception $e) { + $this->throwableStorage2->logThrowable($e); + $this->view->assign('value', [ + 'error' => [ + 'class' => $e::class, + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ] + ]); + } + } - $this->persistenceManager->persistAll(); - } catch (Exception $e) { - $error = new Error(); - $error->setMessage($e->getMessage()); + /** + * Discard all changes in the given document + * + * @phpstan-param array $command + */ + public function discardChangesInDocumentAction(array $command): void + { + try { + /** @todo send from UI */ + $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; + $command['contentRepositoryId'] = $contentRepositoryId->value; + $command['documentId'] = NodeAddress::fromJsonString( + $command['documentId'] + )->aggregateId->value; + $command = DiscardChangesInDocument::fromArray($command); + + $discardingResult = $this->workspacePublishingService->discardChangesInDocument( + $command->contentRepositoryId, + $command->workspaceName, + $command->documentId + ); - $this->feedbackCollection->add($error); + $this->view->assign('value', [ + 'success' => [ + 'numberOfAffectedChanges' => $discardingResult->numberOfDiscardedChanges + ] + ]); + } catch (\Exception $e) { + $this->throwableStorage2->logThrowable($e); + $this->view->assign('value', [ + 'error' => [ + 'class' => $e::class, + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ] + ]); } - - $this->view->assign('value', $this->feedbackCollection); } /** * Change base workspace of current user workspace * * @param string $targetWorkspaceName , - * @param NodeInterface $documentNode + * @param string $documentNode * @return void - * @throws Exception + * @throws \Exception */ - public function changeBaseWorkspaceAction(string $targetWorkspaceName, NodeInterface $documentNode): void + public function changeBaseWorkspaceAction(string $targetWorkspaceName, string $documentNode): void { - try { - $targetWorkspace = $this->workspaceRepository->findOneByName($targetWorkspaceName); - $userWorkspace = $this->userService->getPersonalWorkspace(); - - if (count($this->workspaceService->getPublishableNodeInfo($userWorkspace)) > 0) { - // TODO: proper error dialog - throw new Exception('Your personal workspace currently contains unpublished changes. In order to switch to a different target workspace you need to either publish or discard pending changes first.', 1582800654); - } - - $sitePath = $documentNode->getContext()->getCurrentSiteNode()->getPath(); - $originalNodePath = $documentNode->getPath(); + $documentNodeAddress = NodeAddress::fromJsonString($documentNode); - $userWorkspace->setBaseWorkspace($targetWorkspace); - $this->workspaceRepository->update($userWorkspace); + $user = $this->userService->getBackendUser(); + if ($user === null) { + $error = new Error(); + $error->setMessage('No authenticated account'); + $this->feedbackCollection->add($error); + $this->view->assign('value', $this->feedbackCollection); + return; + } + $userWorkspace = $this->workspaceService->getPersonalWorkspaceForUser($documentNodeAddress->contentRepositoryId, $user->getId()); - $success = new Success(); - $success->setMessage(sprintf('Switched base workspace to %s.', $targetWorkspaceName)); - $this->feedbackCollection->add($success); + /** @todo send from UI */ + $command = new ChangeTargetWorkspace( + $documentNodeAddress->contentRepositoryId, + $userWorkspace->workspaceName, + WorkspaceName::fromString($targetWorkspaceName), + $documentNodeAddress + ); - $updateWorkspaceInfo = new UpdateWorkspaceInfo(); - $updateWorkspaceInfo->setWorkspace($userWorkspace); - $this->feedbackCollection->add($updateWorkspaceInfo); - - // If current document node doesn't exist in the base workspace, traverse its parents to find the one that exists - $nodesOnPath = $documentNode->getContext()->getNodesOnPath($sitePath, $originalNodePath); - $redirectNode = array_pop($nodesOnPath) ?? $documentNode->getContext()->getCurrentSiteNode(); - - // If current document node exists in the base workspace, then reload, else redirect - if ($redirectNode === $documentNode) { - $reloadDocument = new ReloadDocument(); - $reloadDocument->setNode($documentNode); - $this->feedbackCollection->add($reloadDocument); - } else { - $redirect = new Redirect(); - $redirect->setNode($redirectNode); - $this->feedbackCollection->add($redirect); - } + try { + $this->workspacePublishingService->changeBaseWorkspace($documentNodeAddress->contentRepositoryId, $userWorkspace->workspaceName, WorkspaceName::fromString($targetWorkspaceName)); + } catch (WorkspaceContainsPublishableChanges $workspaceIsNotEmptyException) { + $this->throwableStorage2->logThrowable($workspaceIsNotEmptyException); + $error = new Error(); + $error->setMessage( + $this->getLabel('workspaceContainsUnpublishedChanges') + ); - $this->persistenceManager->persistAll(); - } catch (Exception $e) { + $this->feedbackCollection->add($error); + $this->view->assign('value', $this->feedbackCollection); + return; + } catch (\Exception $e) { + $this->throwableStorage2->logThrowable($e); $error = new Error(); $error->setMessage($e->getMessage()); $this->feedbackCollection->add($error); + $this->view->assign('value', $this->feedbackCollection); + return; + } + + $contentRepository = $this->contentRepositoryRegistry->get($documentNodeAddress->contentRepositoryId); + $subgraph = $contentRepository->getContentSubgraph( + $userWorkspace->workspaceName, + $command->documentNode->dimensionSpacePoint, + ); + + $documentNodeInstance = $subgraph->findNodeById($command->documentNode->aggregateId); + assert($documentNodeInstance !== null); + + $success = new Success(); + $success->setMessage( + $this->getLabel('switchedBaseWorkspace', ['workspace' => $targetWorkspaceName]) + ); + $this->feedbackCollection->add($success); + + $updateWorkspaceInfo = new UpdateWorkspaceInfo($command->contentRepositoryId, $userWorkspace->workspaceName); + $this->feedbackCollection->add($updateWorkspaceInfo); + + // If current document node doesn't exist in the base workspace, + // traverse its parents to find the one that exists + // todo ensure that https://github.com/neos/neos-ui/pull/3734 doesnt need to be refixed in Neos 9.0 + $redirectNode = $documentNodeInstance; + while (true) { + $redirectNodeInBaseWorkspace = $subgraph->findNodeById($redirectNode->aggregateId); + if ($redirectNodeInBaseWorkspace) { + break; + } + $redirectNode = $subgraph->findParentNode($redirectNode->aggregateId); + // get parent always returns Node + if (!$redirectNode) { + throw new \Exception( + sprintf( + 'Wasn\'t able to locate any valid node in rootline of node %s in the workspace %s.', + $documentNodeInstance->aggregateId->value, + $targetWorkspaceName + ), + 1458814469 + ); + } + } + + // If current document node exists in the base workspace, then reload, else redirect + if ($redirectNode->equals($documentNodeInstance)) { + $reloadDocument = new ReloadDocument(); + $reloadDocument->setNode($documentNodeInstance); + $this->feedbackCollection->add($reloadDocument); + } else { + $redirect = new Redirect(); + $redirect->setNode($redirectNode); + $this->feedbackCollection->add($redirect); } $this->view->assign('value', $this->feedbackCollection); } + /** * Persists the clipboard node on copy * - * @param array $nodes + * @phpstan-param list $nodes * @return void + * @throws \Neos\Flow\Property\Exception + * @throws \Neos\Flow\Security\Exception */ public function copyNodesAction(array $nodes): void { - // TODO @christianm want's to have a property mapper for this - $nodes = array_map(function ($node) { - return $this->propertyMapper->convert($node, NodeInterface::class); - }, $nodes); - $this->clipboard->copyNodes($nodes); + /** @var array $nodeAddresses */ + $nodeAddresses = array_map( + NodeAddress::fromJsonString(...), + $nodes + ); + $this->clipboard->copyNodes($nodeAddresses); } /** @@ -440,7 +534,7 @@ public function copyNodesAction(array $nodes): void * * @return void */ - public function clearClipboardAction(): void + public function clearClipboardAction() { $this->clipboard->clear(); } @@ -448,147 +542,124 @@ public function clearClipboardAction(): void /** * Persists the clipboard node on cut * - * @param array $nodes - * @return void + * @phpstan-param list $nodes + * @throws \Neos\Flow\Property\Exception + * @throws \Neos\Flow\Security\Exception */ public function cutNodesAction(array $nodes): void { - // TODO @christianm want's to have a property mapper for this - $nodes = array_map(function ($node) { - return $this->propertyMapper->convert($node, NodeInterface::class); - }, $nodes); - $this->clipboard->cutNodes($nodes); + /** @var array $nodeAddresses */ + $nodeAddresses = array_map( + NodeAddress::fromJsonString(...), + $nodes + ); + + $this->clipboard->cutNodes($nodeAddresses); } public function getWorkspaceInfoAction(): void { - $workspaceHelper = new WorkspaceHelper(); - $personalWorkspaceInfo = $workspaceHelper->getPersonalWorkspace(); + $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; + $personalWorkspaceInfo = (new WorkspaceHelper())->getPersonalWorkspace($contentRepositoryId); $this->view->assign('value', $personalWorkspaceInfo); } - public function initializeLoadTreeAction(): void - { - $this->arguments['nodeTreeArguments']->getPropertyMappingConfiguration()->allowAllProperties(); - } - /** - * Load the nodetree - * - * @param NodeTreeBuilder $nodeTreeArguments - * @param boolean $includeRoot - * @return void - */ - public function loadTreeAction(NodeTreeBuilder $nodeTreeArguments, $includeRoot = false): void - { - $nodeTreeArguments->setControllerContext($this->controllerContext); - $this->view->assign('value', $nodeTreeArguments->build($includeRoot)); - } - - /** - * @throws NoSuchArgumentException + * @throws \Neos\Flow\Mvc\Exception\NoSuchArgumentException */ public function initializeGetAdditionalNodeMetadataAction(): void { - $this->arguments->getArgument('nodes')->getPropertyMappingConfiguration()->allowAllProperties(); + $this->arguments->getArgument('nodes') + ->getPropertyMappingConfiguration()->allowAllProperties(); } /** * Fetches all the node information that can be lazy-loaded - * - * @param array $nodes + * @phpstan-param list $nodes */ public function getAdditionalNodeMetadataAction(array $nodes): void { $result = []; - /** @var NodeInterface $node */ - foreach ($nodes as $node) { - $otherNodeVariants = array_values(array_filter(array_map(function ($node) { + foreach ($nodes as $nodeAddressString) { + $nodeAddress = NodeAddress::fromJsonString($nodeAddressString); + $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); + $subgraph = $contentRepository->getContentSubgraph( + $nodeAddress->workspaceName, + $nodeAddress->dimensionSpacePoint, + ); + $node = $subgraph->findNodeById($nodeAddress->aggregateId); + + // TODO finish implementation + /*$otherNodeVariants = array_values(array_filter(array_map(function ($node) { return $this->getCurrentDimensionPresetIdentifiersForNode($node); - }, $node->getOtherNodeVariants()))); - $result[$node->getContextPath()] = [ - 'policy' => $this->nodePolicyService->getNodePolicyInformation($node), - 'dimensions' => $this->getCurrentDimensionPresetIdentifiersForNode($node), - 'otherNodeVariants' => $otherNodeVariants - ]; + }, $node->getOtherNodeVariants())));*/ + if (!is_null($node)) { + $nodePrivileges = $this->contentRepositoryAuthorizationService->getNodePermissions($node, $this->securityContext->getRoles()); + $result[$nodeAddress->toJson()] = [ + 'policy' => [ + 'disallowedNodeTypes' => [], // not implemented for Neos 9.0 + 'canRemove' => $nodePrivileges->edit, + 'canEdit' => $nodePrivileges->edit, + 'disallowedProperties' => [] // not implemented for Neos 9.0 + ] + //'dimensions' => $this->getCurrentDimensionPresetIdentifiersForNode($node), + //'otherNodeVariants' => $otherNodeVariants + ]; + } } $this->view->assign('value', $result); } - /** - * Gets an array of current preset identifiers for each dimension of the give node - * - * @param NodeInterface $node - * @return array - */ - protected function getCurrentDimensionPresetIdentifiersForNode($node): array - { - $targetPresets = $this->contentDimensionsPresetSource->findPresetsByTargetValues($node->getDimensions()); - $presetCombo = []; - foreach ($targetPresets as $dimensionName => $presetConfig) { - $fullPresetConfig = $this->contentDimensionsPresetSource->findPresetByDimensionValues($dimensionName, $presetConfig['values']); - if ($fullPresetConfig !== null) { - $presetCombo[$dimensionName] = $fullPresetConfig['identifier']; - } - } - return $presetCombo; - } - /** * Build and execute a flow query chain * - * @param array $chain - * @return string - * @throws \Neos\Eel\Exception + * @phpstan-param non-empty-list}> $chain */ public function flowQueryAction(array $chain): string { + $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; + $createContext = array_shift($chain); $finisher = array_pop($chain); // we deduplicate passed nodes here $nodeContextPaths = array_unique(array_column($createContext['payload'], '$node')); - $flowQuery = new FlowQuery(array_map( - function ($contextPath) { - return $this->nodeService->getNodeFromContextPath($contextPath); - }, - $nodeContextPaths - )); + $flowQuery = new FlowQuery( + array_map( + fn ($nodeContextPath) => $this->nodeService->findNodeBySerializedNodeAddress( + $nodeContextPath + ), + $nodeContextPaths + ) + ); foreach ($chain as $operation) { - $flowQuery = call_user_func_array([$flowQuery, $operation['type']], $operation['payload']); + $flowQuery = $flowQuery->__call($operation['type'], $operation['payload']); } - $nodeInfoHelper = new NodeInfoHelper(); - $result = []; - - switch ($finisher['type']) { - case 'get': - $result = $nodeInfoHelper->renderNodes( - array_filter($flowQuery->get()), - $this->getControllerContext() - ); - break; - case 'getForTree': - $result = $nodeInfoHelper->renderNodes( - array_filter($flowQuery->get()), - $this->getControllerContext(), - true - ); - break; - case 'getForTreeWithParents': - $nodeTypeFilter = $finisher['payload']['nodeTypeFilter'] ?? null; - $result = $nodeInfoHelper->renderNodesWithParents( - array_filter($flowQuery->get()), - $this->getControllerContext(), - $nodeTypeFilter - ); - break; - } + /** @see GetOperation */ + assert(is_object($flowQuery) && is_callable([$flowQuery, 'get'])); - return json_encode($result); + $nodeInfoHelper = new NodeInfoHelper(); + $type = $finisher['type'] ?? null; + $result = match ($type) { + 'get' => $nodeInfoHelper->renderNodes(array_filter($flowQuery->get()), $this->request), + 'getForTree' => $nodeInfoHelper->renderNodes( + array_filter($flowQuery->get()), + $this->request, + true + ), + 'getForTreeWithParents' => $nodeInfoHelper->renderNodesWithParents( + array_filter($flowQuery->get()), + $this->request, + $finisher['payload']['nodeTypeFilter'] ?? null + ), + default => [] + }; + + return json_encode($result, JSON_THROW_ON_ERROR); } /** @@ -596,9 +667,117 @@ function ($contextPath) { * * @throws \Neos\Neos\Exception */ - public function generateUriPathSegmentAction(NodeInterface $contextNode, string $text): void + public function generateUriPathSegmentAction(string $contextNode, string $text): void { + $contextNodeAddress = NodeAddress::fromJsonString($contextNode); + $contentRepository = $this->contentRepositoryRegistry->get($contextNodeAddress->contentRepositoryId); + $subgraph = $contentRepository->getContentSubgraph( + $contextNodeAddress->workspaceName, + $contextNodeAddress->dimensionSpacePoint, + ); + $contextNode = $subgraph->findNodeById($contextNodeAddress->aggregateId); + $slug = $this->nodeUriPathSegmentGenerator->generateUriPathSegment($contextNode, $text); $this->view->assign('value', $slug); } + + /** + * Rebase user workspace to current workspace + * + * @param string $targetWorkspaceName + * @param bool $force + * @phpstan-param null|array $dimensionSpacePoint + * @return void + */ + public function syncWorkspaceAction(string $targetWorkspaceName, bool $force, ?array $dimensionSpacePoint): void + { + try { + $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; + $targetWorkspaceName = WorkspaceName::fromString($targetWorkspaceName); + $dimensionSpacePoint = $dimensionSpacePoint + ? DimensionSpacePoint::fromLegacyDimensionArray($dimensionSpacePoint) + : null; + + /** @todo send from UI */ + $command = new SyncWorkspaceCommand( + contentRepositoryId: $contentRepositoryId, + workspaceName: $targetWorkspaceName, + preferredDimensionSpacePoint: $dimensionSpacePoint, + rebaseErrorHandlingStrategy: $force + ? RebaseErrorHandlingStrategy::STRATEGY_FORCE + : RebaseErrorHandlingStrategy::STRATEGY_FAIL + ); + + $result = $this->syncWorkspaceCommandHandler->handle($command); + + $this->view->assign('value', $result); + } catch (\Exception $e) { + $this->throwableStorage2->logThrowable($e); + $this->view->assign('value', [ + 'error' => [ + 'class' => $e::class, + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ] + ]); + } + } + + /** + * @phpstan-param array $query + * @return void + */ + public function reloadNodesAction(array $query): void + { + /** @todo send from UI */ + $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId; + $query['contentRepositoryId'] = $contentRepositoryId->value; + $query['siteId'] = NodeAddress::fromJsonString( + $query['siteId'] + )->aggregateId->value; + $query['documentId'] = NodeAddress::fromJsonString( + $query['documentId'] + )->aggregateId->value; + $query['ancestorsOfDocumentIds'] = array_map( + fn (string $nodeAddress) => + NodeAddress::fromJsonString( + $nodeAddress + )->aggregateId->value, + $query['ancestorsOfDocumentIds'] + ); + $query['toggledNodesIds'] = array_map( + fn (string $nodeAddress) => + NodeAddress::fromJsonString( + $nodeAddress + )->aggregateId->value, + $query['toggledNodesIds'] + ); + $query['clipboardNodesIds'] = array_map( + fn (string $nodeAddress) => + NodeAddress::fromJsonString( + $nodeAddress + )->aggregateId->value, + $query['clipboardNodesIds'] + ); + $query = ReloadNodesQuery::fromArray($query); + + + try { + $result = $this->reloadNodesQueryHandler->handle($query, $this->request); + $this->view->assign('value', [ + 'success' => $result + ]); + } catch (\Exception $e) { + $this->throwableStorage2->logThrowable($e); + $this->view->assign('value', [ + 'error' => [ + 'class' => $e::class, + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ] + ]); + } + } } diff --git a/Classes/Controller/TranslationTrait.php b/Classes/Controller/TranslationTrait.php new file mode 100644 index 0000000000..33bbcd3de5 --- /dev/null +++ b/Classes/Controller/TranslationTrait.php @@ -0,0 +1,43 @@ + $arguments + */ + public function getLabel(string $id, array $arguments = [], ?int $quantity = null): string + { + return $this->translator->translateById( + $id, + $arguments, + $quantity, + null, + 'Main', + 'Neos.Neos.Ui' + ) ?: $id; + } +} diff --git a/Classes/Domain/InitialData/CacheConfigurationVersionProviderInterface.php b/Classes/Domain/InitialData/CacheConfigurationVersionProviderInterface.php new file mode 100644 index 0000000000..13982abb82 --- /dev/null +++ b/Classes/Domain/InitialData/CacheConfigurationVersionProviderInterface.php @@ -0,0 +1,27 @@ + */ + public function getFrontendConfiguration( + ActionRequest $actionRequest + ): array; +} diff --git a/Classes/Domain/InitialData/InitialStateProviderInterface.php b/Classes/Domain/InitialData/InitialStateProviderInterface.php new file mode 100644 index 0000000000..40b7c0b5af --- /dev/null +++ b/Classes/Domain/InitialData/InitialStateProviderInterface.php @@ -0,0 +1,37 @@ + */ + public function getInitialState( + ActionRequest $actionRequest, + ?Node $documentNode, + ?Node $site, + User $user, + ): array; +} diff --git a/Classes/Domain/InitialData/MenuProviderInterface.php b/Classes/Domain/InitialData/MenuProviderInterface.php new file mode 100644 index 0000000000..34f4b8355a --- /dev/null +++ b/Classes/Domain/InitialData/MenuProviderInterface.php @@ -0,0 +1,31 @@ +}> + */ + public function getMenu(ActionRequest $actionRequest): array; +} diff --git a/Classes/Domain/InitialData/NodeTypeGroupsAndRolesProviderInterface.php b/Classes/Domain/InitialData/NodeTypeGroupsAndRolesProviderInterface.php new file mode 100644 index 0000000000..65eb9de3ab --- /dev/null +++ b/Classes/Domain/InitialData/NodeTypeGroupsAndRolesProviderInterface.php @@ -0,0 +1,32 @@ + */ + public function getRoutes(UriBuilder $uriBuilder): array; +} diff --git a/Classes/Domain/Model/AbstractChange.php b/Classes/Domain/Model/AbstractChange.php index 001f7a531c..f683eba000 100644 --- a/Classes/Domain/Model/AbstractChange.php +++ b/Classes/Domain/Model/AbstractChange.php @@ -11,20 +11,27 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Neos\Ui\ContentRepository\Service\NodeService; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; +use Neos\Neos\Service\UserService; use Neos\Neos\Ui\Domain\Model\Feedback\Operations\NodeCreated; use Neos\Neos\Ui\Domain\Model\Feedback\Operations\ReloadDocument; use Neos\Neos\Ui\Domain\Model\Feedback\Operations\UpdateWorkspaceInfo; +/** + * @internal + */ abstract class AbstractChange implements ChangeInterface { - /** - * @var NodeInterface - */ - protected $subject; + protected Node $subject; + + #[Flow\Inject] + protected ContentRepositoryRegistry $contentRepositoryRegistry; /** * @Flow\Inject @@ -33,68 +40,58 @@ abstract class AbstractChange implements ChangeInterface protected $feedbackCollection; /** - * @var PersistenceManagerInterface + * @Flow\Inject + * @var UserService */ - protected $persistenceManager; + protected $userService; /** - * Inject the persistence manager - * - * @param PersistenceManagerInterface $persistenceManager - * @return void + * @Flow\Inject + * @var PersistenceManagerInterface */ - public function injectPersistenceManager(PersistenceManagerInterface $persistenceManager) - { - $this->persistenceManager = $persistenceManager; - } + protected $persistenceManager; - /** - * Set the subject - * - * @param NodeInterface $subject - * @return void - */ - public function setSubject(NodeInterface $subject) + final public function setSubject(Node $subject): void { $this->subject = $subject; } - /** - * Get the subject - * - * @return NodeInterface - */ - public function getSubject() + final public function getSubject(): Node { return $this->subject; } /** * Helper method to inform the client, that new workspace information is available - * - * @return void */ - protected function updateWorkspaceInfo() + final protected function updateWorkspaceInfo(): void + { + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($this->subject); + $documentNode = $subgraph->findClosestNode($this->subject->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_DOCUMENT)); + if (!is_null($documentNode)) { + $updateWorkspaceInfo = new UpdateWorkspaceInfo($documentNode->contentRepositoryId, $documentNode->workspaceName); + $this->feedbackCollection->add($updateWorkspaceInfo); + } + } + + final protected function findParentNode(Node $node): ?Node { - $nodeService = new NodeService(); - $updateWorkspaceInfo = new UpdateWorkspaceInfo(); - $documentNode = $nodeService->getClosestDocument($this->getSubject()); - $updateWorkspaceInfo->setWorkspace( - $documentNode->getContext()->getWorkspace() - ); + return $this->contentRepositoryRegistry->subgraphForNode($node) + ->findParentNode($node->aggregateId); + } - $this->feedbackCollection->add($updateWorkspaceInfo); + final protected function getNodeType(Node $node): ?NodeType + { + $contentRepository = $this->contentRepositoryRegistry->get($node->contentRepositoryId); + return $contentRepository->getNodeTypeManager()->getNodeType($node->nodeTypeName); } /** * Inform the client to reload the currently-displayed document, because the rendering has changed. * * This method will be triggered if [nodeType].properties.[propertyName].ui.reloadIfChanged is TRUE. - * - * @param ?NodeInterface $node - * @return void */ - protected function reloadDocument($node = null) + protected function reloadDocument(?Node $node = null): void { $reloadDocument = new ReloadDocument(); if ($node) { @@ -106,13 +103,10 @@ protected function reloadDocument($node = null) /** * Inform the client that a node has been created, the client decides if and which tree should react to this change. - * - * @param ?NodeInterface $subject - * @return void */ - protected function addNodeCreatedFeedback($subject = null) + final protected function addNodeCreatedFeedback(?Node $subject = null): void { - $node = $subject ?: $this->getSubject(); + $node = $subject ?? $this->getSubject(); $nodeCreated = new NodeCreated(); $nodeCreated->setNode($node); $this->feedbackCollection->add($nodeCreated); diff --git a/Classes/Domain/Model/AbstractFeedback.php b/Classes/Domain/Model/AbstractFeedback.php index 5a4ca35ae1..556d761252 100644 --- a/Classes/Domain/Model/AbstractFeedback.php +++ b/Classes/Domain/Model/AbstractFeedback.php @@ -14,9 +14,13 @@ use Neos\Flow\Mvc\Controller\ControllerContext; +/** + * @internal + */ abstract class AbstractFeedback implements FeedbackInterface { - public function serialize(ControllerContext $controllerContext) + /** @return array */ + public function serialize(ControllerContext $controllerContext): array { return [ 'type' => $this->getType(), diff --git a/Classes/Domain/Model/ChangeCollection.php b/Classes/Domain/Model/ChangeCollection.php index fa233d89cb..4c17a6506a 100644 --- a/Classes/Domain/Model/ChangeCollection.php +++ b/Classes/Domain/Model/ChangeCollection.php @@ -13,33 +13,29 @@ /** * A collection of changes + * @internal */ class ChangeCollection { /** * Changes in this collection * - * @var array + * @var array */ - protected $changes = []; + protected array $changes = []; /** * Add a change to this collection - * - * @param ChangeInterface $change - * @return void */ - public function add(ChangeInterface $change) + public function add(ChangeInterface $change): void { $this->changes[] = $change; } /** * Apply all changes - * - * @return void */ - public function apply() + public function apply(): void { while ($change = array_shift($this->changes)) { if ($change->canApply()) { @@ -50,10 +46,8 @@ public function apply() /** * Get the number of changes in this collection - * - * @return integer */ - public function count() + public function count(): int { return count($this->changes); } diff --git a/Classes/Domain/Model/ChangeInterface.php b/Classes/Domain/Model/ChangeInterface.php index 7f54538baf..932b8f12d9 100644 --- a/Classes/Domain/Model/ChangeInterface.php +++ b/Classes/Domain/Model/ChangeInterface.php @@ -1,4 +1,6 @@ setNode($node); - $updateNodeInfo->recursive(); - $this->feedbackCollection->add($updateNodeInfo); - parent::finish($node); - } - - /** - * Generate a unique node name for the copied node - * - * @param NodeInterface $parentNode - * @return string - */ - protected function generateUniqueNodeName(NodeInterface $parentNode) - { - return $this->contentRepositoryNodeService - ->generateUniqueNodeName($parentNode->getPath()); - } -} diff --git a/Classes/Domain/Model/Changes/AbstractCreate.php b/Classes/Domain/Model/Changes/AbstractCreate.php index 0e93cc5ad4..cd8aa19595 100644 --- a/Classes/Domain/Model/Changes/AbstractCreate.php +++ b/Classes/Domain/Model/Changes/AbstractCreate.php @@ -1,4 +1,5 @@ */ - protected $name = null; + protected array $data = []; /** - * @Flow\Inject - * @var NodeServiceInterface + * An (optional) name that will be used for the new node path */ - protected $nodeService; + protected ?string $name = null; /** - * Perform finish tasks - needs to be called from inheriting class on `apply` - * - * @param NodeInterface $node - * @return void + * An (optional) node aggregate identifier that will be used for testing */ - protected function finish(NodeInterface $node): void - { - $updateNodeInfo = new UpdateNodeInfo(); - $updateNodeInfo->setNode($node); - $updateNodeInfo->setBaseNodeType($this->baseNodeType); - $updateNodeInfo->recursive(); - $this->feedbackCollection->add($updateNodeInfo); - parent::finish($node); - } + protected ?NodeAggregateId $nodeAggregateId = null; /** - * Set the node type - * - * @param string $nodeType + * @param string $nodeTypeName */ - public function setNodeType($nodeType) + public function setNodeType(string $nodeTypeName): void { - if (is_string($nodeType)) { - $nodeType = $this->nodeTypeManager->getNodeType($nodeType); - } - - if (!$nodeType instanceof NodeType) { - throw new \InvalidArgumentException('nodeType needs to be of type string or NodeType', 1452100970); - } - - $this->nodeType = $nodeType; + $this->nodeTypeName = NodeTypeName::fromString($nodeTypeName); } - /** - * Get the node type - * - * @return NodeType - */ - public function getNodeType() + public function getNodeTypeName(): ?NodeTypeName { - return $this->nodeType; + return $this->nodeTypeName; } /** - * Set the data - * + * @phpstan-param array $data * @param array $data */ - public function setData(array $data) + public function setData(array $data): void { $this->data = $data; } /** - * Get the data - * - * @return array + * @return array */ - public function getData() + public function getData(): array { return $this->data; } - /** - * Set the name - * - * @param string $name - */ - public function setName($name) + public function setName(string $name): void { $this->name = $name; } - /** - * Get the name - * - * @return string|null - */ - public function getName() + public function getName(): ?string { return $this->name; } + public function setNodeAggregateId(string $nodeAggregateId): void + { + $this->nodeAggregateId = NodeAggregateId::fromString($nodeAggregateId); + } + + public function getNodeAggregateId(): ?NodeAggregateId + { + return $this->nodeAggregateId; + } + /** - * Creates a new node beneath $parent - * - * @param NodeInterface $parent - * @return NodeInterface + * @param Node $parentNode + * @param NodeAggregateId|null $succeedingSiblingNodeAggregateId + * @return Node + * @throws InvalidNodeCreationHandlerException */ - protected function createNode(NodeInterface $parent) - { - $nodeType = $this->getNodeType(); - $name = $this->getName() ?: $this->nodeService->generateUniqueNodeName($parent->getPath()); + protected function createNode( + Node $parentNode, + ?NodeAggregateId $succeedingSiblingNodeAggregateId = null + ): Node { + $nodeTypeName = $this->getNodeTypeName(); + if (is_null($nodeTypeName)) { + throw new \RuntimeException('Cannot run createNode without a set node type.', 1645577794); + } + + $contentRepository = $this->contentRepositoryRegistry->get($parentNode->contentRepositoryId); + $nodeType = $contentRepository->getNodeTypeManager()->getNodeType($nodeTypeName); + if (is_null($nodeType)) { + throw new \RuntimeException(sprintf('Cannot run create node because the node type %s is missing.', $nodeTypeName->value), 1716019747); + } + + $nodeAggregateId = $this->getNodeAggregateId() ?? NodeAggregateId::create(); // generate a new NodeAggregateId + + $command = CreateNodeAggregateWithNode::create( + $parentNode->workspaceName, + $nodeAggregateId, + $nodeTypeName, + OriginDimensionSpacePoint::fromDimensionSpacePoint($parentNode->dimensionSpacePoint), + $parentNode->aggregateId, + $succeedingSiblingNodeAggregateId + ); + + if ($this->getName()) { + $command = $command->withNodeName(NodeName::fromString($this->getName())); + } - $node = $parent->createNode($name, $nodeType); + $commands = $this->applyNodeCreationHandlers( + NodeCreationCommands::fromFirstCommand( + $command, + $contentRepository->getNodeTypeManager() + ), + $this->nodePropertyConversionService->convertNodeCreationElements( + $nodeType, + $this->getData() ?: [] + ), + $nodeType, + $contentRepository + ); + + foreach ($commands as $command) { + $contentRepository->handle($command); + } - $this->applyNodeCreationHandlers($node); + $newlyCreatedNode = $this->contentRepositoryRegistry->subgraphForNode($parentNode) + ->findNodeById($nodeAggregateId); - $this->finish($node); - // NOTE: we need to run "finish" before "addNodeCreatedFeedback" to ensure the new node already exists when the last feedback is processed - $this->addNodeCreatedFeedback($node); + if (!$newlyCreatedNode) { + throw new \RuntimeException(sprintf('Node %s was not created successfully or the graph was not up to date.', $nodeAggregateId->value)); + } - return $node; + $this->finish($newlyCreatedNode); + // NOTE: we need to run "finish" before "addNodeCreatedFeedback" + // to ensure the new node already exists when the last feedback is processed + $this->addNodeCreatedFeedback($newlyCreatedNode); + return $newlyCreatedNode; } /** - * Apply nodeCreationHandlers - * - * @param NodeInterface $node * @throws InvalidNodeCreationHandlerException - * @return void */ - protected function applyNodeCreationHandlers(NodeInterface $node) - { - $data = $this->getData() ?: []; - $nodeType = $node->getNodeType(); - if (isset($nodeType->getOptions()['nodeCreationHandlers'])) { - $nodeCreationHandlers = $nodeType->getOptions()['nodeCreationHandlers']; - if (is_array($nodeCreationHandlers)) { - foreach ((new PositionalArraySorter($nodeCreationHandlers))->toArray() as $nodeCreationHandlerConfiguration) { - $nodeCreationHandler = new $nodeCreationHandlerConfiguration['nodeCreationHandler'](); - if (!$nodeCreationHandler instanceof NodeCreationHandlerInterface) { - throw new InvalidNodeCreationHandlerException(sprintf('Expected NodeCreationHandlerInterface but got "%s"', get_class($nodeCreationHandler)), 1364759956); - } - $nodeCreationHandler->handle($node, $data); - } - } + protected function applyNodeCreationHandlers( + NodeCreationCommands $commands, + NodeCreationElements $elements, + NodeType $nodeType, + ContentRepository $contentRepository + ): NodeCreationCommands { + if (!isset($nodeType->getOptions()['nodeCreationHandlers']) + || !is_array($nodeType->getOptions()['nodeCreationHandlers'])) { + return $commands; } + foreach ((new PositionalArraySorter($nodeType->getOptions()['nodeCreationHandlers']))->toArray() as $key => $nodeCreationHandlerConfiguration) { + if (!isset($nodeCreationHandlerConfiguration['factoryClassName'])) { + throw new InvalidNodeCreationHandlerException(sprintf( + 'Node creation handler "%s" has no "factoryClassName" specified.', + $key + ), 1697750190); + } - $this->emitNodeCreationHandlersApplied($node); - } + $nodeCreationHandlerFactory = $this->objectManager->get($nodeCreationHandlerConfiguration['factoryClassName']); + if (!$nodeCreationHandlerFactory instanceof NodeCreationHandlerFactoryInterface) { + throw new InvalidNodeCreationHandlerException(sprintf( + 'Node creation handler "%s" didnt specify factory class of type %s. Got "%s"', + $key, + NodeCreationHandlerFactoryInterface::class, + get_class($nodeCreationHandlerFactory) + ), 1697750193); + } - /** - * Signals, that all changes by node creation handlers are applied - * - * @Flow\Signal - * - * @param NodeInterface $node The node, the node creation handlers are applied to - * @return void - */ - public function emitNodeCreationHandlersApplied(NodeInterface $node) - { + $nodeCreationHandler = $nodeCreationHandlerFactory->build($contentRepository); + if (!$nodeCreationHandler instanceof NodeCreationHandlerInterface) { + throw new InvalidNodeCreationHandlerException(sprintf( + 'Node creation handler "%s" didnt specify factory class of type %s. Got "%s"', + $key, + NodeCreationHandlerInterface::class, + get_class($nodeCreationHandler) + ), 1364759956); + } + $commands = $nodeCreationHandler->handle($commands, $elements); + } + return $commands; } } diff --git a/Classes/Domain/Model/Changes/AbstractMove.php b/Classes/Domain/Model/Changes/AbstractMove.php deleted file mode 100644 index a872f9ab42..0000000000 --- a/Classes/Domain/Model/Changes/AbstractMove.php +++ /dev/null @@ -1,85 +0,0 @@ -getContextPath() !== $this->getSubject()->getContextPath()) { - $updateNodePath = new UpdateNodePath(); - $updateNodePath->setOldContextPath($node->getContextPath()); - $updateNodePath->setNewContextPath($this->getSubject()->getContextPath()); - $this->feedbackCollection->add($updateNodePath); - } - - // $this->getSubject() is the moved node at the NEW location! - parent::finish($this->getSubject()); - } - - protected static function cloneNodeWithNodeData(NodeInterface $node) - { - if ($node instanceof Node) { - $originalNode = $node; - $node = clone $originalNode; - $node->setNodeData(clone $originalNode->getNodeData()); - - return $node; - } else { - // do a best-effort clone - return clone $node; - } - } - - /** - * Generate a unique node name for the copied node - * - * @param NodeInterface $parentNode - * @return string - */ - protected function generateUniqueNodeName(NodeInterface $parentNode) - { - return $this->contentRepositoryNodeService - ->generateUniqueNodeName($parentNode->getPath()); - } - - /** - * Returns true if the current name of $node is "free" below $parentNode - * - * @param NodeInterface $parentNode - * @param NodeInterface $node - * @return bool - */ - protected function nodeNameAvailableBelowNode(NodeInterface $parentNode, NodeInterface $node) - { - return $this->contentRepositoryNodeService->nodePathAvailableForNode($parentNode->getPath() . '/' . $node->getName(), $node); - } -} diff --git a/Classes/Domain/Model/Changes/AbstractStructuralChange.php b/Classes/Domain/Model/Changes/AbstractStructuralChange.php index 683c2075fb..6046554775 100644 --- a/Classes/Domain/Model/Changes/AbstractStructuralChange.php +++ b/Classes/Domain/Model/Changes/AbstractStructuralChange.php @@ -1,4 +1,5 @@ baseNodeType = $baseNodeType; } - /** - * Get the baseNodeType - * - * @return string|null - */ public function getBaseNodeType(): ?string { return $this->baseNodeType; @@ -79,18 +69,10 @@ public function getBaseNodeType(): ?string /** * Get the insertion mode (before|after|into) that is represented by this change - * - * @return string */ - abstract public function getMode(); + abstract public function getMode(): string; - /** - * Set the parent node dom address - * - * @param ?RenderedNodeDomAddress $parentDomAddress - * @return void - */ - public function setParentDomAddress(?RenderedNodeDomAddress $parentDomAddress = null) + public function setParentDomAddress(?RenderedNodeDomAddress $parentDomAddress = null): void { $this->parentDomAddress = $parentDomAddress; } @@ -99,48 +81,33 @@ public function setParentDomAddress(?RenderedNodeDomAddress $parentDomAddress = * Get the DOM address of the closest RENDERED node in the DOM tree. * * DOES NOT HAVE TO BE THE PARENT NODE! - * - * @return RenderedNodeDomAddress */ - public function getParentDomAddress() + public function getParentDomAddress(): ?RenderedNodeDomAddress { return $this->parentDomAddress; } - /** - * Set the sibling node dom address - * - * @param ?RenderedNodeDomAddress $siblingDomAddress - * @return void - */ - public function setSiblingDomAddress(?RenderedNodeDomAddress $siblingDomAddress = null) + public function setSiblingDomAddress(?RenderedNodeDomAddress $siblingDomAddress = null): void { $this->siblingDomAddress = $siblingDomAddress; } - /** - * Get the sibling node dom address - * - * @return RenderedNodeDomAddress - */ - public function getSiblingDomAddress() + public function getSiblingDomAddress(): ?RenderedNodeDomAddress { return $this->siblingDomAddress; } /** * Get the sibling node - * - * @return ?NodeInterface */ - public function getSiblingNode() + public function getSiblingNode(): ?Node { if ($this->siblingDomAddress === null) { return null; } if ($this->cachedSiblingNode === null) { - $this->cachedSiblingNode = $this->nodeService->getNodeFromContextPath( + $this->cachedSiblingNode = $this->nodeService->findNodeBySerializedNodeAddress( $this->siblingDomAddress->getContextPath() ); } @@ -151,32 +118,42 @@ public function getSiblingNode() /** * Perform finish tasks - needs to be called from inheriting class on `apply` * - * @param NodeInterface $node + * @param Node $node * @return void */ - protected function finish(NodeInterface $node) + protected function finish(Node $node) { - $this->persistenceManager->persistAll(); - - $updateParentNodeInfo = new UpdateNodeInfo(); - $updateParentNodeInfo->setNode($node->getParent()); - if ($this->baseNodeType) { - $updateParentNodeInfo->setBaseNodeType($this->baseNodeType); + $updateNodeInfo = new UpdateNodeInfo(); + $updateNodeInfo->setNode($node); + $updateNodeInfo->recursive(); + $this->feedbackCollection->add($updateNodeInfo); + + $parentNode = $this->contentRepositoryRegistry->subgraphForNode($node) + ->findParentNode($node->aggregateId); + if ($parentNode) { + $updateParentNodeInfo = new UpdateNodeInfo(); + $updateParentNodeInfo->setNode($parentNode); + if ($this->baseNodeType) { + $updateParentNodeInfo->setBaseNodeType($this->baseNodeType); + } + $this->feedbackCollection->add($updateParentNodeInfo); } - $this->feedbackCollection->add($updateParentNodeInfo); $this->updateWorkspaceInfo(); - if ($node->getNodeType()->isOfType('Neos.Neos:Content') && ($this->getParentDomAddress() || $this->getSiblingDomAddress())) { + if ($this->getNodeType($node)?->isOfType('Neos.Neos:Content') + && ($this->getParentDomAddress() || $this->getSiblingDomAddress())) { // we can ONLY render out of band if: - // 1) the parent of our new (or copied or moved) node is a ContentCollection; so we can directly update an element of this content collection - if ($node->getParent()->getNodeType()->isOfType('Neos.Neos:ContentCollection') && + // 1) the parent of our new (or copied or moved) node is a ContentCollection; + // so we can directly update an element of this content collection - // 2) the parent DOM address (i.e. the closest RENDERED node in DOM is actually the ContentCollection; and - // no other node in between + if ($parentNode && $this->getNodeType($parentNode)?->isOfType('Neos.Neos:ContentCollection') && + // 2) the parent DOM address (i.e. the closest RENDERED node in DOM is actually the ContentCollection; + // and no other node in between $this->getParentDomAddress() && $this->getParentDomAddress()->getFusionPath() && - $this->getParentDomAddress()->getContextPath() === $node->getParent()->getContextPath() + $this->getParentDomAddress()->getContextPath() === + NodeAddress::fromNode($parentNode)->toJson() ) { $renderContentOutOfBand = new RenderContentOutOfBand(); $renderContentOutOfBand->setNode($node); @@ -193,4 +170,38 @@ protected function finish(NodeInterface $node) } } } + + final protected function findChildNodes(Node $node): Nodes + { + // TODO REMOVE + return $this->contentRepositoryRegistry->subgraphForNode($node) + ->findChildNodes($node->aggregateId, FindChildNodesFilter::create()); + } + + final protected function isNodeTypeAllowedAsChildNode(Node $parentNode, NodeTypeName $nodeTypeNameToCheck): bool + { + $nodeTypeManager = $this->contentRepositoryRegistry->get($parentNode->contentRepositoryId)->getNodeTypeManager(); + + $parentNodeType = $nodeTypeManager->getNodeType($parentNode->nodeTypeName); + if (!$parentNodeType) { + return false; + } + + if ($parentNode->classification !== NodeAggregateClassification::CLASSIFICATION_TETHERED) { + $nodeTypeToCheck = $nodeTypeManager->getNodeType($nodeTypeNameToCheck); + if (!$nodeTypeToCheck) { + return false; + } + return $parentNodeType->allowsChildNodeType($nodeTypeToCheck); + } + assert($parentNode->name !== null); // were tethered + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($parentNode); + $grandParentNode = $subgraph->findParentNode($parentNode->aggregateId); + + return !$grandParentNode || $nodeTypeManager->isNodeTypeAllowedAsChildToTetheredNode( + $grandParentNode->nodeTypeName, + $parentNode->name, + $nodeTypeNameToCheck + ); + } } diff --git a/Classes/Domain/Model/Changes/CopyAfter.php b/Classes/Domain/Model/Changes/CopyAfter.php index 40e31836f5..c3e8307719 100644 --- a/Classes/Domain/Model/Changes/CopyAfter.php +++ b/Classes/Domain/Model/Changes/CopyAfter.php @@ -1,4 +1,5 @@ getSubject()->getNodeType(); - - return $this->getSiblingNode()->getParent()->isNodeTypeAllowedAsChildNode($nodeType); + $siblingNode = $this->getSiblingNode(); + if (is_null($siblingNode)) { + return false; + } + $parentNode = $this->findParentNode($siblingNode); + return $parentNode && $this->isNodeTypeAllowedAsChildNode($parentNode, $this->subject->nodeTypeName); } - public function getMode() + public function getMode(): string { return 'after'; } /** * Applies this change - * - * @return void */ - public function apply() + public function apply(): void { - if ($this->canApply()) { - $nodeName = $this->generateUniqueNodeName($this->getSiblingNode()->getParent()); - $node = $this->getSubject()->copyAfter($this->getSiblingNode(), $nodeName); - $this->finish($node); + $previousSibling = $this->getSiblingNode(); + $parentNodeOfPreviousSibling = !is_null($previousSibling) + ? $this->findParentNode($previousSibling) + : null; + $subject = $this->subject; + + if ($this->canApply() && !is_null($previousSibling) && !is_null($parentNodeOfPreviousSibling)) { + $succeedingSibling = null; + try { + $succeedingSibling = $this->findChildNodes($parentNodeOfPreviousSibling)->next($previousSibling); + } catch (\InvalidArgumentException $e) { + // do nothing; $succeedingSibling is null. Todo add Nodes::contain() + } + if (!$subject->dimensionSpacePoint->equals($parentNodeOfPreviousSibling->dimensionSpacePoint)) { + throw new \RuntimeException('Copying across dimensions is not supported yet (https://github.com/neos/neos-development-collection/issues/5054)', 1733586265); + } + $this->nodeDuplicationService->copyNodesRecursively( + $subject->contentRepositoryId, + $subject->workspaceName, + $subject->dimensionSpacePoint, + $subject->aggregateId, + OriginDimensionSpacePoint::fromDimensionSpacePoint($parentNodeOfPreviousSibling->dimensionSpacePoint), + $parentNodeOfPreviousSibling->aggregateId, + $succeedingSibling?->aggregateId, + NodeAggregateIdMapping::createEmpty() + ->withNewNodeAggregateId($subject->aggregateId, $newlyCreatedNodeId = NodeAggregateId::create()) + ); + + $newlyCreatedNode = $this->contentRepositoryRegistry->subgraphForNode($parentNodeOfPreviousSibling) + ->findNodeById($newlyCreatedNodeId); + if (!$newlyCreatedNode) { + throw new \RuntimeException(sprintf('Node %s was not found after copy.', $newlyCreatedNodeId->value), 1716023308); + } + $this->finish($newlyCreatedNode); + // NOTE: we need to run "finish" before "addNodeCreatedFeedback" + // to ensure the new node already exists when the last feedback is processed + $this->addNodeCreatedFeedback($newlyCreatedNode); } } } diff --git a/Classes/Domain/Model/Changes/CopyBefore.php b/Classes/Domain/Model/Changes/CopyBefore.php index 08a9393c2e..764a03a3cc 100644 --- a/Classes/Domain/Model/Changes/CopyBefore.php +++ b/Classes/Domain/Model/Changes/CopyBefore.php @@ -1,4 +1,5 @@ getSubject()->getNodeType(); + $siblingNode = $this->getSiblingNode(); + if (is_null($siblingNode)) { + return false; + } + $parentNode = $this->findParentNode($siblingNode); - return $this->getSiblingNode()->getParent()->isNodeTypeAllowedAsChildNode($nodeType); + return $parentNode && $this->isNodeTypeAllowedAsChildNode($parentNode, $this->subject->nodeTypeName); } - public function getMode() + public function getMode(): string { return 'before'; } @@ -35,12 +52,40 @@ public function getMode() * * @return void */ - public function apply() + public function apply(): void { - if ($this->canApply()) { - $nodeName = $this->generateUniqueNodeName($this->getSiblingNode()->getParent()); - $node = $this->getSubject()->copyBefore($this->getSiblingNode(), $nodeName); - $this->finish($node); + $succeedingSibling = $this->getSiblingNode(); + $parentNodeOfSucceedingSibling = !is_null($succeedingSibling) + ? $this->findParentNode($succeedingSibling) + : null; + $subject = $this->subject; + if ($this->canApply() && !is_null($succeedingSibling) + && !is_null($parentNodeOfSucceedingSibling) + ) { + if (!$subject->dimensionSpacePoint->equals($succeedingSibling->dimensionSpacePoint)) { + throw new \RuntimeException('Copying across dimensions is not supported yet (https://github.com/neos/neos-development-collection/issues/5054)', 1733586265); + } + $this->nodeDuplicationService->copyNodesRecursively( + $subject->contentRepositoryId, + $subject->workspaceName, + $subject->dimensionSpacePoint, + $subject->aggregateId, + OriginDimensionSpacePoint::fromDimensionSpacePoint($succeedingSibling->dimensionSpacePoint), + $parentNodeOfSucceedingSibling->aggregateId, + $succeedingSibling->aggregateId, + NodeAggregateIdMapping::createEmpty() + ->withNewNodeAggregateId($subject->aggregateId, $newlyCreatedNodeId = NodeAggregateId::create()) + ); + + $newlyCreatedNode = $this->contentRepositoryRegistry->subgraphForNode($parentNodeOfSucceedingSibling) + ->findNodeById($newlyCreatedNodeId); + if (!$newlyCreatedNode) { + throw new \RuntimeException(sprintf('Node %s was not found after copy.', $newlyCreatedNodeId->value), 1716023308); + } + $this->finish($newlyCreatedNode); + // NOTE: we need to run "finish" before "addNodeCreatedFeedback" + // to ensure the new node already exists when the last feedback is processed + $this->addNodeCreatedFeedback($newlyCreatedNode); } } } diff --git a/Classes/Domain/Model/Changes/CopyInto.php b/Classes/Domain/Model/Changes/CopyInto.php index 3aed4cd3ef..6e5c0eb2a9 100644 --- a/Classes/Domain/Model/Changes/CopyInto.php +++ b/Classes/Domain/Model/Changes/CopyInto.php @@ -12,37 +12,38 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\Neos\Domain\Service\NodeDuplication\NodeAggregateIdMapping; +use Neos\Neos\Domain\Service\NodeDuplicationService; +use Neos\Flow\Annotations as Flow; -class CopyInto extends AbstractCopy +/** + * @internal These objects internally reflect possible operations made by the Neos.Ui. + * They are sorely an implementation detail. You should not use them! + * Please look into the php command API of the Neos CR instead. + */ +class CopyInto extends AbstractStructuralChange { - /** - * @var string - */ - protected $parentContextPath; + #[Flow\Inject()] + protected NodeDuplicationService $nodeDuplicationService; - /** - * @var NodeInterface - */ - protected $cachedParentNode; + protected ?string $parentContextPath; - /** - * @param string $parentContextPath - */ - public function setParentContextPath($parentContextPath) + protected ?Node $cachedParentNode = null; + + public function setParentContextPath(string $parentContextPath): void { $this->parentContextPath = $parentContextPath; } - /** - * @return NodeInterface - */ - public function getParentNode() + public function getParentNode(): ?Node { - if ($this->cachedParentNode === null) { - $this->cachedParentNode = $this->nodeService->getNodeFromContextPath( - $this->parentContextPath - ); + if (!isset($this->cachedParentNode)) { + $this->cachedParentNode = $this->parentContextPath + ? $this->nodeService->findNodeBySerializedNodeAddress($this->parentContextPath) + : null; } return $this->cachedParentNode; @@ -50,40 +51,51 @@ public function getParentNode() /** * "Subject" is the to-be-copied node; the "parent" node is the new parent - * - * @return boolean */ - public function canApply() + public function canApply(): bool { - $nodeType = $this->getSubject()->getNodeType(); + $parentNode = $this->getParentNode(); - return $this->getParentNode()->isNodeTypeAllowedAsChildNode($nodeType); + return $parentNode && $this->isNodeTypeAllowedAsChildNode($parentNode, $this->subject->nodeTypeName); } - public function getMode() + public function getMode(): string { return 'into'; } /** * Applies this change - * - * @return void */ - public function apply() + public function apply(): void { - if ($this->canApply()) { - $parentNode = $this->getParentNode(); - $nodeName = $this->generateUniqueNodeName($parentNode); - // If the parent node has children, we copy the node after the last child node to prevent the copied nodes - // from being mixed with the existing ones due the duplication of their relative indices. - if ($parentNode->hasChildNodes()) { - $lastChildNode = array_slice($parentNode->getChildNodes(), -1, 1)[0]; - $node = $this->getSubject()->copyAfter($lastChildNode, $nodeName); - } else { - $node = $this->getSubject()->copyInto($parentNode, $nodeName); + $subject = $this->getSubject(); + $parentNode = $this->getParentNode(); + if ($parentNode && $this->canApply()) { + if (!$subject->dimensionSpacePoint->equals($parentNode->dimensionSpacePoint)) { + throw new \RuntimeException('Copying across dimensions is not supported yet (https://github.com/neos/neos-development-collection/issues/5054)', 1733586265); + } + $this->nodeDuplicationService->copyNodesRecursively( + $subject->contentRepositoryId, + $subject->workspaceName, + $subject->dimensionSpacePoint, + $subject->aggregateId, + OriginDimensionSpacePoint::fromDimensionSpacePoint($parentNode->dimensionSpacePoint), + $parentNode->aggregateId, + null, + NodeAggregateIdMapping::createEmpty() + ->withNewNodeAggregateId($subject->aggregateId, $newlyCreatedNodeId = NodeAggregateId::create()) + ); + + $newlyCreatedNode = $this->contentRepositoryRegistry->subgraphForNode($parentNode) + ->findNodeById($newlyCreatedNodeId); + if (!$newlyCreatedNode) { + throw new \RuntimeException(sprintf('Node %s was not found after copy.', $newlyCreatedNodeId->value), 1716023308); } - $this->finish($node); + $this->finish($newlyCreatedNode); + // NOTE: we need to run "finish" before "addNodeCreatedFeedback" + // to ensure the new node already exists when the last feedback is processed + $this->addNodeCreatedFeedback($newlyCreatedNode); } } } diff --git a/Classes/Domain/Model/Changes/Create.php b/Classes/Domain/Model/Changes/Create.php index 98067adaa7..431ab888be 100644 --- a/Classes/Domain/Model/Changes/Create.php +++ b/Classes/Domain/Model/Changes/Create.php @@ -1,4 +1,5 @@ getSubject(); - $nodeType = $this->getNodeType(); + $nodeTypeName = $this->getNodeTypeName(); - return $subject->isNodeTypeAllowedAsChildNode($nodeType); + return $nodeTypeName && $this->isNodeTypeAllowedAsChildNode($subject, $nodeTypeName); } /** * Create a new node beneath the subject - * - * @return void */ - public function apply() + public function apply(): void { + $parentNode = $this->getSubject(); if ($this->canApply()) { - $subject = $this->getSubject(); - $this->createNode($subject); + $this->createNode($parentNode, null); $this->updateWorkspaceInfo(); } } diff --git a/Classes/Domain/Model/Changes/CreateAfter.php b/Classes/Domain/Model/Changes/CreateAfter.php index 9fbf845b66..1d1a4a0e5d 100644 --- a/Classes/Domain/Model/Changes/CreateAfter.php +++ b/Classes/Domain/Model/Changes/CreateAfter.php @@ -1,4 +1,5 @@ getSubject()->getParent(); - $nodeType = $this->getNodeType(); + $parent = $this->findParentNode($this->subject); + $nodeTypeName = $this->getNodeTypeName(); - return $parent->isNodeTypeAllowedAsChildNode($nodeType); + return $parent && $nodeTypeName && $this->isNodeTypeAllowedAsChildNode($parent, $nodeTypeName); } /** * Create a new node after the subject - * - * @return void */ - public function apply() + public function apply(): void { - if ($this->canApply()) { - $subject = $this->getSubject(); - $parent = $subject->getParent(); - $node = $this->createNode($parent); + $parentNode = $this->findParentNode($this->subject); + $subject = $this->subject; + if ($this->canApply() && !is_null($parentNode)) { + $succeedingSibling = null; + try { + $succeedingSibling = $this->findChildNodes($parentNode)->next($subject); + } catch (\InvalidArgumentException $e) { + // do nothing; $succeedingSibling is null. + } + + $this->createNode($parentNode, $succeedingSibling?->aggregateId); - $node->moveAfter($subject); $this->updateWorkspaceInfo(); } } diff --git a/Classes/Domain/Model/Changes/CreateBefore.php b/Classes/Domain/Model/Changes/CreateBefore.php index 8939479e3f..0907d07301 100644 --- a/Classes/Domain/Model/Changes/CreateBefore.php +++ b/Classes/Domain/Model/Changes/CreateBefore.php @@ -1,4 +1,5 @@ getSubject()->getParent(); - $nodeType = $this->getNodeType(); + $parent = $this->findParentNode($this->subject); + $nodeTypeName = $this->getNodeTypeName(); - return $parent->isNodeTypeAllowedAsChildNode($nodeType); + return $parent && $nodeTypeName && $this->isNodeTypeAllowedAsChildNode($parent, $nodeTypeName); } /** * Create a new node after the subject - * - * @return void */ - public function apply() + public function apply(): void { - if ($this->canApply()) { - $subject = $this->getSubject(); - $parent = $subject->getParent(); - $node = $this->createNode($parent); - - $node->moveBefore($subject); + $parent = $this->findParentNode($this->subject); + $subject = $this->subject; + if ($this->canApply() && !is_null($parent)) { + $this->createNode($parent, $subject->aggregateId); $this->updateWorkspaceInfo(); } } diff --git a/Classes/Domain/Model/Changes/MoveAfter.php b/Classes/Domain/Model/Changes/MoveAfter.php index 94dd0e5752..fadb9bc50d 100644 --- a/Classes/Domain/Model/Changes/MoveAfter.php +++ b/Classes/Domain/Model/Changes/MoveAfter.php @@ -1,4 +1,5 @@ getSubject()->getNodeType(); + $sibling = $this->getSiblingNode(); + if (is_null($sibling)) { + return false; + } + $parent = $this->findParentNode($sibling); - return $this->getSiblingNode()->getParent()->isNodeTypeAllowedAsChildNode($nodeType); + return $parent && $this->isNodeTypeAllowedAsChildNode($parent, $this->subject->nodeTypeName); } - public function getMode() + public function getMode(): string { return 'after'; } @@ -37,28 +49,53 @@ public function getMode() * * @return void */ - public function apply() + public function apply(): void { - if ($this->canApply()) { - $before = self::cloneNodeWithNodeData($this->getSubject()); - $parent = $before->getParent(); - - if ($this->nodeNameAvailableBelowNode($this->getSiblingNode()->getParent(), $this->getSubject())) { - $this->getSubject()->moveAfter($this->getSiblingNode()); - } else { - $nodeName = $this->generateUniqueNodeName($this->getSiblingNode()->getParent()); - $this->getSubject()->moveAfter($this->getSiblingNode(), $nodeName); + $precedingSibling = $this->getSiblingNode(); + $parentNodeOfPreviousSibling = $precedingSibling ? $this->findParentNode($precedingSibling) : null; + // "subject" is the to-be-moved node + $subject = $this->subject; + $parentNode = $this->findParentNode($this->subject); + if ($this->canApply() + && !is_null($precedingSibling) + && !is_null($parentNodeOfPreviousSibling) + && !is_null($parentNode) + ) { + $succeedingSibling = null; + try { + $succeedingSibling = $this->findChildNodes($parentNodeOfPreviousSibling)->next($precedingSibling); + } catch (InvalidArgumentException $e) { + // do nothing; $succeedingSibling is null. } - $updateParentNodeInfo = new UpdateNodeInfo(); - $updateParentNodeInfo->setNode($parent); - if ($this->baseNodeType) { - $updateParentNodeInfo->setBaseNodeType($this->baseNodeType); + $hasEqualParentNode = $parentNode->aggregateId + ->equals($parentNodeOfPreviousSibling->aggregateId); + + $contentRepository = $this->contentRepositoryRegistry->get($subject->contentRepositoryId); + $rawMoveNodeStrategy = $this->getNodeType($this->subject)?->getConfiguration('options.moveNodeStrategy'); + if (!is_string($rawMoveNodeStrategy)) { + throw new \RuntimeException(sprintf('NodeType "%s" has an invalid configuration for option "moveNodeStrategy" expected string got %s', $this->subject->nodeTypeName->value, get_debug_type($rawMoveNodeStrategy)), 1732010016); } + $moveNodeStrategy = RelationDistributionStrategy::tryFrom($rawMoveNodeStrategy); + if ($moveNodeStrategy === null) { + throw new \RuntimeException(sprintf('NodeType "%s" has an invalid configuration for option "moveNodeStrategy" got %s', $this->subject->nodeTypeName->value, $rawMoveNodeStrategy), 1732010011); + } + $command = MoveNodeAggregate::create( + $subject->workspaceName, + $subject->dimensionSpacePoint, + $subject->aggregateId, + $moveNodeStrategy, + $hasEqualParentNode ? null : $parentNodeOfPreviousSibling->aggregateId, + $precedingSibling->aggregateId, + $succeedingSibling?->aggregateId, + ); + $contentRepository->handle($command); + $updateParentNodeInfo = new UpdateNodeInfo(); + $updateParentNodeInfo->setNode($parentNodeOfPreviousSibling); $this->feedbackCollection->add($updateParentNodeInfo); - $this->finish($before); + $this->finish($subject); } } } diff --git a/Classes/Domain/Model/Changes/MoveBefore.php b/Classes/Domain/Model/Changes/MoveBefore.php index fb1f822747..d71d2c7566 100644 --- a/Classes/Domain/Model/Changes/MoveBefore.php +++ b/Classes/Domain/Model/Changes/MoveBefore.php @@ -1,4 +1,5 @@ getSubject()->getNodeType(); + $siblingNode = $this->getSiblingNode(); + if (is_null($siblingNode)) { + return false; + } + $parent = $this->findParentNode($siblingNode); - return $this->getSiblingNode()->getParent()->isNodeTypeAllowedAsChildNode($nodeType); + return $parent && $this->isNodeTypeAllowedAsChildNode($parent, $this->subject->nodeTypeName); } - public function getMode() + public function getMode(): string { return 'before'; } /** * Applies this change - * - * @return void */ - public function apply() + public function apply(): void { - if ($this->canApply()) { - $before = self::cloneNodeWithNodeData($this->getSubject()); - $parent = $before->getParent(); + $succeedingSibling = $this->getSiblingNode(); + // "subject" is the to-be-moved node + $subject = $this->subject; + $parentNode = $this->findParentNode($subject); + $succeedingSiblingParent = $succeedingSibling ? $this->findParentNode($succeedingSibling) : null; + if ($this->canApply() && !is_null($succeedingSibling) + && !is_null($parentNode) && !is_null($succeedingSiblingParent) + ) { + $precedingSibling = null; + try { + $precedingSibling = $this->findChildNodes($parentNode) + ->previous($succeedingSibling); + } catch (\InvalidArgumentException $e) { + // do nothing; $precedingSibling is null. + } - if ($this->nodeNameAvailableBelowNode($this->getSiblingNode()->getParent(), $this->getSubject())) { - $this->getSubject()->moveBefore($this->getSiblingNode()); - } else { - $nodeName = $this->generateUniqueNodeName($this->getSiblingNode()->getParent()); - $this->getSubject()->moveBefore($this->getSiblingNode(), $nodeName); + $hasEqualParentNode = $parentNode->aggregateId + ->equals($succeedingSiblingParent->aggregateId); + + $contentRepository = $this->contentRepositoryRegistry->get($subject->contentRepositoryId); + $rawMoveNodeStrategy = $this->getNodeType($this->subject)?->getConfiguration('options.moveNodeStrategy'); + if (!is_string($rawMoveNodeStrategy)) { + throw new \RuntimeException(sprintf('NodeType "%s" has an invalid configuration for option "moveNodeStrategy" expected string got %s', $this->subject->nodeTypeName->value, get_debug_type($rawMoveNodeStrategy)), 1732010016); + } + $moveNodeStrategy = RelationDistributionStrategy::tryFrom($rawMoveNodeStrategy); + if ($moveNodeStrategy === null) { + throw new \RuntimeException(sprintf('NodeType "%s" has an invalid configuration for option "moveNodeStrategy" got %s', $this->subject->nodeTypeName->value, $rawMoveNodeStrategy), 1732010011); } + $contentRepository->handle( + MoveNodeAggregate::create( + $subject->workspaceName, + $subject->dimensionSpacePoint, + $subject->aggregateId, + $moveNodeStrategy, + $hasEqualParentNode + ? null + : $succeedingSiblingParent->aggregateId, + $precedingSibling?->aggregateId, + $succeedingSibling->aggregateId, + ) + ); $updateParentNodeInfo = new UpdateNodeInfo(); - $updateParentNodeInfo->setNode($parent); - if ($this->baseNodeType) { - $updateParentNodeInfo->setBaseNodeType($this->baseNodeType); - } + $updateParentNodeInfo->setNode($succeedingSiblingParent); $this->feedbackCollection->add($updateParentNodeInfo); - $this->finish($before); + $this->finish($subject); } } } diff --git a/Classes/Domain/Model/Changes/MoveInto.php b/Classes/Domain/Model/Changes/MoveInto.php index 59963d1f66..95dad1c83e 100644 --- a/Classes/Domain/Model/Changes/MoveInto.php +++ b/Classes/Domain/Model/Changes/MoveInto.php @@ -1,4 +1,5 @@ parentContextPath = $parentContextPath; } - /** - * Get the insertion mode (before|after|into) that is represented by this change - * - * @return string - */ - public function getMode() + public function getParentNode(): ?Node { - return 'into'; + if ($this->parentContextPath === null) { + return null; + } + + return $this->nodeService->findNodeBySerializedNodeAddress( + $this->parentContextPath + ); } + /** - * @return NodeInterface + * Get the insertion mode (before|after|into) that is represented by this change */ - public function getParentNode() + public function getMode(): string { - if ($this->cachedParentNode === null) { - $this->cachedParentNode = $this->nodeService->getNodeFromContextPath( - $this->parentContextPath - ); - } - - return $this->cachedParentNode; + return 'into'; } /** - * "Subject" is the to-be-copied node; the "parent" node is the new parent - * - * @return boolean + * Checks whether this change can be applied to the subject */ - public function canApply() + public function canApply(): bool { - $nodeType = $this->getSubject()->getNodeType(); + $parent = $this->getParentNode(); - return $this->getParentNode()->isNodeTypeAllowedAsChildNode($nodeType); + return $parent && $this->isNodeTypeAllowedAsChildNode($parent, $this->subject->nodeTypeName); } /** * Applies this change - * - * @return void */ - public function apply() + public function apply(): void { - if ($this->canApply()) { - $before = self::cloneNodeWithNodeData($this->getSubject()); - $parent = $before->getParent(); + // "parentNode" is the node where the $subject should be moved INTO + $parentNode = $this->getParentNode(); + // "subject" is the to-be-moved node + $subject = $this->subject; + if ($this->canApply() && $parentNode) { + $otherParent = $this->contentRepositoryRegistry->subgraphForNode($subject) + ->findParentNode($subject->aggregateId); - if ($this->nodeNameAvailableBelowNode($this->getParentNode(), $this->getSubject())) { - $this->getSubject()->moveInto($this->getParentNode()); - } else { - $nodeName = $this->generateUniqueNodeName($this->getParentNode()); - $this->getSubject()->moveInto($this->getParentNode(), $nodeName); - } + $hasEqualParentNode = $otherParent && $otherParent->aggregateId + ->equals($parentNode->aggregateId); - $updateParentNodeInfo = new UpdateNodeInfo(); - $updateParentNodeInfo->setNode($parent); - if ($this->baseNodeType) { - $updateParentNodeInfo->setBaseNodeType($this->baseNodeType); + $contentRepository = $this->contentRepositoryRegistry->get($subject->contentRepositoryId); + $rawMoveNodeStrategy = $this->getNodeType($this->subject)?->getConfiguration('options.moveNodeStrategy'); + if (!is_string($rawMoveNodeStrategy)) { + throw new \RuntimeException(sprintf('NodeType "%s" has an invalid configuration for option "moveNodeStrategy" expected string got %s', $this->subject->nodeTypeName->value, get_debug_type($rawMoveNodeStrategy)), 1732010016); } + $moveNodeStrategy = RelationDistributionStrategy::tryFrom($rawMoveNodeStrategy); + if ($moveNodeStrategy === null) { + throw new \RuntimeException(sprintf('NodeType "%s" has an invalid configuration for option "moveNodeStrategy" got %s', $this->subject->nodeTypeName->value, $rawMoveNodeStrategy), 1732010011); + } + $contentRepository->handle( + MoveNodeAggregate::create( + $subject->workspaceName, + $subject->dimensionSpacePoint, + $subject->aggregateId, + $moveNodeStrategy, + $hasEqualParentNode ? null : $parentNode->aggregateId, + ) + ); + $updateParentNodeInfo = new UpdateNodeInfo(); + $updateParentNodeInfo->setNode($parentNode); $this->feedbackCollection->add($updateParentNodeInfo); - $this->finish($before); + $this->finish($subject); } } } diff --git a/Classes/Domain/Model/Changes/Property.php b/Classes/Domain/Model/Changes/Property.php index c8b65d12fd..a1ff559b74 100644 --- a/Classes/Domain/Model/Changes/Property.php +++ b/Classes/Domain/Model/Changes/Property.php @@ -1,4 +1,5 @@ |null */ - protected $value; + protected string|array|null $value = null; /** * The change has been initiated from the inline editing - * - * @var bool */ - protected $isInline; + protected bool $isInline = false; - /** - * Set the property name - * - * @param string $propertyName - * @return void - */ - public function setPropertyName($propertyName) + public function setPropertyName(string $propertyName): void { $this->propertyName = $propertyName; } - /** - * Get the property name - * - * @return string - */ - public function getPropertyName() + public function getPropertyName(): ?string { return $this->propertyName; } - /** - * Set the node dom address - * - * @param ?RenderedNodeDomAddress $nodeDomAddress - * @return void - */ - public function setNodeDomAddress(?RenderedNodeDomAddress $nodeDomAddress = null) + public function setNodeDomAddress(?RenderedNodeDomAddress $nodeDomAddress = null): void { $this->nodeDomAddress = $nodeDomAddress; } - /** - * Get the node dom address - * - * @return RenderedNodeDomAddress - */ - public function getNodeDomAddress() + public function getNodeDomAddress(): ?RenderedNodeDomAddress { return $this->nodeDomAddress; } /** - * Set the value - * - * @param string $value + * @param string|array|null $value */ - public function setValue($value) + public function setValue(string|array|null $value): void { $this->value = $value; } /** - * Get the value - * - * @return string + * @return string|array|null */ - public function getValue() + public function getValue(): string|array|null { return $this->value; } - /** - * Set isInline - * - * @param bool $isInline - */ - public function setIsInline($isInline) + public function setIsInline(bool $isInline): void { $this->isInline = $isInline; } - /** - * Get isInline - * - * @return bool - */ - public function getIsInline() + public function getIsInline(): bool { return $this->isInline; } /** * Checks whether this change can be applied to the subject - * - * @return boolean */ - public function canApply() + public function canApply(): bool { - $nodeType = $this->getSubject()->getNodeType(); $propertyName = $this->getPropertyName(); - $nodeTypeProperties = $nodeType->getProperties(); - - if (!isset($nodeTypeProperties[$propertyName])) { + if (!$propertyName) { return false; } - - if (isset($nodeTypeProperties[$propertyName]['validation'])) { - foreach ($nodeTypeProperties[$propertyName]['validation'] as $validatorName => $validatorConfiguration) { - if (!\is_array($validatorConfiguration)) { - $validatorConfiguration = []; - } - // Fixes "Unsupported validation option(s) found: validationErrorMessage" by omitting this option https://github.com/neos/neos-ui/issues/3691 - unset($validatorConfiguration['validationErrorMessage']); - if ($this->nodePropertyValidationService->validate($this->value, $validatorName, $validatorConfiguration) === false) { - return false; - } - } + $nodeType = $this->getNodeType($this->subject); + if (!$nodeType) { + return false; } - - return true; + return $nodeType->hasProperty($propertyName) || $nodeType->hasReference($propertyName); } /** * Applies this change * - * @return void - * @throws NodeTypeNotFoundException + * @throws ContentStreamDoesNotExistYet + * @throws DimensionSpacePointNotFound + * @throws \Exception */ - public function apply() + public function apply(): void + { + $subject = $this->subject; + $nodeType = $this->getNodeType($subject); + $propertyName = $this->getPropertyName(); + if (is_null($nodeType) || is_null($propertyName) || $this->canApply() === false) { + return; + } + + match (true) { + $nodeType->hasReference($propertyName) => $this->handleNodeReferenceChange($subject, $propertyName), + // todo create custom 'changes' for these special cases + // we continue to use the underscore logic in the Neos Ui code base as the JS-client code works this way + $propertyName === '_nodeType' => $this->handleNodeTypeChange($subject), + $propertyName === '_hidden' => $this->handleHiddenPropertyChange($subject), + default => $this->handlePropertyChange($subject, $nodeType, $propertyName) + }; + + $this->createFeedback($subject); + } + + private function createFeedback(Node $subject): void { - if ($this->canApply()) { - $node = $this->getSubject(); - $propertyName = $this->getPropertyName(); - $value = $this->nodePropertyConversionService->convert( - $node->getNodeType(), - $propertyName, - $this->getValue(), - $node->getContext() + $propertyName = $this->getPropertyName(); + + // We have to refetch the Node after modifications because its a read-only model + // These 'Change' classes have been designed with mutable Neos < 9 Nodes and thus this might seem hacky + // When fully redesigning the Neos Ui php integration this will fixed + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($subject); + $originalNodeAggregateId = $subject->aggregateId; + $node = $subgraph->findNodeById($originalNodeAggregateId); + if (is_null($node)) { + throw new \InvalidArgumentException( + 'Cannot apply Property on missing node ' . $originalNodeAggregateId->value, + 1645560836 ); + } - // TODO: Make changing the node type a separated, specific/defined change operation. - if ($propertyName === '_nodeType') { - $nodeType = $this->nodeTypeManager->getNodeType($value); - $node = $this->changeNodeType($node, $nodeType); - } elseif ($propertyName[0] === '_') { - ObjectAccess::setProperty($node, substr($propertyName, 1), $value); + $this->updateWorkspaceInfo(); + $parentNode = $subgraph->findParentNode($node->aggregateId); + + // This might be needed to update node label and other things that we can calculate only on the server + $updateNodeInfo = new UpdateNodeInfo(); + $updateNodeInfo->setNode($node); + $this->feedbackCollection->add($updateNodeInfo); + + $reloadIfChangedConfigurationPathForProperty = sprintf('properties.%s.ui.reloadIfChanged', $propertyName); + $reloadIfChangedConfigurationPathForReference = sprintf('references.%s.ui.reloadIfChanged', $propertyName); + if (!$this->getIsInline() + && ( + $this->getNodeType($node)?->getConfiguration($reloadIfChangedConfigurationPathForProperty) + || $this->getNodeType($node)?->getConfiguration($reloadIfChangedConfigurationPathForReference) + ) + ) { + if ($this->getNodeDomAddress() && $this->getNodeDomAddress()->getFusionPath() + && $parentNode + && $this->getNodeType($parentNode)?->isOfType('Neos.Neos:ContentCollection')) { + $reloadContentOutOfBand = new ReloadContentOutOfBand(); + $reloadContentOutOfBand->setNode($node); + $reloadContentOutOfBand->setNodeDomAddress($this->getNodeDomAddress()); + $this->feedbackCollection->add($reloadContentOutOfBand); } else { - $node->setProperty($propertyName, $value); + $this->reloadDocument($node); } + } - $this->updateWorkspaceInfo(); - - // This might be needed to update node label and other things that we can calculate only on the server - $updateNodeInfo = new UpdateNodeInfo(); - $updateNodeInfo->setNode($node); - $this->feedbackCollection->add($updateNodeInfo); - - $reloadIfChangedConfigurationPath = sprintf('properties.%s.ui.reloadIfChanged', $propertyName); - if (!$this->getIsInline() && $node->getNodeType()->getConfiguration($reloadIfChangedConfigurationPath)) { - if ($this->getNodeDomAddress() && $this->getNodeDomAddress()->getFusionPath() - && $node->getNodeType()->isOfType('Neos.Neos:Content') - && $node->getParent()->getNodeType()->isOfType('Neos.Neos:ContentCollection') - ) { - $reloadContentOutOfBand = new ReloadContentOutOfBand(); - $reloadContentOutOfBand->setNode($node); - $reloadContentOutOfBand->setNodeDomAddress($this->getNodeDomAddress()); - $this->feedbackCollection->add($reloadContentOutOfBand); - } else { - // To prevent a full document reload we try to find a ContentCollection in the list of parents - // which would allows us to reload its children. Then we request a reload on the child that is - // a parent of our modified node. - $closestCollectionChildNode = $node; - while ($closestCollectionChildNode->getParent() - && !($closestCollectionChildNode->getParent()->getNodeType()->isOfType('Neos.Neos:ContentCollection') - || $closestCollectionChildNode->getParent()->getNodeType()->isOfType('Neos.Neos:Document'))) { - $closestCollectionChildNode = $closestCollectionChildNode->getParent(); - } - if ($closestCollectionChildNode && $closestCollectionChildNode->getParent() && $closestCollectionChildNode->getParent()->getNodeType()->isOfType('Neos.Neos:ContentCollection')) { - $fusionContextNodeTypeTag = '<' . $closestCollectionChildNode->getNodeType() . '>'; - - // Traverse to the fusion path that matches the tag of the closest node we can reload - $closestCollectionChildNodeFusionPath = explode('/', $this->getNodeDomAddress()->getFusionPath()); - for ($i = count($closestCollectionChildNodeFusionPath) - 1; $i >= 0; $i--) { - if (strpos($closestCollectionChildNodeFusionPath[$i], $fusionContextNodeTypeTag) === false) { - array_pop($closestCollectionChildNodeFusionPath); - } else { - break; - } - } - - $reloadContentOutOfBand = new ReloadContentOutOfBand(); - $reloadContentOutOfBand->setNode($closestCollectionChildNode); - $parentNodeDomAddress = new RenderedNodeDomAddress(); - $parentNodeDomAddress->setContextPath($closestCollectionChildNode->getContextPath()); - $parentNodeDomAddress->setFusionPath(join('/', $closestCollectionChildNodeFusionPath)); - $reloadContentOutOfBand->setNodeDomAddress($parentNodeDomAddress); - $this->feedbackCollection->add($reloadContentOutOfBand); - } else { - $this->reloadDocument($node); - } - } - } + $reloadPageIfChangedConfigurationPathForProperty = sprintf('properties.%s.ui.reloadPageIfChanged', $propertyName); + $reloadPageIfChangedConfigurationPathForReference = sprintf('references.%s.ui.reloadPageIfChanged', $propertyName); + if (!$this->getIsInline() + && ( + $this->getNodeType($node)?->getConfiguration($reloadPageIfChangedConfigurationPathForProperty) + || $this->getNodeType($node)?->getConfiguration($reloadPageIfChangedConfigurationPathForReference) + ) + ) { + $this->reloadDocument($node); + } + } - $reloadPageIfChangedConfigurationPath = sprintf('properties.%s.ui.reloadPageIfChanged', $propertyName); - if (!$this->getIsInline() && $node->getNodeType()->getConfiguration($reloadPageIfChangedConfigurationPath)) { - $this->reloadDocument($node); - } + private function handleNodeReferenceChange(Node $subject, string $propertyName): void + { + $contentRepository = $this->contentRepositoryRegistry->get($subject->contentRepositoryId); + $value = $this->getValue(); + + if (!is_array($value)) { + $value = [$value]; } + + $value = array_filter($value, fn ($v) => is_string($v) && !empty($v)); + $destinationNodeAggregateIds = array_values($value); + + $contentRepository->handle( + SetNodeReferences::create( + $subject->workspaceName, + $subject->aggregateId, + $subject->originDimensionSpacePoint, + NodeReferencesToWrite::create( + NodeReferencesForName::fromTargets( + ReferenceName::fromString($propertyName), + NodeAggregateIds::fromArray($destinationNodeAggregateIds) + ) + ) + ) + ); } - /** - * @param NodeInterface $node - * @param NodeType $nodeType - * @return NodeInterface - */ - protected function changeNodeType(NodeInterface $node, NodeType $nodeType) + private function handleHiddenPropertyChange(Node $subject): void { - $oldNodeType = $node->getNodeType(); - ObjectAccess::setProperty($node, 'nodeType', $nodeType); - $this->nodeService->cleanUpProperties($node); - $this->nodeService->cleanUpAutoCreatedChildNodes($node, $oldNodeType); - $this->nodeService->createChildNodes($node); + // todo simplify conversion + $value = (bool)$this->nodePropertyConversionService->convert('boolean', $this->getValue()); + + $contentRepository = $this->contentRepositoryRegistry->get($subject->contentRepositoryId); + + $command = match ($value) { + false => UntagSubtree::create( + $subject->workspaceName, + $subject->aggregateId, + $subject->originDimensionSpacePoint->toDimensionSpacePoint(), + NodeVariantSelectionStrategy::STRATEGY_ALL_SPECIALIZATIONS, + NeosSubtreeTag::disabled() + ), + true => TagSubtree::create( + $subject->workspaceName, + $subject->aggregateId, + $subject->originDimensionSpacePoint->toDimensionSpacePoint(), + NodeVariantSelectionStrategy::STRATEGY_ALL_SPECIALIZATIONS, + NeosSubtreeTag::disabled() + ) + }; + + $contentRepository->handle($command); + } + + private function handleNodeTypeChange(Node $subject): void + { + $contentRepository = $this->contentRepositoryRegistry->get($subject->contentRepositoryId); + // todo simplify conversion + /** @var string $value */ + $value = $this->nodePropertyConversionService->convert('string', $this->getValue()); + + $contentRepository->handle( + ChangeNodeAggregateType::create( + $subject->workspaceName, + $subject->aggregateId, + NodeTypeName::fromString($value), + NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy::STRATEGY_DELETE + ) + ); + } + + private function handlePropertyChange(Node $subject, NodeType $nodeType, string $propertyName): void + { + $contentRepository = $this->contentRepositoryRegistry->get($subject->contentRepositoryId); + $value = $this->nodePropertyConversionService->convert( + $nodeType->getPropertyType($propertyName), + $this->getValue() + ); + + $originDimensionSpacePoint = $subject->originDimensionSpacePoint; + if (!$subject->dimensionSpacePoint->equals($originDimensionSpacePoint)) { + $originDimensionSpacePoint = OriginDimensionSpacePoint::fromDimensionSpacePoint($subject->dimensionSpacePoint); + // if origin dimension space point != current DSP -> translate transparently (matching old behavior) + $contentRepository->handle( + CreateNodeVariant::create( + $subject->workspaceName, + $subject->aggregateId, + $subject->originDimensionSpacePoint, + $originDimensionSpacePoint + ) + ); + } - return $node; + $contentRepository->handle( + SetNodeProperties::create( + $subject->workspaceName, + $subject->aggregateId, + $originDimensionSpacePoint, + PropertyValuesToWrite::fromArray( + [ + $propertyName => $value + ] + ) + ) + ); } } diff --git a/Classes/Domain/Model/Changes/Remove.php b/Classes/Domain/Model/Changes/Remove.php index b4e096a375..379e4276e6 100644 --- a/Classes/Domain/Model/Changes/Remove.php +++ b/Classes/Domain/Model/Changes/Remove.php @@ -1,4 +1,5 @@ subject; if ($this->canApply()) { - $node = $this->getSubject(); - $node->remove(); + $parentNode = $this->findParentNode($subject); + if (is_null($parentNode)) { + throw new \InvalidArgumentException( + 'Cannot apply Remove without a parent on node ' . $subject->aggregateId->value, + 1645560717 + ); + } + // we have to schedule and the update workspace info before we actually delete the node; + // otherwise we cannot find the parent nodes anymore. $this->updateWorkspaceInfo(); - $removeNode = new RemoveNode(); - $removeNode->setNode($node); + // Issuing 'hard' removals via 'RemoveNodeAggregate' on a non-live workspace is not desired in Neos, see SoftRemovedTag + $command = TagSubtree::create( + $subject->workspaceName, + $subject->aggregateId, + $subject->dimensionSpacePoint, + NodeVariantSelectionStrategy::STRATEGY_ALL_SPECIALIZATIONS, + NeosSubtreeTag::removed() + ); + $contentRepository = $this->contentRepositoryRegistry->get($subject->contentRepositoryId); + $contentRepository->handle($command); + + $removeNode = new RemoveNode($subject, $parentNode); $this->feedbackCollection->add($removeNode); $updateParentNodeInfo = new UpdateNodeInfo(); - $updateParentNodeInfo->setNode($node->getParent()); + $updateParentNodeInfo->setNode($parentNode); $this->feedbackCollection->add($updateParentNodeInfo); } diff --git a/Classes/Domain/Model/Feedback/AbstractMessageFeedback.php b/Classes/Domain/Model/Feedback/AbstractMessageFeedback.php index a9075adea4..a9dbf90fa6 100644 --- a/Classes/Domain/Model/Feedback/AbstractMessageFeedback.php +++ b/Classes/Domain/Model/Feedback/AbstractMessageFeedback.php @@ -15,6 +15,9 @@ use Neos\Neos\Ui\Domain\Model\AbstractFeedback; use Neos\Neos\Ui\Domain\Model\FeedbackInterface; +/** + * @internal + */ abstract class AbstractMessageFeedback extends AbstractFeedback { /** diff --git a/Classes/Domain/Model/Feedback/Messages/Error.php b/Classes/Domain/Model/Feedback/Messages/Error.php index b32515a712..81068e1427 100644 --- a/Classes/Domain/Model/Feedback/Messages/Error.php +++ b/Classes/Domain/Model/Feedback/Messages/Error.php @@ -13,6 +13,9 @@ use Neos\Neos\Ui\Domain\Model\Feedback\AbstractMessageFeedback; +/** + * @internal + */ class Error extends AbstractMessageFeedback { /** diff --git a/Classes/Domain/Model/Feedback/Messages/Info.php b/Classes/Domain/Model/Feedback/Messages/Info.php index f4679633d5..69253c78b2 100644 --- a/Classes/Domain/Model/Feedback/Messages/Info.php +++ b/Classes/Domain/Model/Feedback/Messages/Info.php @@ -13,6 +13,9 @@ use Neos\Neos\Ui\Domain\Model\Feedback\AbstractMessageFeedback; +/** + * @internal + */ class Info extends AbstractMessageFeedback { /** diff --git a/Classes/Domain/Model/Feedback/Messages/Success.php b/Classes/Domain/Model/Feedback/Messages/Success.php index b86e58d5ff..af10fbbf5f 100644 --- a/Classes/Domain/Model/Feedback/Messages/Success.php +++ b/Classes/Domain/Model/Feedback/Messages/Success.php @@ -13,6 +13,9 @@ use Neos\Neos\Ui\Domain\Model\Feedback\AbstractMessageFeedback; +/** + * @internal + */ class Success extends AbstractMessageFeedback { /** diff --git a/Classes/Domain/Model/Feedback/Messages/Warning.php b/Classes/Domain/Model/Feedback/Messages/Warning.php index 7940240b99..5119c2d3da 100644 --- a/Classes/Domain/Model/Feedback/Messages/Warning.php +++ b/Classes/Domain/Model/Feedback/Messages/Warning.php @@ -13,12 +13,15 @@ use Neos\Neos\Ui\Domain\Model\Feedback\AbstractMessageFeedback; +/** + * @internal + */ class Warning extends AbstractMessageFeedback { /** * @var string */ - protected $severity = 'Warning'; + protected $severity = 'WARNING'; /** * Get the type identifier diff --git a/Classes/Domain/Model/Feedback/Operations/NodeCreated.php b/Classes/Domain/Model/Feedback/Operations/NodeCreated.php index 28636703c1..f79d75b208 100644 --- a/Classes/Domain/Model/Feedback/Operations/NodeCreated.php +++ b/Classes/Domain/Model/Feedback/Operations/NodeCreated.php @@ -11,58 +11,58 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\ControllerContext; -use Neos\Neos\Ui\ContentRepository\Service\NodeService; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Neos\Ui\Domain\Model\AbstractFeedback; use Neos\Neos\Ui\Domain\Model\FeedbackInterface; +/** + * @internal + */ class NodeCreated extends AbstractFeedback { + #[Flow\Inject] + protected ContentRepositoryRegistry $contentRepositoryRegistry; + /** - * @var NodeInterface + * @var Node */ protected $node; /** * Set the node - * - * @param NodeInterface $node - * @return void */ - public function setNode(NodeInterface $node) + public function setNode(Node $node): void { $this->node = $node; } /** * Get the node - * - * @return NodeInterface */ - public function getNode() + public function getNode(): Node { return $this->node; } /** * Get the type identifier - * - * @return string */ - public function getType() + public function getType(): string { return 'Neos.Neos.Ui:NodeCreated'; } /** * Get the description - * - * @return string */ - public function getDescription() + public function getDescription(): string { - return sprintf('Document Node "%s" created.', $this->getNode()->getContextPath()); + return sprintf('Document Node "%s" created.', $this->getNode()->aggregateId->value); } /** @@ -77,7 +77,7 @@ public function isSimilarTo(FeedbackInterface $feedback) return false; } - return $this->getNode()->getContextPath() === $feedback->getNode()->getContextPath(); + return $this->getNode()->equals($feedback->getNode()); } /** @@ -88,13 +88,14 @@ public function isSimilarTo(FeedbackInterface $feedback) */ public function serializePayload(ControllerContext $controllerContext) { - $nodeService = new NodeService(); $node = $this->getNode(); + $contentRepository = $this->contentRepositoryRegistry->get($node->contentRepositoryId); + $nodeType = $contentRepository->getNodeTypeManager()->getNodeType($node->nodeTypeName); return [ - 'contextPath' => $node->getContextPath(), - 'identifier' => $node->getIdentifier(), - 'isDocument' => $nodeService->isDocument($node) + 'contextPath' => NodeAddress::fromNode($node)->toJson(), + 'identifier' => $node->aggregateId->value, + 'isDocument' => $nodeType?->isOfType(NodeTypeNameFactory::NAME_DOCUMENT) ]; } } diff --git a/Classes/Domain/Model/Feedback/Operations/Redirect.php b/Classes/Domain/Model/Feedback/Operations/Redirect.php index bde9255ad8..3e3b303630 100644 --- a/Classes/Domain/Model/Feedback/Operations/Redirect.php +++ b/Classes/Domain/Model/Feedback/Operations/Redirect.php @@ -1,40 +1,52 @@ node = $node; } @@ -42,7 +54,7 @@ public function setNode(NodeInterface $node) /** * Get the node * - * @return NodeInterface + * @return Node */ public function getNode() { @@ -66,7 +78,7 @@ public function getType() */ public function getDescription() { - return sprintf('Redirect to node "%s".', $this->getNode()->getContextPath()); + return sprintf('Redirect to node "%s".', $this->nodeLabelGenerator->getLabel($this->getNode())); } /** @@ -81,23 +93,30 @@ public function isSimilarTo(FeedbackInterface $feedback) return false; } - return $this->getNode()->getContextPath() === $feedback->getNode()->getContextPath(); + return $this->getNode()->equals($feedback->getNode()); } /** * Serialize the payload for this feedback * * @param ControllerContext $controllerContext - * @return array + * @return array */ - public function serializePayload(ControllerContext $controllerContext) + public function serializePayload(ControllerContext $controllerContext): array { $node = $this->getNode(); - $redirectUri = $this->linkingService->createNodeUri($controllerContext, $node, null, null, true); + + $redirectUri = $this->nodeUriBuilderFactory->forActionRequest($controllerContext->getRequest()) + ->uriFor( + NodeAddress::fromNode($node), + Options::createForceAbsolute() + ); + + $contentRepository = $this->contentRepositoryRegistry->get($node->contentRepositoryId); return [ - 'redirectUri' => $redirectUri, - 'redirectContextPath' => $node->getContextPath() + 'redirectUri' => (string)$redirectUri, + 'redirectContextPath' => NodeAddress::fromNode($node)->toJson(), ]; } } diff --git a/Classes/Domain/Model/Feedback/Operations/ReloadContentOutOfBand.php b/Classes/Domain/Model/Feedback/Operations/ReloadContentOutOfBand.php index c7dfbacd20..45f8f7bf12 100644 --- a/Classes/Domain/Model/Feedback/Operations/ReloadContentOutOfBand.php +++ b/Classes/Domain/Model/Feedback/Operations/ReloadContentOutOfBand.php @@ -11,30 +11,29 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Fusion\Core\Cache\ContentCache; use Neos\Fusion\Exception as FusionException; +use Neos\Neos\Domain\Service\RenderingModeService; +use Neos\Neos\Fusion\Helper\CachingHelper; use Neos\Neos\Ui\Domain\Model\AbstractFeedback; use Neos\Neos\Ui\Domain\Model\FeedbackInterface; use Neos\Neos\Ui\Domain\Model\RenderedNodeDomAddress; -use Neos\Neos\View\FusionView as FusionView; -use Neos\Neos\Fusion\Helper\CachingHelper; +use Neos\Neos\Ui\View\OutOfBandRenderingViewFactory; +use Psr\Http\Message\ResponseInterface; +/** + * @internal + */ class ReloadContentOutOfBand extends AbstractFeedback { - /** - * @var NodeInterface - */ - protected $node; + protected Node $node; - /** - * The node dom address - * - * @var RenderedNodeDomAddress - */ - protected $nodeDomAddress; + protected ?RenderedNodeDomAddress $nodeDomAddress; /** * @Flow\Inject @@ -49,124 +48,115 @@ class ReloadContentOutOfBand extends AbstractFeedback protected $cachingHelper; /** - * Set the node - * - * @param NodeInterface $node - * @return void + * @Flow\Inject + * @var ContentRepositoryRegistry */ - public function setNode(NodeInterface $node) + protected $contentRepositoryRegistry; + + #[Flow\Inject] + protected RenderingModeService $renderingModeService; + + #[Flow\Inject] + protected OutOfBandRenderingViewFactory $outOfBandRenderingViewFactory; + + public function setNode(Node $node): void { $this->node = $node; } - /** - * Get the node - * - * @return NodeInterface - */ - public function getNode() + public function getNode(): Node { return $this->node; } - /** - * Set the node dom address - * - * @param ?RenderedNodeDomAddress $nodeDomAddress - * @return void - */ - public function setNodeDomAddress(?RenderedNodeDomAddress $nodeDomAddress = null) + public function setNodeDomAddress(?RenderedNodeDomAddress $nodeDomAddress = null): void { $this->nodeDomAddress = $nodeDomAddress; } - /** - * Get the node dom address - * - * @return RenderedNodeDomAddress - */ - public function getNodeDomAddress() + public function getNodeDomAddress(): ?RenderedNodeDomAddress { return $this->nodeDomAddress; } - /** - * Get the type identifier - * - * @return string - */ - public function getType() + public function getType(): string { return 'Neos.Neos.Ui:ReloadContentOutOfBand'; } - /** - * Get the description - * - * @return string - */ - public function getDescription() + public function getDescription(): string { - return sprintf('Rendering of node "%s" required.', $this->getNode()->getPath()); + return sprintf('Rendering of node "%s" required.', $this->node->aggregateId->value); } /** * Checks whether this feedback is similar to another - * - * @param FeedbackInterface $feedback - * @return boolean */ - public function isSimilarTo(FeedbackInterface $feedback) + public function isSimilarTo(FeedbackInterface $feedback): bool { if (!$feedback instanceof ReloadContentOutOfBand) { return false; } - return ( - $this->getNode()->getContextPath() === $feedback->getNode()->getContextPath() && - $this->getNodeDomAddress() == $feedback->getNodeDomAddress() - ); + return $this->getNode()->equals($feedback->getNode()) + && $this->getNodeDomAddress() == $feedback->getNodeDomAddress(); } /** * Serialize the payload for this feedback * - * @return mixed + * @return array */ - public function serializePayload(ControllerContext $controllerContext) + public function serializePayload(ControllerContext $controllerContext): array { - return [ - 'contextPath' => $this->getNode()->getContextPath(), - 'nodeDomAddress' => $this->getNodeDomAddress(), - 'renderedContent' => $this->renderContent($controllerContext) - ]; + if (!is_null($this->nodeDomAddress)) { + return [ + 'contextPath' => NodeAddress::fromNode($this->node)->toJson(), + 'nodeDomAddress' => $this->nodeDomAddress, + 'renderedContent' => $this->renderContent($controllerContext) + ]; + } + return []; } /** * Render the node - * - * @param ControllerContext $controllerContext - * @return string */ - protected function renderContent(ControllerContext $controllerContext) + protected function renderContent(ControllerContext $controllerContext): string { - $cacheTags = $this->cachingHelper->nodeTag($this->getNode()); + $cacheTags = $this->cachingHelper->nodeTag($this->node); foreach ($cacheTags as $tag) { $this->contentCache->flushByTag($tag); } - $nodeDomAddress = $this->getNodeDomAddress(); - - $fusionView = new FusionView(); - $fusionView->setControllerContext($controllerContext); - - $fusionView->assign('value', $this->getNode()); - $fusionView->setFusionPath($nodeDomAddress->getFusionPathForContentRendering()); + if ($this->nodeDomAddress) { + $renderingMode = $this->renderingModeService->findByCurrentUser(); + + $view = $this->outOfBandRenderingViewFactory->resolveView(); + if (method_exists($view, 'setControllerContext')) { + // deprecated + $view->setControllerContext($controllerContext); + } + $view->setOption('renderingModeName', $renderingMode->name); + + $view->assign('value', $this->node); + $view->setRenderingEntryPoint($this->nodeDomAddress->getFusionPathForContentRendering()); + + $content = $view->render(); + if ($content instanceof ResponseInterface) { + // todo should not happen, as we never render a full Neos.Neos:Page here? + return $content->getBody()->getContents(); + } + return $content->getContents(); + } - return $fusionView->render(); + return ''; } - public function serialize(ControllerContext $controllerContext) + /** + * @return array + */ + public function serialize(ControllerContext $controllerContext): array { try { return parent::serialize($controllerContext); diff --git a/Classes/Domain/Model/Feedback/Operations/ReloadDocument.php b/Classes/Domain/Model/Feedback/Operations/ReloadDocument.php index c1dc69e371..6f2e0ddee7 100644 --- a/Classes/Domain/Model/Feedback/Operations/ReloadDocument.php +++ b/Classes/Domain/Model/Feedback/Operations/ReloadDocument.php @@ -11,79 +11,48 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\ControllerContext; -use Neos\Neos\Service\LinkingService; -use Neos\Neos\Ui\ContentRepository\Service\NodeService; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Neos\Ui\Domain\Model\AbstractFeedback; use Neos\Neos\Ui\Domain\Model\FeedbackInterface; +use Neos\Neos\Ui\Fusion\Helper\NodeInfoHelper; +/** + * @internal + */ class ReloadDocument extends AbstractFeedback { - /** - * @var NodeInterface - */ - protected $node; - - /** - * @Flow\Inject - * @var LinkingService - */ - protected $linkingService; + protected ?Node $node = null; - /** - * @Flow\Inject - * @var NodeService - */ - protected $nodeService; + #[Flow\Inject] + protected ContentRepositoryRegistry $contentRepositoryRegistry; - /** - * Get the type identifier - * - * @return string - */ - public function getType() + public function getType(): string { return 'Neos.Neos.Ui:ReloadDocument'; } - /** - * Set the node - * - * @param NodeInterface $node - * @return void - */ - public function setNode(NodeInterface $node) + public function setNode(Node $node): void { $this->node = $node; } - /** - * Get the node - * - * @return NodeInterface - */ - public function getNode() + public function getNode(): ?Node { return $this->node; } - /** - * Get the description - * - * @return string - */ - public function getDescription() + public function getDescription(): string { return sprintf('Reload of current document required.'); } /** * Checks whether this feedback is similar to another - * - * @param FeedbackInterface $feedback - * @return boolean */ public function isSimilarTo(FeedbackInterface $feedback) { @@ -97,17 +66,21 @@ public function isSimilarTo(FeedbackInterface $feedback) /** * Serialize the payload for this feedback * - * @param ControllerContext $controllerContext - * @return mixed + * @return array */ - public function serializePayload(ControllerContext $controllerContext) + public function serializePayload(ControllerContext $controllerContext): array { if (!$this->node) { return []; } - if ($documentNode = $this->nodeService->getClosestDocument($this->node)) { + $nodeInfoHelper = new NodeInfoHelper(); + + $documentNode = $this->contentRepositoryRegistry->subgraphForNode($this->node) + ->findClosestNode($this->node->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_DOCUMENT)); + + if ($documentNode) { return [ - 'uri' => $this->linkingService->createNodeUri($controllerContext, $documentNode, null, null, true) + 'uri' => $nodeInfoHelper->previewUri($documentNode, $controllerContext->getRequest()) ]; } diff --git a/Classes/Domain/Model/Feedback/Operations/RemoveNode.php b/Classes/Domain/Model/Feedback/Operations/RemoveNode.php index 6eac0a3bfa..826da07d67 100644 --- a/Classes/Domain/Model/Feedback/Operations/RemoveNode.php +++ b/Classes/Domain/Model/Feedback/Operations/RemoveNode.php @@ -11,35 +11,53 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\ControllerContext; +use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface; use Neos\Neos\Ui\Domain\Model\AbstractFeedback; use Neos\Neos\Ui\Domain\Model\FeedbackInterface; +/** + * @internal + */ class RemoveNode extends AbstractFeedback { + protected Node $node; + + protected Node $parentNode; + + private NodeAddress $nodeAddress; + + private NodeAddress $parentNodeAddress; + /** - * @var NodeInterface + * @Flow\Inject + * @var NodeLabelGeneratorInterface */ - protected $node; + protected $nodeLabelGenerator; /** - * Set the node - * - * @param NodeInterface $node - * @return void + * @Flow\Inject + * @var ContentRepositoryRegistry */ - public function setNode(NodeInterface $node) + protected $contentRepositoryRegistry; + + public function __construct(Node $node, Node $parentNode) { $this->node = $node; + $this->parentNode = $parentNode; } - /** - * Get the node - * - * @return NodeInterface - */ - public function getNode() + protected function initializeObject(): void + { + $this->nodeAddress = NodeAddress::fromNode($this->node); + $this->parentNodeAddress = NodeAddress::fromNode($this->parentNode); + } + + public function getNode(): Node { return $this->node; } @@ -59,9 +77,9 @@ public function getType() * * @return string */ - public function getDescription() + public function getDescription(): string { - return sprintf('Node "%s" has been removed.', $this->getNode()->getLabel()); + return sprintf('Node "%s" has been removed.', $this->nodeLabelGenerator->getLabel($this->getNode())); } /** @@ -76,7 +94,7 @@ public function isSimilarTo(FeedbackInterface $feedback) return false; } - return $this->getNode()->getContextPath() === $feedback->getNode()->getContextPath(); + return $this->getNode()->equals($feedback->getNode()); } /** @@ -88,8 +106,8 @@ public function isSimilarTo(FeedbackInterface $feedback) public function serializePayload(ControllerContext $controllerContext) { return [ - 'contextPath' => $this->getNode()->getContextPath(), - 'parentContextPath' => $this->getNode()->getParent()->getContextPath() + 'contextPath' => $this->nodeAddress->toJson(), + 'parentContextPath' => $this->parentNodeAddress->toJson() ]; } } diff --git a/Classes/Domain/Model/Feedback/Operations/RenderContentOutOfBand.php b/Classes/Domain/Model/Feedback/Operations/RenderContentOutOfBand.php index 92a5baf74b..04cefb0642 100644 --- a/Classes/Domain/Model/Feedback/Operations/RenderContentOutOfBand.php +++ b/Classes/Domain/Model/Feedback/Operations/RenderContentOutOfBand.php @@ -1,5 +1,4 @@ node = $node; } - /** - * Get the node - * - * @return NodeInterface - */ - public function getNode() + public function getNode(): ?Node { return $this->node; } - /** - * Set the parent node dom address - * - * @param ?RenderedNodeDomAddress $parentDomAddress - * @return void - */ - public function setParentDomAddress(?RenderedNodeDomAddress $parentDomAddress = null) + public function setParentDomAddress(?RenderedNodeDomAddress $parentDomAddress = null): void { $this->parentDomAddress = $parentDomAddress; } - /** - * Get the parent node dom address - * - * @return RenderedNodeDomAddress - */ - public function getParentDomAddress() + public function getParentDomAddress(): ?RenderedNodeDomAddress { return $this->parentDomAddress; } - /** - * Set the sibling node dom address - * - * @param ?RenderedNodeDomAddress $siblingDomAddress - * @return void - */ - public function setSiblingDomAddress(?RenderedNodeDomAddress $siblingDomAddress = null) + public function setSiblingDomAddress(?RenderedNodeDomAddress $siblingDomAddress = null): void { $this->siblingDomAddress = $siblingDomAddress; } - /** - * Get the sibling node dom address - * - * @return RenderedNodeDomAddress - */ - public function getSiblingDomAddress() + public function getSiblingDomAddress(): ?RenderedNodeDomAddress { return $this->siblingDomAddress; } /** * Set the insertion mode (before|after|into) - * - * @param string $mode - * @return void */ - public function setMode($mode) + public function setMode(string $mode): void { $this->mode = $mode; } /** * Get the insertion mode (before|after|into) - * - * @return string */ - public function getMode() + public function getMode(): ?string { return $this->mode; } - /** - * Get the type identifier - * - * @return string - */ - public function getType() + public function getType(): string { return 'Neos.Neos.Ui:RenderContentOutOfBand'; } - /** - * Get the description - * - * @return string - */ - public function getDescription() + public function getDescription(): string { - return sprintf('Rendering of node "%s" required.', $this->getNode()->getPath()); + return sprintf('Rendering of node "%s" required.', $this->node?->aggregateId->value); } /** * Checks whether this feedback is similar to another - * - * @param FeedbackInterface $feedback - * @return boolean */ - public function isSimilarTo(FeedbackInterface $feedback) + public function isSimilarTo(FeedbackInterface $feedback): bool { if (!$feedback instanceof RenderContentOutOfBand) { return false; } + if (is_null($this->node)) { + return false; + } + $feedbackNode = $feedback->getNode(); + if (is_null($feedbackNode)) { + return false; + } - return ( - $this->getNode()->getContextPath() === $feedback->getNode()->getContextPath() && - $this->getReferenceData() == $feedback->getReferenceData() - ); + return $this->node->equals($feedbackNode); + // @todo what's this? && $this->getReferenceData() == $feedback->getReferenceData() } /** * Serialize the payload for this feedback * - * @return mixed + * @return array */ - public function serializePayload(ControllerContext $controllerContext) + public function serializePayload(ControllerContext $controllerContext): array { - return [ - 'contextPath' => $this->getNode()->getContextPath(), - 'parentDomAddress' => $this->getParentDomAddress(), - 'siblingDomAddress' => $this->getSiblingDomAddress(), - 'mode' => $this->getMode(), - 'renderedContent' => $this->renderContent($controllerContext) - ]; + if (!is_null($this->node)) { + return [ + 'contextPath' => NodeAddress::fromNode($this->node)->toJson(), + 'parentDomAddress' => $this->getParentDomAddress(), + 'siblingDomAddress' => $this->getSiblingDomAddress(), + 'mode' => $this->getMode(), + 'renderedContent' => $this->renderContent($controllerContext) + ]; + } + return []; } /** * Render the node - * - * @param ControllerContext $controllerContext - * @return string */ - protected function renderContent(ControllerContext $controllerContext) + protected function renderContent(ControllerContext $controllerContext): string { - $cacheTags = $this->cachingHelper->nodeTag($this->getNode()->getParent()); - foreach ($cacheTags as $tag) { - $this->contentCache->flushByTag($tag); + if (is_null($this->node)) { + return ''; + } + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($this->node); + $parentNode = $subgraph->findParentNode($this->node->aggregateId); + if ($parentNode) { + $cacheTags = $this->cachingHelper->nodeTag($parentNode); + foreach ($cacheTags as $tag) { + $this->contentCache->flushByTag($tag); + } + + $parentDomAddress = $this->getParentDomAddress(); + if ($parentDomAddress) { + $renderingMode = $this->renderingModeService->findByCurrentUser(); + + $view = $this->outOfBandRenderingViewFactory->resolveView(); + if (method_exists($view, 'setControllerContext')) { + // deprecated + $view->setControllerContext($controllerContext); + } + $view->setOption('renderingModeName', $renderingMode->name); + + $view->assign('value', $parentNode); + $view->setRenderingEntryPoint($parentDomAddress->getFusionPath()); + + $content = $view->render(); + if ($content instanceof ResponseInterface) { + // todo should not happen, as we never render a full Neos.Neos:Page here? + return $content->getBody()->getContents(); + } + return $content->getContents(); + } } - $parentDomAddress = $this->getParentDomAddress(); - - $fusionView = new FusionView(); - $fusionView->setControllerContext($controllerContext); - - $fusionView->assign('value', $this->getNode()->getParent()); - $fusionView->setFusionPath($parentDomAddress->getFusionPath()); - - return $fusionView->render(); + return ''; } - public function serialize(ControllerContext $controllerContext) + /** + * @return array + */ + public function serialize(ControllerContext $controllerContext): array { try { return parent::serialize($controllerContext); diff --git a/Classes/Domain/Model/Feedback/Operations/UpdateNodeInfo.php b/Classes/Domain/Model/Feedback/Operations/UpdateNodeInfo.php index d982abfcc1..5d1dbd9ae4 100644 --- a/Classes/Domain/Model/Feedback/Operations/UpdateNodeInfo.php +++ b/Classes/Domain/Model/Feedback/Operations/UpdateNodeInfo.php @@ -11,19 +11,23 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Neos\Ui\Domain\Model\AbstractFeedback; use Neos\Neos\Ui\Domain\Model\FeedbackInterface; use Neos\Neos\Ui\Fusion\Helper\NodeInfoHelper; +/** + * @internal + */ class UpdateNodeInfo extends AbstractFeedback { - /** - * @var NodeInterface - */ - protected $node; + protected Node $node; /** * @Flow\Inject @@ -31,125 +35,99 @@ class UpdateNodeInfo extends AbstractFeedback */ protected $nodeInfoHelper; - protected $isRecursive = false; - - protected $baseNodeType = null; - /** - * Set the baseNodeType - * - * @param string|null $baseNodeType + * @Flow\Inject + * @var ContentRepositoryRegistry */ + protected $contentRepositoryRegistry; + + protected bool $isRecursive = false; + + protected ?string $baseNodeType = null; + public function setBaseNodeType(?string $baseNodeType): void { $this->baseNodeType = $baseNodeType; } - /** - * Get the baseNodeType - * - * @return string|null - */ public function getBaseNodeType(): ?string { return $this->baseNodeType; } - /** - * Set the node - * - * @param NodeInterface $node - * @return void - */ - public function setNode(NodeInterface $node) + public function setNode(Node $node): void { $this->node = $node; } /** * Update node infos recursively - * - * @return void */ - public function recursive() + public function recursive(): void { $this->isRecursive = true; } - /** - * Get the node - * - * @return NodeInterface - */ - public function getNode() + public function getNode(): Node { return $this->node; } - /** - * Get the type identifier - * - * @return string - */ - public function getType() + public function getType(): string { return 'Neos.Neos.Ui:UpdateNodeInfo'; } - /** - * Get the description - * - * @return string - */ - public function getDescription() + public function getDescription(): string { - return sprintf('Updated info for node "%s" is available.', $this->getNode()->getContextPath()); + return sprintf('Updated info for node "%s" is available.', $this->node->aggregateId->value); } /** * Checks whether this feedback is similar to another - * - * @param FeedbackInterface $feedback - * @return boolean */ - public function isSimilarTo(FeedbackInterface $feedback) + public function isSimilarTo(FeedbackInterface $feedback): bool { if (!$feedback instanceof UpdateNodeInfo) { return false; } - return $this->getNode()->getContextPath() === $feedback->getNode()->getContextPath(); + return $this->getNode()->equals($feedback->getNode()); } /** * Serialize the payload for this feedback * - * @param ControllerContext $controllerContext - * @return mixed + * @return array */ - public function serializePayload(ControllerContext $controllerContext) + public function serializePayload(ControllerContext $controllerContext): array { return [ - 'byContextPath' => $this->serializeNodeRecursively($this->getNode(), $controllerContext) + 'byContextPath' => $this->serializeNodeRecursively($this->node, $controllerContext->getRequest()) ]; } /** * Serialize node and all child nodes * - * @param NodeInterface $node - * @param ControllerContext $controllerContext - * @return array + * @return array> */ - public function serializeNodeRecursively(NodeInterface $node, ControllerContext $controllerContext) + private function serializeNodeRecursively(Node $node, ActionRequest $actionRequest): array { + $contentRepository = $this->contentRepositoryRegistry->get($node->contentRepositoryId); + $result = [ - $node->getContextPath() => $this->nodeInfoHelper->renderNodeWithPropertiesAndChildrenInformation($node, $controllerContext, $this->baseNodeType) + NodeAddress::fromNode($node)->toJson() + => $this->nodeInfoHelper->renderNodeWithPropertiesAndChildrenInformation( + $node, + $actionRequest + ) ]; if ($this->isRecursive === true) { - foreach ($node->getChildNodes() as $childNode) { - $result = array_merge($result, $this->serializeNodeRecursively($childNode, $controllerContext)); + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); + foreach ($subgraph->findChildNodes($node->aggregateId, FindChildNodesFilter::create()) as $childNode) { + $result = array_merge($result, $this->serializeNodeRecursively($childNode, $actionRequest)); } } diff --git a/Classes/Domain/Model/Feedback/Operations/UpdateNodePath.php b/Classes/Domain/Model/Feedback/Operations/UpdateNodePath.php index 10902a0ad6..50538adeee 100644 --- a/Classes/Domain/Model/Feedback/Operations/UpdateNodePath.php +++ b/Classes/Domain/Model/Feedback/Operations/UpdateNodePath.php @@ -15,6 +15,9 @@ use Neos\Neos\Ui\Domain\Model\AbstractFeedback; use Neos\Neos\Ui\Domain\Model\FeedbackInterface; +/** + * @internal + */ class UpdateNodePath extends AbstractFeedback { /** diff --git a/Classes/Domain/Model/Feedback/Operations/UpdateNodePreviewUrl.php b/Classes/Domain/Model/Feedback/Operations/UpdateNodePreviewUrl.php index ca4c5dfb89..5e136e3ec4 100644 --- a/Classes/Domain/Model/Feedback/Operations/UpdateNodePreviewUrl.php +++ b/Classes/Domain/Model/Feedback/Operations/UpdateNodePreviewUrl.php @@ -11,26 +11,45 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\ControllerContext; +use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface; use Neos\Neos\Ui\Domain\Model\AbstractFeedback; use Neos\Neos\Ui\Domain\Model\FeedbackInterface; use Neos\Neos\Ui\Fusion\Helper\NodeInfoHelper; +/** + * @internal + */ class UpdateNodePreviewUrl extends AbstractFeedback { /** - * @var NodeInterface + * @var Node */ protected $node; + /** + * @Flow\Inject + * @var ContentRepositoryRegistry + */ + protected $contentRepositoryRegistry; + + /** + * @Flow\Inject + * @var NodeLabelGeneratorInterface + */ + protected $nodeLabelGenerator; + /** * Set the node * - * @param NodeInterface $node + * @param Node $node * @return void */ - public function setNode(NodeInterface $node) + public function setNode(Node $node) { $this->node = $node; } @@ -38,7 +57,7 @@ public function setNode(NodeInterface $node) /** * Get the node * - * @return NodeInterface + * @return Node */ public function getNode() { @@ -62,7 +81,7 @@ public function getType() */ public function getDescription() { - return sprintf('The "preview URL" of node "%s" has been changed potentially.', $this->getNode()->getLabel()); + return sprintf('The "preview URL" of node "%s" has been changed potentially.', $this->nodeLabelGenerator->getLabel($this->getNode())); } /** @@ -76,24 +95,24 @@ public function isSimilarTo(FeedbackInterface $feedback) if (!$feedback instanceof UpdateNodePreviewUrl) { return false; } - return $this->getNode()->getContextPath() === $feedback->getNode()->getContextPath(); + return $this->getNode()->equals($feedback->getNode()); } /** * Serialize the payload for this feedback * * @param ControllerContext $controllerContext - * @return array + * @return array */ - public function serializePayload(ControllerContext $controllerContext) + public function serializePayload(ControllerContext $controllerContext): array { if ($this->node === null) { $newPreviewUrl = ''; $contextPath = ''; } else { $nodeInfoHelper = new NodeInfoHelper(); - $newPreviewUrl = $nodeInfoHelper->createRedirectToNode($controllerContext, $this->node); - $contextPath = $this->node->getContextPath(); + $newPreviewUrl = $nodeInfoHelper->createRedirectToNode($this->node, $controllerContext->getRequest()); + $contextPath = NodeAddress::fromNode($this->node)->toJson(); } return [ 'newPreviewUrl' => $newPreviewUrl, diff --git a/Classes/Domain/Model/Feedback/Operations/UpdateWorkspaceInfo.php b/Classes/Domain/Model/Feedback/Operations/UpdateWorkspaceInfo.php index 2e5f16fcc5..d123ae7e15 100644 --- a/Classes/Domain/Model/Feedback/Operations/UpdateWorkspaceInfo.php +++ b/Classes/Domain/Model/Feedback/Operations/UpdateWorkspaceInfo.php @@ -11,52 +11,48 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\Workspace; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\ControllerContext; -use Neos\Neos\Ui\ContentRepository\Service\WorkspaceService; +use Neos\Neos\Ui\ContentRepository\Service\WorkspaceService as UiWorkspaceService; use Neos\Neos\Ui\Domain\Model\AbstractFeedback; use Neos\Neos\Ui\Domain\Model\FeedbackInterface; -use Neos\Neos\Domain\Service\UserService as DomainUserService; +/** + * @internal + */ class UpdateWorkspaceInfo extends AbstractFeedback { - /** - * @var Workspace - */ - protected $workspace; - /** * @Flow\Inject - * @var WorkspaceService + * @var ContentRepositoryRegistry */ - protected $workspaceService; + protected $contentRepositoryRegistry; /** * @Flow\Inject - * @var DomainUserService + * @var UiWorkspaceService */ - protected $domainUserService; + protected $uiWorkspaceService; /** - * Set the workspace + * UpdateWorkspaceInfo constructor. * - * @param Workspace $workspace - * @return void */ - public function setWorkspace(Workspace $workspace) - { - $this->workspace = $workspace; + public function __construct( + private readonly ContentRepositoryId $contentRepositoryId, + private readonly WorkspaceName $workspaceName + ) { } /** - * Get the document - * - * @return Workspace + * Getter for WorkspaceName */ - public function getWorkspace() + public function getWorkspaceName(): WorkspaceName { - return $this->workspace; + return $this->workspaceName; } /** @@ -74,7 +70,7 @@ public function getType() * * @return string */ - public function getDescription() + public function getDescription(): string { return sprintf('New workspace info available.'); } @@ -90,24 +86,30 @@ public function isSimilarTo(FeedbackInterface $feedback) if (!$feedback instanceof UpdateWorkspaceInfo) { return false; } - - return $this->getWorkspace() === $feedback->getWorkspace(); + $feedbackWorkspaceName = $feedback->getWorkspaceName(); + return $feedbackWorkspaceName !== null && $this->getWorkspaceName()->equals($feedbackWorkspaceName); } /** * Serialize the payload for this feedback * + * @param ControllerContext $controllerContext * @return mixed */ public function serializePayload(ControllerContext $controllerContext) { - $workspace = $this->getWorkspace(); - $baseWorkspace = $workspace->getBaseWorkspace(); + $contentRepository = $this->contentRepositoryRegistry->get($this->contentRepositoryId); + $workspace = $contentRepository->findWorkspaceByName($this->workspaceName); + if ($workspace === null) { + return null; + } + $publishableNodes = $this->uiWorkspaceService->getPublishableNodeInfo($workspace->workspaceName, $contentRepository->id); return [ - 'name' => $workspace->getName(), - 'publishableNodes' => $this->workspaceService->getPublishableNodeInfo($workspace), - 'baseWorkspace' => $baseWorkspace->getName(), - 'readOnly' => !$this->domainUserService->currentUserCanPublishToWorkspace($baseWorkspace) + 'name' => $this->workspaceName->value, + 'totalNumberOfChanges' => count($publishableNodes), + 'publishableNodes' => $publishableNodes, + 'baseWorkspace' => $workspace->baseWorkspaceName?->value, + 'status' => $workspace->status->value, ]; } } diff --git a/Classes/Domain/Model/FeedbackCollection.php b/Classes/Domain/Model/FeedbackCollection.php index 1020975cc5..8fdbcbde11 100644 --- a/Classes/Domain/Model/FeedbackCollection.php +++ b/Classes/Domain/Model/FeedbackCollection.php @@ -16,6 +16,7 @@ /** * @Flow\Scope("singleton") + * @internal */ class FeedbackCollection implements \JsonSerializable { @@ -48,8 +49,9 @@ public function setControllerContext(ControllerContext $controllerContext) */ public function add(FeedbackInterface $feedback) { - foreach ($this->feedbacks as $value) { - if ($value->isSimilarTo($feedback)) { + foreach ($this->feedbacks as $i => $value) { + if ($feedback->isSimilarTo($value)) { + $this->feedbacks[$i] = $feedback; return; } } @@ -60,10 +62,9 @@ public function add(FeedbackInterface $feedback) /** * Serialize collection to `json_encode`able array * - * @return array + * @return array */ - #[\ReturnTypeWillChange] - public function jsonSerialize() + public function jsonSerialize(): array { $feedbacks = []; @@ -77,7 +78,7 @@ public function jsonSerialize() ]; } - public function reset() + public function reset(): void { $this->feedbacks = []; } diff --git a/Classes/Domain/Model/FeedbackInterface.php b/Classes/Domain/Model/FeedbackInterface.php index 4ca4bd4f97..dddbe5f44c 100644 --- a/Classes/Domain/Model/FeedbackInterface.php +++ b/Classes/Domain/Model/FeedbackInterface.php @@ -13,6 +13,9 @@ use Neos\Flow\Mvc\Controller\ControllerContext; +/** + * @internal + */ interface FeedbackInterface { /** @@ -20,9 +23,9 @@ interface FeedbackInterface * in AbstractFeedback, but can be overridden to implement fallback logic in case of errors. * * @param ControllerContext $controllerContext - * @return array + * @return array */ - public function serialize(ControllerContext $controllerContext); + public function serialize(ControllerContext $controllerContext): array; /** * Get the type identifier diff --git a/Classes/Domain/Model/ReferencingChangeInterface.php b/Classes/Domain/Model/ReferencingChangeInterface.php index 9ba886be33..e7eea0f535 100644 --- a/Classes/Domain/Model/ReferencingChangeInterface.php +++ b/Classes/Domain/Model/ReferencingChangeInterface.php @@ -11,25 +11,26 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; /** * A change that needs to reference another node + * @internal */ interface ReferencingChangeInterface extends ChangeInterface { /** * Set the reference * - * @param NodeInterface $reference + * @param Node $reference * @return void */ - public function setReference(NodeInterface $reference); + public function setReference(Node $reference); /** * Get the reference * - * @return NodeInterface + * @return Node */ public function getReference(); } diff --git a/Classes/Domain/Model/RenderedNodeDomAddress.php b/Classes/Domain/Model/RenderedNodeDomAddress.php index 95264deaaf..b13e8fcd74 100644 --- a/Classes/Domain/Model/RenderedNodeDomAddress.php +++ b/Classes/Domain/Model/RenderedNodeDomAddress.php @@ -13,6 +13,7 @@ /** * Data to address rendered nodes within the DOM + * @internal */ class RenderedNodeDomAddress implements \JsonSerializable { @@ -83,6 +84,7 @@ public function getFusionPath() public function getFusionPathForContentRendering(): string { $fusionPathForContentRendering = $this->getFusionPath(); + /** @var string $fusionPathForContentRendering */ $fusionPathForContentRendering = preg_replace( '/(\/itemRenderer)\/([^<>\/]+)\/element(<[^>]+>)$/', '$1', @@ -95,10 +97,9 @@ public function getFusionPathForContentRendering(): string /** * Serialize to json * - * @return array + * @return array */ - #[\ReturnTypeWillChange] - public function jsonSerialize(): mixed + public function jsonSerialize(): array { return [ 'contextPath' => $this->getContextPath(), diff --git a/Classes/Domain/NodeCreation/NodeCreationCommands.php b/Classes/Domain/NodeCreation/NodeCreationCommands.php new file mode 100644 index 0000000000..8ebdcf2e0a --- /dev/null +++ b/Classes/Domain/NodeCreation/NodeCreationCommands.php @@ -0,0 +1,129 @@ +getContentGraph()->getSubgraph( + * $commands->first->contentStreamId, + * $commands->first->originDimensionSpacePoint->toDimensionSpacePoint(), + * VisibilityConstraints::frontend() + * ); + * $parentNode = $subgraph->findNodeById($commands->first->parentNodeAggregateId); + * + * @Flow\Proxy(false) + * @implements \IteratorAggregate + * @internal Especially the constructors + */ +final readonly class NodeCreationCommands implements \IteratorAggregate +{ + /** + * The initial node creation command. + * It is only allowed to change its properties via {@see self::withInitialPropertyValues()} + */ + public CreateNodeAggregateWithNode $first; + + /** + * Add a list of commands that are executed after the initial created command was run. + * This allows to create child-nodes and append other allowed commands. + */ + public Commands $additionalCommands; + + private function __construct( + CreateNodeAggregateWithNode $first, + Commands $additionalCommands + ) { + $this->first = $first; + $this->additionalCommands = $additionalCommands; + } + + /** + * @internal to guarantee that the initial create command is mostly preserved as intended. + * You can use {@see self::withInitialPropertyValues()} to add new properties of the to be created node. + */ + public static function fromFirstCommand( + CreateNodeAggregateWithNode $first, + NodeTypeManager $nodeTypeManager + ): self { + $tetheredDescendantNodeAggregateIds = NodeAggregateIdsByNodePaths::createForNodeType( + $first->nodeTypeName, + $nodeTypeManager + ); + return new self( + $first->withTetheredDescendantNodeAggregateIds($tetheredDescendantNodeAggregateIds), + Commands::createEmpty() + ); + } + + /** + * Augment the first {@see CreateNodeAggregateWithNode} command with altered properties. + * + * The properties will be completely replaced. + * To merge the properties please use: + * + * $commands->withInitialPropertyValues( + * $commands->first->initialPropertyValues + * ->withValue('album', 'rep') + * ) + * + */ + public function withInitialPropertyValues(PropertyValuesToWrite $newInitialPropertyValues): self + { + return new self( + $this->first->withInitialPropertyValues($newInitialPropertyValues), + $this->additionalCommands + ); + } + + public function withInitialReferences(NodeReferencesToWrite $newInitialReferences): self + { + return new self( + $this->first->withReferences($newInitialReferences), + $this->additionalCommands + ); + } + + public function withAdditionalCommands( + Commands $additionalCommands + ): self { + return new self($this->first, $this->additionalCommands->merge($additionalCommands)); + } + + /** + * @return \Traversable + */ + public function getIterator(): \Traversable + { + yield from [$this->first, ...$this->additionalCommands]; + } +} diff --git a/Classes/Domain/NodeCreation/NodeCreationElements.php b/Classes/Domain/NodeCreation/NodeCreationElements.php new file mode 100644 index 0000000000..b21392e30d --- /dev/null +++ b/Classes/Domain/NodeCreation/NodeCreationElements.php @@ -0,0 +1,102 @@ + + * @internal Especially the constructor and the serialized data + */ +final readonly class NodeCreationElements implements \IteratorAggregate +{ + /** + * @param array $elementValues + * @param array $serializedValues + * @internal you should not need to construct this + */ + public function __construct( + private array $elementValues, + private array $serializedValues, + ) { + } + + public function has(string $name): bool + { + return isset($this->elementValues[$name]); + } + + /** + * Returns the type according to the element schema + * For elements that refer to a node {@see NodeAggregateIds} will be returned. + */ + public function get(string $name): mixed + { + return $this->elementValues[$name] ?? null; + } + + /** + * @internal returns values formatted by the internal format used for the Ui + * @return \Traversable + */ + public function serialized(): \Traversable + { + yield from $this->serializedValues; + } + + /** + * @return \Traversable + */ + public function getIterator(): \Traversable + { + yield from $this->elementValues; + } +} diff --git a/Classes/Domain/NodeCreation/NodeCreationHandlerFactoryInterface.php b/Classes/Domain/NodeCreation/NodeCreationHandlerFactoryInterface.php new file mode 100644 index 0000000000..a20d84446b --- /dev/null +++ b/Classes/Domain/NodeCreation/NodeCreationHandlerFactoryInterface.php @@ -0,0 +1,14 @@ + */ protected $fusionDefaultEelContext; /** * @Flow\InjectConfiguration(path="configurationDefaultEelContext") - * @var array + * @var array */ protected $additionalEelDefaultContext; /** - * @param array $configuration - * @param array $context - * @return array + * @param array $configuration + * @param array $context + * @return array * @throws \Neos\Eel\Exception */ public function computeConfiguration(array $configuration, array $context): array @@ -53,11 +54,11 @@ public function computeConfiguration(array $configuration, array $context): arra } /** - * @param array $adjustedConfiguration - * @param array $context + * @param array $adjustedConfiguration + * @param array $context * @throws \Neos\Eel\Exception */ - protected function computeConfigurationInternally(array &$adjustedConfiguration, array $context) + protected function computeConfigurationInternally(array &$adjustedConfiguration, array $context): void { foreach ($adjustedConfiguration as $key => &$value) { if (is_array($value)) { diff --git a/Classes/Domain/Service/NodePropertyConversionService.php b/Classes/Domain/Service/NodePropertyConversionService.php index 16da7bfd59..7266c39e8a 100644 --- a/Classes/Domain/Service/NodePropertyConversionService.php +++ b/Classes/Domain/Service/NodePropertyConversionService.php @@ -11,19 +11,20 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Model\NodeType; -use Neos\ContentRepository\Domain\Service\Context; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\MvcPropertyMappingConfiguration; use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Flow\Property\PropertyMapper; use Neos\Flow\Property\TypeConverter\PersistentObjectConverter; +use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationElements; use Neos\Utility\Exception\InvalidTypeException; use Neos\Utility\TypeHandling; /** * @Flow\Scope("singleton") + * @internal */ class NodePropertyConversionService { @@ -42,29 +43,29 @@ class NodePropertyConversionService /** * Convert raw property values to the correct type according to a node type configuration * - * @param NodeType $nodeType - * @param string $propertyName - * @param string $rawValue - * @param Context $context - * @return mixed + * @param string|array|null $rawValue */ - public function convert(NodeType $nodeType, $propertyName, $rawValue, Context $context) + public function convert(string $propertyType, string|array|null $rawValue): mixed { - $propertyType = $nodeType->getPropertyType($propertyName); + if (is_null($rawValue)) { + return null; + } + $propertyType = TypeHandling::normalizeType($propertyType); switch ($propertyType) { case 'string': return $rawValue; case 'reference': - return $this->convertReference($rawValue, $context); - case 'references': - return $this->convertReferences($rawValue, $context); + throw new \Exception("not implemented here, must be handled outside."); case 'DateTime': return $this->convertDateTime($rawValue); + case 'float': + return $this->convertFloat($rawValue); + case 'integer': return $this->convertInteger($rawValue); @@ -84,80 +85,97 @@ public function convert(NodeType $nodeType, $propertyName, $rawValue, Context $c } } - if ((is_string($rawValue) || is_array($rawValue)) && $this->objectManager->isRegistered($innerType) && $rawValue !== '') { + if ($this->objectManager->isRegistered($innerType) && $rawValue !== '') { $propertyMappingConfiguration = new MvcPropertyMappingConfiguration(); $propertyMappingConfiguration->allowOverrideTargetType(); $propertyMappingConfiguration->allowAllProperties(); $propertyMappingConfiguration->skipUnknownProperties(); - $propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_MODIFICATION_ALLOWED, true); - $propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_CREATION_ALLOWED, true); + $propertyMappingConfiguration->setTypeConverterOption( + PersistentObjectConverter::class, + PersistentObjectConverter::CONFIGURATION_MODIFICATION_ALLOWED, + true + ); + $propertyMappingConfiguration->setTypeConverterOption( + PersistentObjectConverter::class, + PersistentObjectConverter::CONFIGURATION_CREATION_ALLOWED, + true + ); return $this->propertyMapper->convert($rawValue, $propertyType, $propertyMappingConfiguration); } else { + if ($rawValue === '') { + // bugfix for https://github.com/neos/neos-development-collection/issues/4062 + return null; + } return $rawValue; } } } /** - * Convert raw value to reference - * - * @param string $rawValue - * @param Context $context - * @return NodeInterface + * @param array $data */ - protected function convertReference($rawValue, Context $context) + public function convertNodeCreationElements(NodeType $nodeType, array $data): NodeCreationElements { - return $context->getNodeByIdentifier($rawValue); + $convertedElements = []; + /** @var string $elementName */ + foreach ($nodeType->getConfiguration('ui.creationDialog.elements') ?? [] as $elementName => $elementConfiguration) { + $rawValue = $data[$elementName] ?? null; + if ($rawValue === null) { + continue; + } + $propertyType = $elementConfiguration['type'] ?? 'string'; + if ($propertyType === 'references' || $propertyType === 'reference') { + $nodeAggregateIds = []; + if (is_string($rawValue) && !empty($rawValue)) { + $nodeAggregateIds = [$rawValue]; + } elseif (is_array($rawValue)) { + $nodeAggregateIds = $rawValue; + } + $convertedElements[$elementName] = NodeAggregateIds::fromArray($nodeAggregateIds); + continue; + } + $convertedElements[$elementName] = $this->convert($propertyType, $rawValue); + } + + return new NodeCreationElements(elementValues: $convertedElements, serializedValues: $data); } /** - * Convert raw value to references + * Convert raw value to \DateTime * - * @param string $rawValue - * @param Context $context - * @return array + * @param string|array $rawValue */ - protected function convertReferences($rawValue, Context $context) + protected function convertDateTime(string|array $rawValue): ?\DateTime { - $nodeIdentifiers = $rawValue; - $result = []; - - if (is_array($nodeIdentifiers)) { - foreach ($nodeIdentifiers as $nodeIdentifier) { - $referencedNode = $context->getNodeByIdentifier($nodeIdentifier); - if ($referencedNode !== null) { - $result[] = $referencedNode; - } - } + if (is_string($rawValue) && $rawValue !== '') { + return (\DateTime::createFromFormat(\DateTime::W3C, $rawValue) ?: null) + ?->setTimezone(new \DateTimeZone(date_default_timezone_get())); } - return $result; + return null; } /** - * Convert raw value to \DateTime + * Convert raw value to float * - * @param string $rawValue - * @return \DateTime|null + * @param string|array $rawValue */ - protected function convertDateTime($rawValue) + protected function convertFloat(string|array $rawValue): ?float { - if ($rawValue !== '') { - $result = \DateTime::createFromFormat(\DateTime::W3C, $rawValue); - $result->setTimezone(new \DateTimeZone(date_default_timezone_get())); - - return $result; + if (is_numeric($rawValue)) { + return (float)$rawValue; } - } + return null; + } + /** * Convert raw value to integer * - * @param mixed $rawValue - * @return null|integer + * @param string|array $rawValue */ - protected function convertInteger($rawValue) + protected function convertInteger(string|array $rawValue): ?int { if (is_numeric($rawValue)) { return (int) $rawValue; @@ -169,10 +187,9 @@ protected function convertInteger($rawValue) /** * Convert raw value to boolean * - * @param mixed $rawValue - * @return boolean + * @param string|array $rawValue */ - protected function convertBoolean($rawValue) + protected function convertBoolean(string|array $rawValue): bool { if (is_string($rawValue) && strtolower($rawValue) === 'false') { return false; @@ -184,10 +201,10 @@ protected function convertBoolean($rawValue) /** * Convert raw value to array * - * @param string|array $rawValue - * @return array + * @param string|array $rawValue + * @return array */ - protected function convertArray($rawValue) + protected function convertArray(string|array $rawValue): array { if (is_string($rawValue)) { return json_decode($rawValue, true); diff --git a/Classes/Domain/Service/NodePropertyConverterService.php b/Classes/Domain/Service/NodePropertyConverterService.php new file mode 100644 index 0000000000..a4e5b399a1 --- /dev/null +++ b/Classes/Domain/Service/NodePropertyConverterService.php @@ -0,0 +1,329 @@ +> + */ + protected $typesConfiguration; + + /** + * @Flow\Inject + * @var ObjectManagerInterface + */ + protected $objectManager; + + /** + * @Flow\Inject + * @var PropertyMapper + */ + protected $propertyMapper; + + /** + * @Flow\Transient + * @var array + */ + protected $generatedPropertyMappingConfigurations = []; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var ThrowableStorageInterface + */ + private $throwableStorage; + + /** + * @param LoggerInterface $logger + */ + public function injectLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } + + /** + * @param ThrowableStorageInterface $throwableStorage + */ + public function injectThrowableStorage(ThrowableStorageInterface $throwableStorage): void + { + $this->throwableStorage = $throwableStorage; + } + + /** + * @return list|string|null + */ + private function getReference(Node $node, string $referenceName): array|string|null + { + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); + $references = $subgraph->findReferences( + $node->aggregateId, + FindReferencesFilter::create(referenceName: $referenceName) + ); + + $referenceIdentifiers = []; + foreach ($references as $reference) { + $referenceIdentifiers[] = $reference->node->aggregateId->value; + } + + $maxItems = $this->getNodeType($node)->getReferences()[$referenceName]['constraints']['maxItems'] ?? null; + + if ($maxItems === 1) { + // special handling to simulate old single reference behaviour. + // todo should be adjusted in the ui + if (count($referenceIdentifiers) === 0) { + return null; + } else { + return reset($referenceIdentifiers); + } + } + return $referenceIdentifiers; + } + + /** + * Get a single property reduced to a simple type (no objects) representation + */ + private function getProperty(Node $node, string $propertyName): mixed + { + if ($propertyName === '_hidden') { + return $node->tags->withoutInherited()->contain(SubtreeTag::fromString('disabled')); + } + + $propertyValue = $node->getProperty($propertyName); + $propertyType = $this->getNodeType($node)->getPropertyType($propertyName); + try { + $convertedValue = $this->convertValue($propertyValue, $propertyType); + } catch (PropertyException $exception) { + $logMessage = $this->throwableStorage->logThrowable($exception); + $this->logger->error($logMessage, LogEnvironment::fromMethodName(__METHOD__)); + $convertedValue = null; + } + + if ($convertedValue === null) { + $convertedValue = $this->getDefaultValueForProperty($this->getNodeType($node), $propertyName); + if ($convertedValue !== null) { + try { + $convertedValue = $this->convertValue($convertedValue, $propertyType); + } catch (PropertyException $exception) { + $logMessage = $this->throwableStorage->logThrowable($exception); + $this->logger->error($logMessage, LogEnvironment::fromMethodName(__METHOD__)); + $convertedValue = null; + } + } + } + + return $convertedValue; + } + + /** + * Get all properties and references stuff reduced to simple type (no objects) representations in an array + * + * @param Node $node + * @return array + */ + public function getPropertiesArray(Node $node) + { + $properties = []; + foreach ($this->getNodeType($node)->getProperties() as $propertyName => $_) { + if ($propertyName[0] === '_' && $propertyName[1] === '_') { + // skip fully-private properties + continue; + } + + $properties[$propertyName] = $this->getProperty($node, $propertyName); + } + foreach ($this->getNodeType($node)->getReferences() as $referenceName => $_) { + $properties[$referenceName] = $this->getReference($node, $referenceName); + } + return $properties; + } + + /** + * Convert the given value to a simple type or an array of simple types. + * + * @param mixed $propertyValue + * @param string $dataType + * @return mixed + * @throws PropertyException + */ + protected function convertValue($propertyValue, $dataType) + { + $parsedType = TypeHandling::parseType($dataType); + + // This hardcoded handling is to circumvent rewriting PropertyMappers that convert objects. + // Usually they expect the source to be an object already and break if not. + if ( + !TypeHandling::isSimpleType($parsedType['type']) && !is_object($propertyValue) + && !is_array($propertyValue) + ) { + return null; + } + + $conversionTargetType = $parsedType['type']; + if (!TypeHandling::isSimpleType($parsedType['type'])) { + $conversionTargetType = 'array'; + } + // Technically the "string" hardcoded here is irrelevant as the configured type converter wins, + // but it cannot be the "elementType" because if the source is of the type $elementType + // then the PropertyMapper skips the type converter. + if ($parsedType['type'] === 'array' && $parsedType['elementType'] !== null) { + $conversionTargetType .= '<' + . (TypeHandling::isSimpleType($parsedType['elementType']) ? $parsedType['elementType'] : 'string') + . '>'; + } + + $propertyMappingConfiguration = $this->createConfiguration($dataType); + $convertedValue = $this->propertyMapper->convert( + $propertyValue, + $conversionTargetType, + $propertyMappingConfiguration + ); + if ($convertedValue instanceof \Neos\Error\Messages\Error) { + throw new PropertyException($convertedValue->getMessage(), $convertedValue->getCode()); + } + + return $convertedValue; + } + + /** + * Tries to find a default value for the given property trying: + * 1) The specific property configuration for the given NodeType + * 2) The generic configuration for the property type in settings. + * + * @param NodeType $nodeType + * @param string $propertyName + * @return mixed + */ + protected function getDefaultValueForProperty(NodeType $nodeType, $propertyName) + { + $defaultValues = $nodeType->getDefaultValuesForProperties(); + if (!isset($defaultValues[$propertyName])) { + return null; + } + + return $defaultValues[$propertyName]; + } + + /** + * Create a property mapping configuration for the given dataType to convert a Node property value + * from the given dataType to a simple type. + * + * @param string $dataType + * @return PropertyMappingConfigurationInterface + */ + protected function createConfiguration($dataType) + { + if (!isset($this->generatedPropertyMappingConfigurations[$dataType])) { + $propertyMappingConfiguration = new PropertyMappingConfiguration(); + $propertyMappingConfiguration->allowAllProperties(); + + $parsedType = TypeHandling::parseType($dataType); + + if ($this->setTypeConverterForType($propertyMappingConfiguration, $dataType) === false) { + $this->setTypeConverterForType($propertyMappingConfiguration, $parsedType['type']); + } + + $elementConfiguration = $propertyMappingConfiguration->forProperty('*'); + $this->setTypeConverterForType($elementConfiguration, $parsedType['elementType']); + + $this->generatedPropertyMappingConfigurations[$dataType] = $propertyMappingConfiguration; + } + + return $this->generatedPropertyMappingConfigurations[$dataType]; + } + + protected function setTypeConverterForType( + PropertyMappingConfiguration $propertyMappingConfiguration, + string $dataType = null + ): bool { + $typeConverterClassName = $this->typesConfiguration[$dataType]['typeConverter'] ?? null; + if (!is_string($typeConverterClassName)) { + return false; + } + + $typeConverter = $this->objectManager->get($typeConverterClassName); + if (!$typeConverter instanceof TypeConverterInterface) { + throw new \RuntimeException( + 'Configured class ' . $typeConverterClassName + . ' does not implement the required TypeConverterInterface', + 1645557392 + ); + } + $propertyMappingConfiguration->setTypeConverter($typeConverter); + if (is_string($dataType)) { + $this->setTypeConverterOptionsForType( + $propertyMappingConfiguration, + $typeConverterClassName, + $dataType + ); + } + + return true; + } + + protected function setTypeConverterOptionsForType( + PropertyMappingConfiguration $propertyMappingConfiguration, + string $typeConverterClass, + string $dataType + ): void { + if ( + !isset($this->typesConfiguration[$dataType]['typeConverterOptions']) + || !is_array($this->typesConfiguration[$dataType]['typeConverterOptions']) + ) { + return; + } + + foreach ($this->typesConfiguration[$dataType]['typeConverterOptions'] as $option => $value) { + $propertyMappingConfiguration->setTypeConverterOption($typeConverterClass, $option, $value); + } + } +} diff --git a/Classes/Domain/Service/NodeTreeBuilder.php b/Classes/Domain/Service/NodeTreeBuilder.php deleted file mode 100644 index f1ca6fca71..0000000000 --- a/Classes/Domain/Service/NodeTreeBuilder.php +++ /dev/null @@ -1,235 +0,0 @@ -root = $this->nodeService->getNodeFromContextPath($rootContextPath); - } - - /** - * Get the root node - * - * @return NodeInterface - */ - public function getRoot() - { - if (!$this->root) { - $this->root = $this->active->getContext()->getCurrentSiteNode(); - } - - return $this->root; - } - - /** - * Set the active node - * - * @param string $activeContextPath - * @return void - */ - public function setActive($activeContextPath) - { - $this->active = $this->nodeService->getNodeFromContextPath($activeContextPath); - } - - /** - * Set the node type filter - * - * @param string $nodeTypeFilter - * @return void - */ - public function setNodeTypeFilter($nodeTypeFilter) - { - $this->nodeTypeFilter = $nodeTypeFilter; - } - - /** - * Set the search term filter - * - * @param string $searchTermFilter - * @return void - */ - public function setSearchTermFilter($searchTermFilter) - { - $this->searchTermFilter = $searchTermFilter; - } - - /** - * Set the depth - * - * @param integer $depth - */ - public function setDepth($depth) - { - $this->depth = $depth; - } - - /** - * Set the controller context - * - * @param ControllerContext $controllerContext - * @return void - */ - public function setControllerContext(ControllerContext $controllerContext) - { - $this->controllerContext = $controllerContext; - } - - /** - * Build a json serializable tree structure containing node information - * - * @param bool $includeRoot - * @param null $root - * @param null $depth - * @return array - */ - public function build($includeRoot = false, $root = null, $depth = null) - { - $root = $root === null ? $this->getRoot() : $root; - $depth = $depth === null ? $this->depth : $depth; - - $result = []; - - /** @var NodeInterface $childNode */ - foreach ($root->getChildNodes($this->nodeTypeFilter) as $childNode) { - $hasChildNodes = $childNode->hasChildNodes($this->nodeTypeFilter); - $shouldLoadChildNodes = $hasChildNodes && ($depth > 1 || $this->isInRootLine($this->active, $childNode)); - - $result[$childNode->getName()] = [ - 'label' => $childNode->getNodeType()->isOfType('Neos.Neos:Document') ? - $childNode->getProperty('title') : $childNode->getLabel(), - 'contextPath' => $childNode->getContextPath(), - 'nodeType' => $childNode->getNodeType()->getName(), - 'hasChildren' => $hasChildNodes, - 'isActive' => $this->active && ($childNode->getPath() === $this->active->getPath()), - 'isFocused' => $this->active && ($childNode->getPath() === $this->active->getPath()), - 'isCollapsed' => !$shouldLoadChildNodes, - 'isCollapsable' => $hasChildNodes - ]; - - if ($shouldLoadChildNodes) { - $result[$childNode->getName()]['children'] = - $this->build(false, $childNode, $depth - 1); - } - - if ($childNode->getNodeType()->isOfType('Neos.Neos:Document')) { - $result[$childNode->getName()]['href'] = $this->linkingService->createNodeUri( - /* $controllerContext */ - $this->controllerContext, - /* $node */ - $childNode, - /* $baseNode */ - null, - /* $format */ - null, - /* $absolute */ - true - ); - } - } - - if ($includeRoot) { - return [ - $root->getName() => [ - 'label' => $root->getNodeType()->isOfType('Neos.Neos:Document') ? - $root->getProperty('title') : $root->getLabel(), - 'icon' => 'globe', - 'contextPath' => $root->getContextPath(), - 'nodeType' => $root->getNodeType()->getName(), - 'hasChildren' => count($result), - 'isCollapsed' => false, - 'isActive' => $this->active && ($root->getPath() === $this->active->getPath()), - 'isFocused' => $this->active && ($root->getPath() === $this->active->getPath()), - 'children' => $result - ] - ]; - } - - return $result; - } - - protected function isInRootLine(?NodeInterface $haystack = null, NodeInterface $needle) - { - if ($haystack === null) { - return false; - } - - return mb_strrpos($haystack->getPath(), $needle->getPath(), null, 'UTF-8') === 0; - } -} diff --git a/Classes/Domain/Service/StyleAndJavascriptInclusionService.php b/Classes/Domain/Service/StyleAndJavascriptInclusionService.php index 4704c16afd..da787a4414 100644 --- a/Classes/Domain/Service/StyleAndJavascriptInclusionService.php +++ b/Classes/Domain/Service/StyleAndJavascriptInclusionService.php @@ -19,6 +19,7 @@ /** * @Flow\Scope("singleton") + * @internal */ class StyleAndJavascriptInclusionService { @@ -36,43 +37,46 @@ class StyleAndJavascriptInclusionService /** * @Flow\InjectConfiguration(package="Neos.Fusion", path="defaultContext") - * @var array + * @var array */ protected $fusionDefaultEelContext; /** * @Flow\InjectConfiguration(path="configurationDefaultEelContext") - * @var array + * @var array */ protected $additionalEelDefaultContext; /** * @Flow\InjectConfiguration(path="resources.javascript") - * @var array + * @var array, position: string}> */ protected $javascriptResources; /** * @Flow\InjectConfiguration(path="resources.stylesheets") - * @var array + * @var array, position: string}> */ protected $stylesheetResources; - public function getHeadScripts() + public function getHeadScripts(): string { return $this->build($this->javascriptResources, function ($uri, $additionalAttributes) { - return ''; + return ''; }); } - public function getHeadStylesheets() + public function getHeadStylesheets(): string { return $this->build($this->stylesheetResources, function ($uri, $additionalAttributes) { return ''; }); } - protected function build(array $resourceArrayToSort, \Closure $builderForLine) + /** + * @param array}> $resourceArrayToSort + */ + protected function build(array $resourceArrayToSort, \Closure $builderForLine): string { $sortedResources = (new PositionalArraySorter($resourceArrayToSort))->toArray(); @@ -92,15 +96,17 @@ protected function build(array $resourceArrayToSort, \Closure $builderForLine) if (strpos($resourceExpression, 'resource://') === 0) { // Cache breaker - $hash = substr(md5_file($resourceExpression), 0, 8); + $hash = substr(md5_file($resourceExpression) ?: '', 0, 8); $resourceExpression = $this->resourceManager->getPublicPackageResourceUriByPath($resourceExpression); } $finalUri = $hash ? $resourceExpression . (str_contains($resourceExpression, '?') ? '&' : '?') . $hash : $resourceExpression; - $additionalAttributes = array_merge( - // legacy first level 'defer' attribute - isset($element['defer']) ? ['defer' => $element['defer']] : [], - $element['attributes'] ?? [] - ); + $additionalAttributes = $element['attributes'] ?? []; + + // All scripts are deferred by default. This prevents the attribute from + // being specified redundantly. + if (isset($additionalAttributes['defer'])) { + unset($additionalAttributes['defer']); + } $result .= $builderForLine($finalUri, $this->htmlAttributesArrayToString($additionalAttributes)); } return $result; diff --git a/Classes/Domain/Service/UserLocaleService.php b/Classes/Domain/Service/UserLocaleService.php index eafa2d12e5..2caa5556be 100644 --- a/Classes/Domain/Service/UserLocaleService.php +++ b/Classes/Domain/Service/UserLocaleService.php @@ -17,6 +17,9 @@ use Neos\Neos\Domain\Service\UserService; use Neos\Neos\Fusion\Helper\NodeLabelToken; +/** + * @internal + */ class UserLocaleService { /** @@ -41,7 +44,7 @@ class UserLocaleService /** * The current user's locale (cached for performance) * - * @var Locale + * @var Locale|null */ protected $userLocaleRuntimeCache; @@ -51,9 +54,9 @@ class UserLocaleService * For example {@see NodeLabelToken::resolveLabelFromNodeType()} will call the translator which will uses the globally set locale. * FIXME we should eliminate hacking the global state and passing the locale differently * - * @param boolean $reset Reset to remebered locale + * @param boolean $reset Reset to remembered locale */ - public function switchToUILocale($reset = false) + public function switchToUILocale($reset = false): void { if ($reset === true) { // Reset the locale diff --git a/Classes/Exception/InvalidNodeCreationHandlerException.php b/Classes/Exception/InvalidNodeCreationHandlerException.php index 3b47764cb1..ce03b2c0ac 100644 --- a/Classes/Exception/InvalidNodeCreationHandlerException.php +++ b/Classes/Exception/InvalidNodeCreationHandlerException.php @@ -11,11 +11,10 @@ * source code. */ -use Neos\ContentRepository\Exception; - /** * InvalidNodeCreationHandlerException exception + * @internal */ -class InvalidNodeCreationHandlerException extends Exception +class InvalidNodeCreationHandlerException extends \Exception { } diff --git a/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php b/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php index b198f312e4..5123e8f616 100644 --- a/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php +++ b/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php @@ -1,4 +1,5 @@ $context (or array-like object) onto which this operation should be applied * @return boolean TRUE if the operation can be applied onto the $context, FALSE otherwise */ public function canEvaluate($context) { - return isset($context[0]) && ($context[0] instanceof TraversableNodeInterface); + return isset($context[0]) && ($context[0] instanceof Node); } /** * {@inheritdoc} * - * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments the arguments for this operation + * @param FlowQuery $flowQuery the FlowQuery object + * @param array $arguments the arguments for this operation * @return void */ public function evaluate(FlowQuery $flowQuery, array $arguments) { - /** @var TraversableNodeInterface $siteNode */ - $siteNode = $flowQuery->getContext()[0]; - /** @var TraversableNodeInterface $documentNode */ - $documentNode = $flowQuery->getContext()[1] ?? $siteNode; + /** @var array $context */ + $context = $flowQuery->getContext(); + + /** @var Node $siteNode */ + $siteNode = $context[0]; + /** @var Node $documentNode */ + $documentNode = $context[1] ?? $siteNode; /** @var string[] $toggledNodes */ list($baseNodeType, $loadingDepth, $toggledNodes, $clipboardNodesContextPaths) = $arguments; - // Collect all parents of documentNode up to siteNode - $parents = []; - $currentNode = null; - try { - $currentNode = $documentNode->findParentNode(); - } catch (NodeException $ignored) { - // parent does not exist - } - if ($currentNode) { - $parentNodeIsUnderneathSiteNode = strpos((string)$currentNode->findNodePath(), (string)$siteNode->findNodePath()) === 0; - while ((string)$currentNode->getNodeAggregateIdentifier() !== (string)$siteNode->getNodeAggregateIdentifier() && $parentNodeIsUnderneathSiteNode) { - $parents[] = (string)$currentNode->getNodeAggregateIdentifier(); - $currentNode = $currentNode->findParentNode(); - } - } + $contentRepository = $this->contentRepositoryRegistry->get($documentNode->contentRepositoryId); + + $baseNodeTypeConstraints = NodeTypeCriteria::fromFilterString($baseNodeType); + + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($documentNode); + + $ancestors = $subgraph->findAncestorNodes( + $documentNode->aggregateId, + FindAncestorNodesFilter::create( + NodeTypeCriteria::fromFilterString('Neos.Neos:Document') + ) + ); $nodes = [ - ((string)$siteNode->getNodeAggregateIdentifier()) => $siteNode + ($siteNode->aggregateId->value) => $siteNode ]; - $gatherNodesRecursively = function (&$nodes, TraversableNodeInterface $baseNode, $level = 0) use (&$gatherNodesRecursively, $baseNodeType, $loadingDepth, $toggledNodes, $parents) { - if ( - $level < $loadingDepth || // load all nodes within loadingDepth + + $gatherNodesRecursively = function ( + &$nodes, + Node $baseNode, + $level = 0 + ) use ( + &$gatherNodesRecursively, + $baseNodeTypeConstraints, + $loadingDepth, + $toggledNodes, + $ancestors, + $subgraph + ) { + $baseNodeAddress = NodeAddress::fromNode($baseNode); + + if ($level < $loadingDepth || // load all nodes within loadingDepth $loadingDepth === 0 || // unlimited loadingDepth - in_array($baseNode->getContextPath(), $toggledNodes) || // load toggled nodes - in_array((string)$baseNode->getNodeAggregateIdentifier(), $parents) // load children of all parents of documentNode + // load toggled nodes + in_array($baseNodeAddress->toJson(), $toggledNodes) || + // load children of all parents of documentNode + in_array($baseNode->aggregateId->value, array_map( + fn (Node $node): string => $node->aggregateId->value, + iterator_to_array($ancestors) + )) ) { - foreach ($baseNode->findChildNodes($this->nodeTypeConstraintFactory->parseFilterString($baseNodeType)) as $childNode) { - $nodes[(string)$childNode->getNodeAggregateIdentifier()] = $childNode; + foreach ($subgraph->findChildNodes( + $baseNode->aggregateId, + FindChildNodesFilter::create(nodeTypes: $baseNodeTypeConstraints) + ) as $childNode) { + $nodes[$childNode->aggregateId->value] = $childNode; $gatherNodesRecursively($nodes, $childNode, $level + 1); } } }; $gatherNodesRecursively($nodes, $siteNode); - if (!isset($nodes[(string)$documentNode->getNodeAggregateIdentifier()])) { - $nodes[(string)$documentNode->getNodeAggregateIdentifier()] = $documentNode; + if (!isset($nodes[$documentNode->aggregateId->value])) { + $nodes[$documentNode->aggregateId->value] = $documentNode; } foreach ($clipboardNodesContextPaths as $clipboardNodeContextPath) { - $clipboardNode = $this->propertyMapper->convert($clipboardNodeContextPath, NodeInterface::class); - if ($clipboardNode && !in_array($clipboardNode, $nodes)) { - $nodes[] = $clipboardNode; + // TODO: might not work across multiple CRs yet. + $clipboardNodeAddress = NodeAddress::fromJsonString($clipboardNodeContextPath); + $clipboardNode = $subgraph->findNodeById($clipboardNodeAddress->aggregateId); + if ($clipboardNode && !array_key_exists($clipboardNode->aggregateId->value, $nodes)) { + $nodes[$clipboardNode->aggregateId->value] = $clipboardNode; } } + /* TODO: we might use the Subtree as this may be more efficient + - but the logic above mirrors the old behavior better. + if ($loadingDepth === 0) { + throw new \RuntimeException('TODO: Loading Depth 0 not supported'); + } + $subtree = $contentSubgraph->findSubtree([$siteNode], $loadingDepth, $nodeTypeConstraints); + $subtree = $subtree->getChildren()[0]; + $this->flattenSubtreeToNodeList($nodeAccessor, $subtree, $nodes);*/ + $flowQuery->setContext($nodes); } + + /** + * @param array &$nodes + */ + /* + private function flattenSubtreeToNodeList( + NodeAccessorInterface $nodeAccessor, + SubtreeInterface $subtree, + array &$nodes + ): void { + $currentNode = $subtree->getNode(); + if (is_null($currentNode)) { + return; + } + + $nodes[(string)$currentNode->getNodeAggregateId()] = $currentNode; + + foreach ($subtree->getChildren() as $childSubtree) { + $this->flattenSubtreeToNodeList($nodeAccessor, $childSubtree, $nodes); + } + }*/ } diff --git a/Classes/FlowQueryOperations/NeosUiFilteredChildrenOperation.php b/Classes/FlowQueryOperations/NeosUiFilteredChildrenOperation.php index 7dd6fb590d..7a080d010d 100644 --- a/Classes/FlowQueryOperations/NeosUiFilteredChildrenOperation.php +++ b/Classes/FlowQueryOperations/NeosUiFilteredChildrenOperation.php @@ -11,16 +11,20 @@ * source code. */ -use Neos\Flow\Annotations as Flow; -use Neos\ContentRepository\Domain\NodeType\NodeTypeConstraintFactory; -use Neos\ContentRepository\Domain\Projection\Content\TraversableNodeInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\NodeType\NodeTypeConstraintParser; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Eel\FlowQuery\FlowQuery; use Neos\Eel\FlowQuery\Operations\AbstractOperation; +use Neos\Flow\Annotations as Flow; /** * "children" operation working on ContentRepository nodes. It iterates over all * context elements and returns all child nodes or only those matching * the filter expression specified as optional argument. + * @internal */ class NeosUiFilteredChildrenOperation extends AbstractOperation { @@ -36,30 +40,30 @@ class NeosUiFilteredChildrenOperation extends AbstractOperation * * @var integer */ - protected static $priority = 100; + protected static $priority = 500; /** * @Flow\Inject - * @var NodeTypeConstraintFactory + * @var ContentRepositoryRegistry */ - protected $nodeTypeConstraintFactory; + protected $contentRepositoryRegistry; /** * {@inheritdoc} * - * @param array (or array-like object) $context onto which this operation should be applied + * @param array $context (or array-like object) onto which this operation should be applied * @return boolean TRUE if the operation can be applied onto the $context, FALSE otherwise */ public function canEvaluate($context) { - return isset($context[0]) && ($context[0] instanceof TraversableNodeInterface); + return isset($context[0]) && ($context[0] instanceof Node); } /** * {@inheritdoc} * - * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments the arguments for this operation + * @param FlowQuery $flowQuery the FlowQuery object + * @param array $arguments the arguments for this operation * @return void */ public function evaluate(FlowQuery $flowQuery, array $arguments) @@ -67,15 +71,17 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) $output = []; $outputNodeIdentifiers = []; - $filter = isset($arguments[0]) ? $arguments[0] : null; - - /** @var TraversableNodeInterface $contextNode */ + /** @var Node $contextNode */ foreach ($flowQuery->getContext() as $contextNode) { - /** @var TraversableNodeInterface $childNode */ - foreach ($contextNode->findChildNodes($this->nodeTypeConstraintFactory->parseFilterString($filter)) as $childNode) { - if (!isset($outputNodeIdentifiers[(string)$childNode->getNodeAggregateIdentifier()])) { + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); + + foreach ($subgraph->findChildNodes( + $contextNode->aggregateId, + FindChildNodesFilter::create(nodeTypes: $arguments[0] ?? null) + ) as $childNode) { + if (!isset($outputNodeIdentifiers[$childNode->aggregateId->value])) { $output[] = $childNode; - $outputNodeIdentifiers[(string)$childNode->getNodeAggregateIdentifier()] = true; + $outputNodeIdentifiers[$childNode->aggregateId->value] = true; } } } diff --git a/Classes/FlowQueryOperations/SearchOperation.php b/Classes/FlowQueryOperations/SearchOperation.php index b3a9982691..11d2405f0d 100644 --- a/Classes/FlowQueryOperations/SearchOperation.php +++ b/Classes/FlowQueryOperations/SearchOperation.php @@ -11,43 +11,21 @@ * source code. */ -use Neos\ContentRepository\Domain\Factory\NodeFactory; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Repository\NodeDataRepository; -use Neos\ContentRepository\Domain\Service\NodeTypeManager; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Eel\FlowQuery\FlowQuery; use Neos\Eel\FlowQuery\Operations\AbstractOperation; use Neos\Flow\Annotations as Flow; -use Neos\Neos\Domain\Service\NodeSearchServiceInterface; /** + * Custom search operation using the Content Graph fulltext search + * + * Original implementation: \Neos\Neos\Ui\FlowQueryOperations\SearchOperation + * @internal */ class SearchOperation extends AbstractOperation { - /** - * @Flow\Inject - * @var NodeSearchServiceInterface - */ - protected $nodeSearchService; - - /** - * @Flow\Inject - * @var NodeTypeManager - */ - protected $nodeTypeManager; - - /** - * @Flow\Inject - * @var NodeDataRepository - */ - protected $nodeDataRepository; - - /** - * @Flow\Inject - * @var NodeFactory - */ - protected $nodeFactory; - /** * {@inheritdoc} * @@ -62,50 +40,49 @@ class SearchOperation extends AbstractOperation */ protected static $priority = 100; + /** + * @Flow\Inject + * @var ContentRepositoryRegistry + */ + protected $contentRepositoryRegistry; + /** * {@inheritdoc} * - * @param array (or array-like object) $context onto which this operation should be applied - * @return boolean TRUE if the operation can be applied onto the $context, FALSE otherwise + * We can only handle ContentRepository Nodes. + * + * @param mixed $context + * @return boolean */ public function canEvaluate($context) { - return isset($context[0]) && ($context[0] instanceof NodeInterface); + return (isset($context[0]) && ($context[0] instanceof Node)); } /** * {@inheritdoc} * - * @param FlowQuery $flowQuery the FlowQuery object - * @param array $arguments the arguments for this operation - * @return void + * @param FlowQuery $flowQuery the FlowQuery object + * @param array $arguments the arguments for this operation */ - public function evaluate(FlowQuery $flowQuery, array $arguments) + public function evaluate(FlowQuery $flowQuery, array $arguments): void { - $term = isset($arguments[0]) ? $arguments[0] : null; - $filterNodeTypeName = isset($arguments[1]) ? $arguments[1] : null; - - $nodeTypes = strlen($filterNodeTypeName) > 0 ? [$filterNodeTypeName] : array_keys($this->nodeTypeManager->getSubNodeTypes('Neos.Neos:Document', false)); - - /** @var NodeInterface $contextNode */ - $contextNode = $flowQuery->getContext()[0]; - - $context = $contextNode->getContext(); - - if ($term) { - $matchedNodes = $this->nodeSearchService->findByProperties($term, $nodeTypes, $context, $contextNode); - } else { - $matchedNodes = []; - // Yep, an internal API. But that's what we used in the old UI. - $nodeDataRecords = $this->nodeDataRepository->findByParentAndNodeTypeRecursively($contextNode->getPath(), implode(',', $nodeTypes), $context->getWorkspace(), $context->getDimensions()); - foreach ($nodeDataRecords as $nodeData) { - $matchedNode = $this->nodeFactory->createFromNodeData($nodeData, $context); - if ($matchedNode !== null) { - $matchedNodes[$matchedNode->getPath()] = $matchedNode; - } - } + /** @var array $context */ + $context = $flowQuery->getContext(); + /** @var Node $contextNode */ + $contextNode = $context[0]; + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); + $filter = FindDescendantNodesFilter::create(); + if (isset($arguments[0]) && $arguments[0] !== '') { + $filter = $filter->with(searchTerm: $arguments[0]); } - - $flowQuery->setContext($matchedNodes); + if (isset($arguments[1]) && $arguments[1] !== '') { + $filter = $filter->with(nodeTypes: $arguments[1]); + } + $nodes = $subgraph->findDescendantNodes( + $contextNode->aggregateId, + $filter + ); + $flowQuery->setContext(iterator_to_array($nodes)); } } diff --git a/Classes/Fusion/AppendToCollectionImplementation.php b/Classes/Fusion/AppendToCollectionImplementation.php deleted file mode 100644 index 150f40bef7..0000000000 --- a/Classes/Fusion/AppendToCollectionImplementation.php +++ /dev/null @@ -1,37 +0,0 @@ -fusionValue('collection'); - $key = $this->fusionValue('key'); - $item = $this->fusionValue('item'); - - if ($key) { - $collection[$key] = $item; - } else { - $collection[] = $item; - } - - return $collection; - } -} diff --git a/Classes/Fusion/ExceptionHandler/PageExceptionHandler.php b/Classes/Fusion/ExceptionHandler/PageExceptionHandler.php index 8fdfb46ed7..b2db3b74a8 100644 --- a/Classes/Fusion/ExceptionHandler/PageExceptionHandler.php +++ b/Classes/Fusion/ExceptionHandler/PageExceptionHandler.php @@ -12,22 +12,24 @@ */ use GuzzleHttp\Psr7\Message; -use function GuzzleHttp\Psr7\str; use Neos\Flow\Annotations as Flow; use Neos\Flow\Exception; +use Neos\Flow\Mvc\Controller\Arguments; +use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Mvc\View\ViewInterface; use Neos\Flow\Utility\Environment; use Neos\FluidAdaptor\View\StandaloneView; use Neos\Fusion\Core\ExceptionHandlers\AbstractRenderingExceptionHandler; use Neos\Fusion\Core\ExceptionHandlers\HtmlMessageHandler; use Psr\Http\Message\ResponseFactoryInterface; -use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\StreamInterface; /** * A page exception handler for the new UI. * * FIXME: When the old UI is removed this handler needs to be untangled from the PageHandler as the parent functionality is no longer needed. * FIXME: We should adapt rendering to requested "format" at some point + * @internal */ class PageExceptionHandler extends AbstractRenderingExceptionHandler { @@ -37,12 +39,6 @@ class PageExceptionHandler extends AbstractRenderingExceptionHandler */ protected $responseFactory; - /** - * @Flow\Inject - * @var StreamFactoryInterface - */ - protected $contentFactory; - /** * @Flow\Inject * @var Environment @@ -54,7 +50,7 @@ class PageExceptionHandler extends AbstractRenderingExceptionHandler * * @param string $fusionPath path causing the exception * @param \Exception $exception exception to handle - * @param integer $referenceCode + * @param string|null $referenceCode * @return string * @throws \Neos\Flow\Mvc\Exception\StopActionException * @throws \Neos\Flow\Security\Exception @@ -70,20 +66,19 @@ protected function handle($fusionPath, \Exception $exception, $referenceCode): s 'message' => $output ]); + // @phpstan-ignore-next-line return $this->wrapHttpResponse($exception, $fluidView->render()); } /** * Renders an actual HTTP response including the correct status and cache control header. * - * @param \Exception the exception - * @param string $bodyContent - * @return string + * @param \Exception $exception the exception */ - protected function wrapHttpResponse(\Exception $exception, string $bodyContent): string + protected function wrapHttpResponse(\Exception $exception, StreamInterface $bodyContent): string { $response = $this->responseFactory->createResponse($exception instanceof Exception ? $exception->getStatusCode() : 500) - ->withBody($this->contentFactory->createStream($bodyContent)) + ->withBody($bodyContent) ->withHeader('Cache-Control', 'no-store'); return Message::toString($response); @@ -98,13 +93,20 @@ protected function wrapHttpResponse(\Exception $exception, string $bodyContent): protected function prepareFluidView(): ViewInterface { $fluidView = new StandaloneView(); - $fluidView->setControllerContext($this->runtime->getControllerContext()); + $fluidView->setControllerContext( + new ControllerContext( + $this->runtime->getControllerContext()->getRequest(), + $this->runtime->getControllerContext()->getResponse(), + new Arguments(), + $this->runtime->getControllerContext()->getUriBuilder() + ) + ); $fluidView->setFormat('html'); $fluidView->setTemplatePathAndFilename('resource://Neos.Neos.Ui/Private/Templates/Error/ErrorMessage.html'); $guestNotificationScript = new StandaloneView(); $guestNotificationScript->setTemplatePathAndFilename('resource://Neos.Neos.Ui/Private/Templates/Backend/GuestNotificationScript.html'); - $fluidView->assign('guestNotificationScript', $guestNotificationScript->render()); + $fluidView->assign('guestNotificationScript', $guestNotificationScript->render()->getContents()); return $fluidView; } diff --git a/Classes/Fusion/Helper/ApiHelper.php b/Classes/Fusion/Helper/ApiHelper.php deleted file mode 100644 index 4f51751f9e..0000000000 --- a/Classes/Fusion/Helper/ApiHelper.php +++ /dev/null @@ -1,41 +0,0 @@ -> Dimensions indexed by name with presets indexed by name */ - public function contentDimensionsByName() + public function contentDimensionsByName(ContentRepositoryId $contentRepositoryId): array { - return $this->contentDimensionsPresetSource->getAllPresets(); + $contentDimensionSource = $this->contentRepositoryRegistry->get($contentRepositoryId) + ->getContentDimensionSource(); + $dimensions = $contentDimensionSource->getContentDimensionsOrderedByPriority(); + + $result = []; + foreach ($dimensions as $dimension) { + $result[$dimension->id->value] = [ + 'label' => $dimension->getConfigurationValue('label'), + 'icon' => $dimension->getConfigurationValue('icon'), + + # TODO 'default' => $dimension->defaultValue->value, + # TODO 'defaultPreset' => $dimension->defaultValue->value, + 'presets' => [] + ]; + + foreach ($dimension->values as $value) { + // TODO: make certain values hidable + $result[$dimension->id->value]['presets'][$value->value] = [ + // TODO: name, uriSegment! + 'values' => [$value->value], + 'label' => $value->getConfigurationValue('label'), + 'group' => $value->getConfigurationValue('group'), + ]; + } + } + return $result; } /** - * @param array $dimensions Dimension values indexed by dimension name - * @return array Allowed preset names for the given dimension combination indexed by dimension name + * @param DimensionSpacePoint $dimensions Dimension values indexed by dimension name + * @return array>|object Allowed preset names for the given dimension combination indexed by dimension name */ - public function allowedPresetsByName(array $dimensions) + public function allowedPresetsByName(DimensionSpacePoint $dimensions, ContentRepositoryId $contentRepositoryId): array|object { + $contentDimensionSource = $this->contentRepositoryRegistry->get($contentRepositoryId) + ->getContentDimensionSource(); + + // TODO: re-implement this here; currently EVERYTHING is allowed!! $allowedPresets = []; - $preselectedDimensionPresets = []; - foreach ($dimensions as $dimensionName => $dimensionValues) { - $preset = $this->contentDimensionsPresetSource->findPresetByDimensionValues($dimensionName, $dimensionValues); - if ($preset !== null) { - $preselectedDimensionPresets[$dimensionName] = $preset['identifier']; + foreach ($dimensions->coordinates as $dimensionName => $dimensionValue) { + $dimension = $contentDimensionSource->getDimension(new ContentDimensionId($dimensionName)); + if (!is_null($dimension)) { + $value = $dimension->getValue($dimensionValue); + if ($value !== null) { + $allowedPresets[$dimensionName] = array_keys($dimension->values->values); + } } } - foreach ($preselectedDimensionPresets as $dimensionName => $presetName) { - $presets = $this->contentDimensionsPresetSource->getAllowedDimensionPresetsAccordingToPreselection($dimensionName, $preselectedDimensionPresets); - $allowedPresets[$dimensionName] = array_keys($presets[$dimensionName]['presets']); - } - return $allowedPresets; + /** empty arrays must be rendered as `{}` in json for our client code to work */ + return $allowedPresets === [] ? new \stdClass() : $allowedPresets; + } + + /** @return array> */ + public function dimensionSpacePointArray(AbstractDimensionSpacePoint $dimensionSpacePoint): array + { + return $dimensionSpacePoint->toLegacyDimensionArray(); } /** diff --git a/Classes/Fusion/Helper/NodeInfoHelper.php b/Classes/Fusion/Helper/NodeInfoHelper.php index dcfb6e6274..c209c31140 100644 --- a/Classes/Fusion/Helper/NodeInfoHelper.php +++ b/Classes/Fusion/Helper/NodeInfoHelper.php @@ -11,49 +11,47 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\Node; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; -use Neos\ContentRepository\Domain\Utility\NodePaths; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\CountAncestorNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateClassification; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Eel\ProtectedContextAwareInterface; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Mvc\Controller\ControllerContext; +use Neos\Flow\Mvc\ActionRequest; +use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Neos\Domain\Service\ContentContext; -use Neos\Neos\Service\LinkingService; -use Neos\Neos\Service\Mapping\NodePropertyConverterService; -use Neos\Neos\TypeConverter\EntityToIdentityConverter; +use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface; +use Neos\Neos\Domain\SubtreeTagging\NeosSubtreeTag; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\Ui\Domain\Service\NodePropertyConverterService; use Neos\Neos\Ui\Domain\Service\UserLocaleService; -use Neos\Neos\Ui\Service\NodePolicyService; +use Neos\Neos\Utility\NodeTypeWithFallbackProvider; /** * @Flow\Scope("singleton") + * @internal implementation detail of the Neos Ui to build its initialState. + * and used for rendering node properties for the inline element wrapping from php. */ class NodeInfoHelper implements ProtectedContextAwareInterface { - /** - * @Flow\Inject - * @var NodePolicyService - */ - protected $nodePolicyService; + use NodeTypeWithFallbackProvider; - /** - * @Flow\Inject - * @var UserLocaleService - */ - protected $userLocaleService; + #[Flow\Inject] + protected ContentRepositoryRegistry $contentRepositoryRegistry; - /** - * @Flow\Inject - * @var LinkingService - */ - protected $linkingService; + #[Flow\Inject] + protected NodeUriBuilderFactory $nodeUriBuilderFactory; + + #[Flow\Inject] + protected NodeLabelGeneratorInterface $nodeLabelGenerator; /** * @Flow\Inject - * @var EntityToIdentityConverter + * @var UserLocaleService */ - protected $entityToIdentityConverter; + protected $userLocaleService; /** * @Flow\Inject @@ -67,23 +65,11 @@ class NodeInfoHelper implements ProtectedContextAwareInterface */ protected $nodePropertyConverterService; - /** - * @Flow\Inject - * @var ContextFactoryInterface - */ - protected $contextFactory; - /** * @Flow\InjectConfiguration(path="userInterface.navigateComponent.nodeTree.presets.default.baseNodeType", package="Neos.Neos") * @var string */ - protected $defaultBaseNodeType; - - /** - * @Flow\InjectConfiguration(path="userInterface.navigateComponent.nodeTree.loadingDepth", package="Neos.Neos") - * @var string - */ - protected $loadingDepth; + protected $baseNodeType; /** * @Flow\InjectConfiguration(path="nodeTypeRoles.document", package="Neos.Neos.Ui") @@ -98,52 +84,40 @@ class NodeInfoHelper implements ProtectedContextAwareInterface protected $ignoredNodeTypeRole; /** - * @param NodeInterface $node - * @param ?ControllerContext $controllerContext - * @param bool $omitMostPropertiesForTreeState - * @param ?string $nodeTypeFilterOverride - * @return array - * @deprecated See methods with specific names for different behaviors - */ - public function renderNode(NodeInterface $node, ?ControllerContext $controllerContext = null, $omitMostPropertiesForTreeState = false, $nodeTypeFilterOverride = null) - { - return ($omitMostPropertiesForTreeState ? - $this->renderNodeWithMinimalPropertiesAndChildrenInformation($node, $controllerContext, $nodeTypeFilterOverride) : - $this->renderNodeWithPropertiesAndChildrenInformation($node, $controllerContext, $nodeTypeFilterOverride) - ); - } - - /** - * @param NodeInterface $node - * @param ControllerContext|null $controllerContext - * @param ?string $nodeTypeFilterOverride - * @return array|null + * @return ?array */ - public function renderNodeWithMinimalPropertiesAndChildrenInformation(NodeInterface $node, ?ControllerContext $controllerContext = null, ?string $nodeTypeFilterOverride = null) - { + public function renderNodeWithMinimalPropertiesAndChildrenInformation( + Node $node, + ?ActionRequest $actionRequest = null, + ?string $nodeTypeFilterOverride = null + ): ?array { + /** @todo implement custom node policy service if (!$this->nodePolicyService->isNodeTreePrivilegeGranted($node)) { - return null; - } + return null; + }*/ $this->userLocaleService->switchToUILocale(); $nodeInfo = $this->getBasicNodeInformation($node); $nodeInfo['properties'] = [ - // if we are only rendering the tree state, ensure _isHidden is sent to hidden nodes are correctly shown in the tree. - '_hidden' => $node->isHidden(), - '_hiddenInIndex' => $node->isHiddenInIndex(), - '_hiddenBeforeDateTime' => $node->getHiddenBeforeDateTime() instanceof \DateTimeInterface ? $node->getHiddenBeforeDateTime()->format(\DateTime::W3C) : '', - '_hiddenAfterDateTime' => $node->getHiddenAfterDateTime() instanceof \DateTimeInterface ? $node->getHiddenAfterDateTime()->format(\DateTime::W3C) : '', + // if we are only rendering the tree state, + // ensure _isHidden is sent to hidden nodes are correctly shown in the tree. + // TODO: we should export this correctly named, but that needs changes throughout the JS code as well. + '_hidden' => $node->tags->withoutInherited()->contain(NeosSubtreeTag::disabled()), + '_hiddenInIndex' => $node->getProperty('hiddenInMenu'), + '_hasTimeableNodeVisibility' => + $node->getProperty('enableAfterDateTime') instanceof \DateTimeInterface + || $node->getProperty('disableAfterDateTime') instanceof \DateTimeInterface, ]; - if ($controllerContext !== null) { - $nodeInfo = array_merge($nodeInfo, $this->getUriInformation($node, $controllerContext)); - if ($controllerContext->getRequest()->hasArgument('presetBaseNodeType')) { - $presetBaseNodeType = $controllerContext->getRequest()->getArgument('presetBaseNodeType'); - } + if ($actionRequest !== null) { + $nodeInfo = array_merge($nodeInfo, $this->getUriInformation($node, $actionRequest)); } - $baseNodeType = $nodeTypeFilterOverride ? $nodeTypeFilterOverride : (isset($presetBaseNodeType) ? $presetBaseNodeType : $this->defaultBaseNodeType); - $nodeTypeFilter = $this->buildNodeTypeFilterString($this->nodeTypeStringsToList($baseNodeType), $this->nodeTypeStringsToList($this->ignoredNodeTypeRole)); + $baseNodeType = $nodeTypeFilterOverride ?: $this->baseNodeType; + $nodeTypeFilter = $this->buildNodeTypeFilterString( + $this->nodeTypeStringsToList($baseNodeType), + $this->nodeTypeStringsToList($this->ignoredNodeTypeRole) + ); $nodeInfo['children'] = $this->renderChildrenInformation($node, $nodeTypeFilter); @@ -153,31 +127,30 @@ public function renderNodeWithMinimalPropertiesAndChildrenInformation(NodeInterf } /** - * @param NodeInterface $node - * @param ControllerContext|null $controllerContext - * @param string|null $nodeTypeFilterOverride - * @return array|null + * @return ?array */ - public function renderNodeWithPropertiesAndChildrenInformation(NodeInterface $node, ?ControllerContext $controllerContext = null, ?string $nodeTypeFilterOverride = null) - { + public function renderNodeWithPropertiesAndChildrenInformation( + Node $node, + ?ActionRequest $actionRequest = null, + ?string $nodeTypeFilterOverride = null + ): ?array { + /** @todo implement custom node policy service if (!$this->nodePolicyService->isNodeTreePrivilegeGranted($node)) { - return null; - } + return null; + }**/ $this->userLocaleService->switchToUILocale(); $nodeInfo = $this->getBasicNodeInformation($node); $nodeInfo['properties'] = $this->nodePropertyConverterService->getPropertiesArray($node); + $nodeInfo['tags'] = $node->tags; $nodeInfo['isFullyLoaded'] = true; - if ($controllerContext !== null) { - $nodeInfo = array_merge($nodeInfo, $this->getUriInformation($node, $controllerContext)); - if ($controllerContext->getRequest()->hasArgument('presetBaseNodeType')) { - $presetBaseNodeType = $controllerContext->getRequest()->getArgument('presetBaseNodeType'); - } + if ($actionRequest !== null) { + $nodeInfo = array_merge($nodeInfo, $this->getUriInformation($node, $actionRequest)); } - $baseNodeType = $nodeTypeFilterOverride ?: (isset($presetBaseNodeType) ? $presetBaseNodeType : $this->defaultBaseNodeType); + $baseNodeType = $nodeTypeFilterOverride ?: $this->baseNodeType; $nodeInfo['children'] = $this->renderChildrenInformation($node, $baseNodeType); $this->userLocaleService->switchToUILocale(true); @@ -188,138 +161,158 @@ public function renderNodeWithPropertiesAndChildrenInformation(NodeInterface $no /** * Get the "uri" and "previewUri" for the given node * - * @param NodeInterface $node - * @param ControllerContext $controllerContext - * @return array + * @param Node $node + * @return array */ - protected function getUriInformation(NodeInterface $node, ControllerContext $controllerContext): array + protected function getUriInformation(Node $node, ActionRequest $actionRequest): array { $nodeInfo = []; - if (!$node->getNodeType()->isOfType($this->documentNodeTypeRole)) { + if (!$this->getNodeType($node)->isOfType($this->documentNodeTypeRole)) { return $nodeInfo; } - - try { - $nodeInfo['uri'] = $this->uri($node, $controllerContext); - } catch (\Neos\Neos\Exception $exception) { - // Unless there is a serious problem with routes there shouldn't be an exception ever. - $nodeInfo['uri'] = ''; - } - + $nodeInfo['uri'] = $this->previewUri($node, $actionRequest); return $nodeInfo; } /** * Get the basic information about a node. * - * @param NodeInterface $node - * @return array + * @return array */ - protected function getBasicNodeInformation(NodeInterface $node): array + protected function getBasicNodeInformation(Node $node): array { + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); + $parentNode = $subgraph->findParentNode($node->aggregateId); + + $nodeAddress = NodeAddress::fromNode($node); + return [ - 'contextPath' => $node->getContextPath(), - 'name' => $node->getName(), - 'identifier' => $node->getIdentifier(), - 'nodeType' => $node->getNodeType()->getName(), - 'label' => $node->getLabel(), - 'isAutoCreated' => $node->isAutoCreated(), - 'depth' => $node->getDepth(), + // todo rename eventually rename to nodeAddress + 'contextPath' => $nodeAddress->toJson(), + 'name' => $node->name?->value ?? '', + 'identifier' => $node->aggregateId->jsonSerialize(), + 'nodeType' => $node->nodeTypeName->value, + 'label' => $this->nodeLabelGenerator->getLabel($node), + 'isAutoCreated' => $node->classification === NodeAggregateClassification::CLASSIFICATION_TETHERED, + // TODO: depth is expensive to calculate; maybe let's get rid of this? + 'depth' => $subgraph->countAncestorNodes( + $node->aggregateId, + CountAncestorNodesFilter::create() + ), 'children' => [], - // In some rare cases the parent node cannot be resolved properly - 'parent' => ($node->getParent() ? $node->getParent()->getContextPath() : null), - 'matchesCurrentDimensions' => ($node instanceof Node && $node->dimensionsAreMatchingTargetDimensionValues()) + 'parent' => $parentNode ? NodeAddress::fromNode($parentNode)->toJson() : null, + 'matchesCurrentDimensions' => $node->dimensionSpacePoint->equals($node->originDimensionSpacePoint), + 'lastModificationDateTime' => $node->timestamps->lastModified?->format(\DateTime::ATOM), + 'creationDateTime' => $node->timestamps->created->format(\DateTime::ATOM), + 'lastPublicationDateTime' => $node->timestamps->originalLastModified?->format(\DateTime::ATOM) ]; } /** * Get information for all children of the given parent node. * - * @param NodeInterface $node + * @param Node $node * @param string $nodeTypeFilterString - * @return array + * @return array> */ - protected function renderChildrenInformation(NodeInterface $node, string $nodeTypeFilterString): array + protected function renderChildrenInformation(Node $node, string $nodeTypeFilterString): array { - $documentChildNodes = $node->getChildNodes($nodeTypeFilterString); - // child nodes for content tree, must not include those nodes filtered out by `baseNodeType` - $contentChildNodes = $node->getChildNodes($this->buildContentChildNodeFilterString()); - $childNodes = array_merge($documentChildNodes, $contentChildNodes); + $contentRepository = $this->contentRepositoryRegistry->get($node->contentRepositoryId); + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); - $mapper = static function (NodeInterface $childNode) { - return [ - 'contextPath' => $childNode->getContextPath(), - 'nodeType' => $childNode->getNodeType()->getName() + $documentChildNodes = $subgraph->findChildNodes( + $node->aggregateId, + FindChildNodesFilter::create(nodeTypes: $nodeTypeFilterString) + ); + // child nodes for content tree, must not include those nodes filtered out by `baseNodeType` + $contentChildNodes = $subgraph->findChildNodes( + $node->aggregateId, + FindChildNodesFilter::create( + nodeTypes: $this->buildContentChildNodeFilterString() + ) + ); + $childNodes = $documentChildNodes->merge($contentChildNodes); + + $infos = []; + foreach ($childNodes as $childNode) { + $contentRepository = $this->contentRepositoryRegistry->get($childNode->contentRepositoryId); + $infos[] = [ + 'contextPath' => NodeAddress::fromNode($childNode)->toJson(), + 'nodeType' => $childNode->nodeTypeName->value ]; }; - - return array_map($mapper, $childNodes); + return $infos; } /** - * @param array $nodes - * @param ControllerContext $controllerContext - * @param bool $omitMostPropertiesForTreeState - * @return array + * @param array $nodes + * @return array> */ - public function renderNodes(array $nodes, ControllerContext $controllerContext, $omitMostPropertiesForTreeState = false): array - { - $methodName = $omitMostPropertiesForTreeState ? 'renderNodeWithMinimalPropertiesAndChildrenInformation' : 'renderNodeWithPropertiesAndChildrenInformation'; - $mapper = function (NodeInterface $node) use ($controllerContext, $methodName) { - return $this->$methodName($node, $controllerContext); + public function renderNodes( + array $nodes, + ActionRequest $actionRequest, + bool $omitMostPropertiesForTreeState = false + ): array { + $mapper = function (Node $node) use ($actionRequest, $omitMostPropertiesForTreeState) { + return $omitMostPropertiesForTreeState + ? $this->renderNodeWithMinimalPropertiesAndChildrenInformation($node, $actionRequest) + : $this->renderNodeWithPropertiesAndChildrenInformation($node, $actionRequest); }; - return array_values(array_filter(array_map($mapper, $nodes))); } /** - * @param array $nodes - * @param ControllerContext $controllerContext - * @param null|string $nodeTypeFilter - * @return array + * @param array> $nodes + * @return array> */ - public function renderNodesWithParents(array $nodes, ControllerContext $controllerContext, ?string $nodeTypeFilter = null): array + public function renderNodesWithParents(array $nodes, ActionRequest $actionRequest, ?string $nodeTypeFilter = null): array { // For search operation we want to include all nodes, not respecting the "baseNodeType" setting $baseNodeTypeOverride = $this->documentNodeTypeRole; $renderedNodes = []; - /** @var NodeInterface $node */ + /** @var Node $node */ foreach ($nodes as $node) { - if (array_key_exists($node->getPath(), $renderedNodes)) { - $renderedNodes[$node->getPath()]['matched'] = true; - } elseif ($renderedNode = $this->renderNodeWithMinimalPropertiesAndChildrenInformation($node, $controllerContext, $nodeTypeFilter ?? $baseNodeTypeOverride)) { + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); + + if (array_key_exists($node->aggregateId->value, $renderedNodes)) { + $renderedNodes[$node->aggregateId->value]['matched'] = true; + } elseif ($renderedNode = $this->renderNodeWithMinimalPropertiesAndChildrenInformation( + $node, + $actionRequest, + $nodeTypeFilter ?? $baseNodeTypeOverride + )) { $renderedNode['matched'] = true; - $renderedNodes[$node->getPath()] = $renderedNode; + $renderedNodes[$node->aggregateId->value] = $renderedNode; } else { continue; } - /* @var $contentContext ContentContext */ - $contentContext = $node->getContext(); - $siteNodePath = $contentContext->getCurrentSiteNode()->getPath(); - $parentNode = $node->getParent(); + $parentNode = $subgraph->findParentNode($node->aggregateId); if ($parentNode === null) { - // There are a multitude of reasons why a node might not have a parent and we should ignore these gracefully. + // There are a multitude of reasons why a node might not have a parent + // and we should ignore these gracefully. continue; } - // we additionally need to check that our parent nodes are underneath the site node; otherwise it might happen that - // we try to send the "/sites" node to the UI (which we cannot do, because this does not have an URL) - $parentNodeIsUnderneathSiteNode = (strpos($parentNode->getPath(), $siteNodePath) === 0); - while ($parentNode->getNodeType()->isOfType($baseNodeTypeOverride) && $parentNodeIsUnderneathSiteNode) { - if (array_key_exists($parentNode->getPath(), $renderedNodes)) { - $renderedNodes[$parentNode->getPath()]['intermediate'] = true; + while ($this->getNodeType($parentNode)->isOfType($baseNodeTypeOverride)) { + if (array_key_exists($parentNode->aggregateId->value, $renderedNodes)) { + $renderedNodes[$parentNode->aggregateId->value]['intermediate'] = true; } else { - $renderedParentNode = $this->renderNodeWithMinimalPropertiesAndChildrenInformation($parentNode, $controllerContext, $baseNodeTypeOverride); + $renderedParentNode = $this->renderNodeWithMinimalPropertiesAndChildrenInformation( + $parentNode, + $actionRequest, + $baseNodeTypeOverride + ); if ($renderedParentNode) { $renderedParentNode['intermediate'] = true; - $renderedNodes[$parentNode->getPath()] = $renderedParentNode; + $renderedNodes[$parentNode->aggregateId->value] = $renderedParentNode; } } - $parentNode = $parentNode->getParent(); + $parentNode = $subgraph->findParentNode($parentNode->aggregateId); if ($parentNode === null) { - // There are a multitude of reasons why a node might not have a parent and we should ignore these gracefully. + // There are a multitude of reasons why a node might not have a parent + // and we should ignore these gracefully. break; } } @@ -329,92 +322,42 @@ public function renderNodesWithParents(array $nodes, ControllerContext $controll } /** - * @param NodeInterface $documentNode - * @param ControllerContext $controllerContext - * @return array - */ - public function renderDocumentNodeAndChildContent(NodeInterface $documentNode, ControllerContext $controllerContext) - { - return $this->renderNodeAndChildContent($documentNode, $controllerContext); - } - - /** - * @param NodeInterface $node - * @param ControllerContext $controllerContext - * @return array + * @return array|null> */ - protected function renderNodeAndChildContent(NodeInterface $node, ControllerContext $controllerContext) - { - $reducer = function ($nodes, $node) use ($controllerContext) { - $nodes = array_merge($nodes, $this->renderNodeAndChildContent($node, $controllerContext)); - - return $nodes; - }; + public function defaultNodesForBackend( + Node $site, + Node $documentNode, + ActionRequest $actionRequest + ): array { + // does not support multiple CRs here yet + $contentRepository = $this->contentRepositoryRegistry->get($site->contentRepositoryId); - return array_reduce($node->getChildNodes($this->buildContentChildNodeFilterString()), $reducer, [$node->getContextPath() => $this->renderNodeWithPropertiesAndChildrenInformation($node, $controllerContext)]); - } - - /** - * @param NodeInterface $site - * @param NodeInterface $documentNode - * @param ControllerContext $controllerContext - * @return array - */ - public function defaultNodesForBackend(NodeInterface $site, NodeInterface $documentNode, ControllerContext $controllerContext): array - { return [ - $site->getContextPath() => $this->renderNodeWithPropertiesAndChildrenInformation($site, $controllerContext), - $documentNode->getContextPath() => $this->renderNodeWithPropertiesAndChildrenInformation($documentNode, $controllerContext) + (NodeAddress::fromNode($site)->toJson()) + => $this->renderNodeWithPropertiesAndChildrenInformation($site, $actionRequest), + (NodeAddress::fromNode($documentNode)->toJson()) + => $this->renderNodeWithPropertiesAndChildrenInformation($documentNode, $actionRequest) ]; } - /** - * Creates a URL that will redirect to the given $node in live or base workspace, or returns an empty string if that doesn't exist or is inaccessible - * - * @param ControllerContext $controllerContext - * @param NodeInterface|null $node - * @return string - */ - public function createRedirectToNode(ControllerContext $controllerContext, ?NodeInterface $node = null) + public function previewUri(Node $node, ActionRequest $actionRequest): string { - if ($node === null) { - return ''; - } - // we always want to redirect to the node in the base workspace. - $baseWorkspace = $node->getContext()->getWorkspace(false)->getBaseWorkspace(); - $baseWorkspaceContextProperties = [ - 'workspaceName' => $baseWorkspace !== null ? $baseWorkspace->getName() : 'live', - 'invisibleContentShown' => false, - 'removedContentShown' => false, - 'inaccessibleContentShown' => false, - ]; - $baseWorkspaceContext = $this->contextFactory->create(array_merge($node->getContext()->getProperties(), $baseWorkspaceContextProperties)); - $nodeInBaseWorkspace = $baseWorkspaceContext->getNodeByIdentifier($node->getIdentifier()); - if ($nodeInBaseWorkspace === null || $nodeInBaseWorkspace->isHidden() || !$nodeInBaseWorkspace->getNodeType()->isAggregate()) { - return ''; - } - return $controllerContext->getUriBuilder() - ->reset() - ->setCreateAbsoluteUri(true) - ->setFormat('html') - ->uriFor('redirectTo', ['node' => $nodeInBaseWorkspace], 'Backend', 'Neos.Neos.Ui'); + $nodeAddress = NodeAddress::fromNode($node); + return (string)$this->nodeUriBuilderFactory + ->forActionRequest($actionRequest) + ->previewUriFor($nodeAddress); } - /** - * @param ?NodeInterface $node - * @param ControllerContext $controllerContext - * @return string - * @throws \Neos\Neos\Exception - */ - public function uri(?NodeInterface $node = null, ControllerContext $controllerContext) + public function createRedirectToNode(Node $node, ActionRequest $actionRequest): string { - if ($node === null) { - // This happens when the document node is not published yet - return ''; - } + $nodeAddress = NodeAddress::fromNode($node); - // Create an absolute URI - return $this->linkingService->createNodeUri($controllerContext, $node, null, null, true); + $uriBuilder = new UriBuilder(); + $uriBuilder->setRequest($actionRequest); + return $uriBuilder + ->setCreateAbsoluteUri(true) + ->setFormat('html') + ->uriFor('redirectTo', ['node' => $nodeAddress->toJson()], 'Backend', 'Neos.Neos.Ui'); } /** @@ -436,11 +379,10 @@ protected function nodeTypeStringsToList(string ...$nodeTypeStrings) } /** - * @param array $includedNodeTypes - * @param array $excludedNodeTypes - * @return string + * @param array $includedNodeTypes + * @param array $excludedNodeTypes */ - protected function buildNodeTypeFilterString(array $includedNodeTypes, array $excludedNodeTypes) + protected function buildNodeTypeFilterString(array $includedNodeTypes, array $excludedNodeTypes): string { $preparedExcludedNodeTypes = array_map(function ($nodeTypeName) { return '!' . $nodeTypeName; @@ -449,12 +391,20 @@ protected function buildNodeTypeFilterString(array $includedNodeTypes, array $ex return implode(',', $mergedIncludesAndExcludes); } - /** - * @return string - */ - protected function buildContentChildNodeFilterString() + protected function buildContentChildNodeFilterString(): string + { + return $this->buildNodeTypeFilterString( + [], + $this->nodeTypeStringsToList( + $this->documentNodeTypeRole, + $this->ignoredNodeTypeRole + ) + ); + } + + public function serializedNodeAddress(Node $node): string { - return $this->buildNodeTypeFilterString([], $this->nodeTypeStringsToList($this->documentNodeTypeRole, $this->ignoredNodeTypeRole)); + return NodeAddress::fromNode($node)->toJson(); } /** @@ -463,6 +413,13 @@ protected function buildContentChildNodeFilterString() */ public function allowsCallOfMethod($methodName) { - return true; + // to control what is used in eel we maintain this list. + return in_array($methodName, [ + 'serializedNodeAddress', + 'createRedirectToNode', + 'renderNodeWithPropertiesAndChildrenInformation', + 'defaultNodesForBackend', + 'previewUri' + ], true); } } diff --git a/Classes/Fusion/Helper/PositionalArraySorterHelper.php b/Classes/Fusion/Helper/PositionalArraySorterHelper.php deleted file mode 100644 index d3aab671b2..0000000000 --- a/Classes/Fusion/Helper/PositionalArraySorterHelper.php +++ /dev/null @@ -1,37 +0,0 @@ -toArray(); - } - - /** - * @param string $methodName - * @return boolean - */ - public function allowsCallOfMethod($methodName) - { - return true; - } -} diff --git a/Classes/Fusion/Helper/RenderingModeHelper.php b/Classes/Fusion/Helper/RenderingModeHelper.php new file mode 100644 index 0000000000..4c5f0da81b --- /dev/null +++ b/Classes/Fusion/Helper/RenderingModeHelper.php @@ -0,0 +1,42 @@ + + */ + protected $editPreviewModes; + + /** + * Returns the sorted configuration of all rendering modes {@see RenderingMode} + * + * TODO evaluate if this should be part of {@see RenderingModeService} + * + * @return array> + */ + public function findAllSorted(): array + { + // sorting seems expected for the Neos.Ui: https://github.com/neos/neos-ui/issues/1658 + return (new PositionalArraySorter($this->editPreviewModes))->toArray(); + } + + public function allowsCallOfMethod($methodName) + { + return true; + } +} diff --git a/Classes/Fusion/Helper/StaticResourcesHelper.php b/Classes/Fusion/Helper/StaticResourcesHelper.php index 2eb9cc9927..c7911e8f8c 100644 --- a/Classes/Fusion/Helper/StaticResourcesHelper.php +++ b/Classes/Fusion/Helper/StaticResourcesHelper.php @@ -14,6 +14,10 @@ use Neos\Eel\ProtectedContextAwareInterface; use Neos\Flow\Annotations as Flow; +/** + * @internal implementation detail of the Neos Ui to build its initialState. + * determines if to use the compiled script resources + */ class StaticResourcesHelper implements ProtectedContextAwareInterface { /** @@ -22,7 +26,7 @@ class StaticResourcesHelper implements ProtectedContextAwareInterface */ protected $frontendDevelopmentMode; - public function compiledResourcePackage() + public function compiledResourcePackage(): string { if ($this->frontendDevelopmentMode) { return 'Neos.Neos.Ui'; diff --git a/Classes/Fusion/Helper/WorkspaceHelper.php b/Classes/Fusion/Helper/WorkspaceHelper.php index 51e35af311..63ad825d21 100644 --- a/Classes/Fusion/Helper/WorkspaceHelper.php +++ b/Classes/Fusion/Helper/WorkspaceHelper.php @@ -11,26 +11,38 @@ * source code. */ -use Neos\ContentRepository\Domain\Model\Workspace; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Eel\ProtectedContextAwareInterface; use Neos\Flow\Annotations as Flow; -use Neos\Neos\Domain\Service\UserService as DomainUserService; -use Neos\Neos\Service\UserService; -use Neos\Neos\Ui\ContentRepository\Service\WorkspaceService; +use Neos\Flow\Security\Context; +use Neos\Neos\Domain\Service\UserService; +use Neos\Neos\Domain\Service\WorkspaceService; +use Neos\Neos\Security\Authorization\ContentRepositoryAuthorizationService; +use Neos\Neos\Ui\ContentRepository\Service\WorkspaceService as UiWorkspaceService; +/** + * @internal implementation detail of the Neos Ui to build its initialState {@see \Neos\Neos\Ui\Infrastructure\Configuration\InitialStateProvider} + */ class WorkspaceHelper implements ProtectedContextAwareInterface { /** * @Flow\Inject - * @var WorkspaceService + * @var ContentRepositoryRegistry */ - protected $workspaceService; + protected $contentRepositoryRegistry; /** * @Flow\Inject - * @var DomainUserService + * @var Context */ - protected $domainUserService; + protected $securityContext; + + /** + * @Flow\Inject + * @var UiWorkspaceService + */ + protected $uiWorkspaceService; /** * @Flow\Inject @@ -39,35 +51,43 @@ class WorkspaceHelper implements ProtectedContextAwareInterface protected $userService; /** - * @param Workspace $workspace - * @return array + * @Flow\Inject + * @var WorkspaceService */ - public function getPublishableNodeInfo(Workspace $workspace) - { - return $this->workspaceService->getPublishableNodeInfo($workspace); - } + protected $workspaceService; - public function getPersonalWorkspace() - { - $personalWorkspace = $this->userService->getPersonalWorkspace(); - $baseWorkspace = $personalWorkspace->getBaseWorkspace(); + /** + * @Flow\Inject + * @var ContentRepositoryAuthorizationService + */ + protected $contentRepositoryAuthorizationService; + /** + * @return array + */ + public function getPersonalWorkspace(ContentRepositoryId $contentRepositoryId): array + { + $currentUser = $this->userService->getCurrentUser(); + if ($currentUser === null) { + return []; + } + $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); + $personalWorkspace = $this->workspaceService->getPersonalWorkspaceForUser($contentRepositoryId, $currentUser->getId()); + $personalWorkspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissions($contentRepositoryId, $personalWorkspace->workspaceName, $this->securityContext->getRoles(), $currentUser->getId()); + $publishableNodes = $this->uiWorkspaceService->getPublishableNodeInfo($personalWorkspace->workspaceName, $contentRepository->id); return [ - 'name' => $personalWorkspace->getName(), - 'publishableNodes' => $this->getPublishableNodeInfo($personalWorkspace), - 'baseWorkspace' => $baseWorkspace->getName(), - 'readOnly' => !$this->domainUserService->currentUserCanPublishToWorkspace($baseWorkspace) + 'name' => $personalWorkspace->workspaceName->value, + 'totalNumberOfChanges' => count($publishableNodes), + 'publishableNodes' => $publishableNodes, + 'baseWorkspace' => $personalWorkspace->baseWorkspaceName?->value, + 'readOnly' => !($personalWorkspace->baseWorkspaceName !== null && $personalWorkspacePermissions->write), + 'status' => $personalWorkspace->status->value, ]; } - public function getAllowedTargetWorkspaces() - { - return $this->workspaceService->getAllowedTargetWorkspaces(); - } - /** * @param string $methodName - * @return boolean + * @return bool */ public function allowsCallOfMethod($methodName) { diff --git a/Classes/Fusion/RenderConfigurationImplementation.php b/Classes/Fusion/RenderConfigurationImplementation.php index 152d82093b..4dd34d281b 100644 --- a/Classes/Fusion/RenderConfigurationImplementation.php +++ b/Classes/Fusion/RenderConfigurationImplementation.php @@ -13,9 +13,13 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Exception; +use Neos\Flow\Mvc\ActionRequest; use Neos\Fusion\FusionObjects\AbstractFusionObject; use Neos\Neos\Ui\Domain\Service\ConfigurationRenderingService; +/** + * @internal + */ class RenderConfigurationImplementation extends AbstractFusionObject { /** @@ -26,12 +30,12 @@ class RenderConfigurationImplementation extends AbstractFusionObject /** * @Flow\InjectConfiguration() - * @var array + * @var array */ protected $settings; /** - * @return array + * @return array */ protected function getContext(): array { @@ -49,14 +53,18 @@ protected function getPath(): string /** * Appends an item to the given collection * - * @return array + * @return array * @throws Exception */ public function evaluate() { $context = $this->getContext(); $pathToRender = $this->getPath(); - $context['controllerContext'] = $this->getruntime()->getControllerContext(); + $actionRequest = $this->getRuntime()->fusionGlobals->get('request'); + if (!$actionRequest instanceof ActionRequest) { + throw new Exception('The request is expected to be an ActionRequest.', 1706639436); + } + $context['request'] = $actionRequest; if (!isset($this->settings[$pathToRender])) { throw new Exception('The path "Neos.Neos.Ui.' . $pathToRender . '" was not found in the settings.', 1458814468); diff --git a/Classes/Infrastructure/Cache/CacheConfigurationVersionProvider.php b/Classes/Infrastructure/Cache/CacheConfigurationVersionProvider.php new file mode 100644 index 0000000000..e9358122da --- /dev/null +++ b/Classes/Infrastructure/Cache/CacheConfigurationVersionProvider.php @@ -0,0 +1,69 @@ +computedCacheConfigurationVersion ??= + $this->computeCacheConfigurationVersion(); + } + + private function computeCacheConfigurationVersion(): string + { + /** @var ?Account $account */ + $account = $this->securityContext->getAccount(); + + // Get all roles and sort them by identifier + $roles = $account ? array_map(static fn ($role) => $role->getIdentifier(), $account->getRoles()) : []; + sort($roles); + + // Use the roles combination as cache key to allow multiple users sharing the same configuration version + $configurationIdentifier = md5(implode('_', $roles)); + $cacheKey = 'ConfigurationVersion_' . $configurationIdentifier; + /** @var string|false $version */ + $version = $this->configurationCache->get($cacheKey); + + if ($version === false) { + $version = (string)time(); + $this->configurationCache->set($cacheKey, $version); + } + return $configurationIdentifier . '_' . $version; + } +} diff --git a/Classes/Infrastructure/Configuration/ConfigurationProvider.php b/Classes/Infrastructure/Configuration/ConfigurationProvider.php new file mode 100644 index 0000000000..68ccdb3af2 --- /dev/null +++ b/Classes/Infrastructure/Configuration/ConfigurationProvider.php @@ -0,0 +1,122 @@ + $this->configurationManager->getConfiguration( + ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, + 'Neos.Neos.userInterface.navigateComponent.nodeTree', + ), + 'structureTree' => $this->configurationManager->getConfiguration( + ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, + 'Neos.Neos.userInterface.navigateComponent.structureTree', + ), + 'allowedTargetWorkspaces' => $this->getAllowedTargetWorkspaces($contentRepository), + 'endpoints' => [ + 'nodeTypeSchema' => $uriBuilder->reset() + ->setCreateAbsoluteUri(true) + ->uriFor( + actionName: 'nodeTypeSchema', + controllerArguments: [ + 'version' => + $this->cacheConfigurationVersionProvider + ->getCacheConfigurationVersion(), + ], + controllerName: 'Backend\\Schema', + packageKey: 'Neos.Neos', + ), + 'translations' => $uriBuilder->reset() + ->setCreateAbsoluteUri(true) + ->uriFor( + actionName: 'xliffAsJson', + controllerArguments: [ + 'locale' => + $this->userService + ->getInterfaceLanguage(), + 'version' => + $this->cacheConfigurationVersionProvider + ->getCacheConfigurationVersion(), + ], + controllerName: 'Backend\\Backend', + packageKey: 'Neos.Neos', + ), + ] + ]; + } + + /** + * @return array + */ + private function getAllowedTargetWorkspaces(ContentRepository $contentRepository): array + { + $result = []; + foreach ($contentRepository->findWorkspaces() as $workspace) { + $workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepository->id, $workspace->workspaceName); + if (!in_array($workspaceMetadata->classification, [WorkspaceClassification::ROOT, WorkspaceClassification::SHARED], true)) { + continue; + } + $workspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissions($contentRepository->id, $workspace->workspaceName, $this->securityContext->getRoles(), $this->userService->getBackendUser()?->getId()); + if ($workspacePermissions->read === false) { + continue; + } + $result[$workspace->workspaceName->value] = [ + 'name' => $workspace->workspaceName->value, + 'title' => $workspaceMetadata->title->value, + 'readonly' => !$workspacePermissions->write, + ]; + } + return $result; + } +} diff --git a/Classes/Infrastructure/Configuration/FrontendConfigurationProvider.php b/Classes/Infrastructure/Configuration/FrontendConfigurationProvider.php new file mode 100644 index 0000000000..e951811f3c --- /dev/null +++ b/Classes/Infrastructure/Configuration/FrontendConfigurationProvider.php @@ -0,0 +1,43 @@ + */ + #[Flow\InjectConfiguration('frontendConfiguration')] + protected array $frontendConfigurationBeforeProcessing; + + public function getFrontendConfiguration( + ActionRequest $actionRequest + ): array { + return $this->configurationRenderingService->computeConfiguration( + $this->frontendConfigurationBeforeProcessing, + ['request' => $actionRequest] + ); + } +} diff --git a/Classes/Infrastructure/Configuration/InitialStateProvider.php b/Classes/Infrastructure/Configuration/InitialStateProvider.php new file mode 100644 index 0000000000..d4beb3de26 --- /dev/null +++ b/Classes/Infrastructure/Configuration/InitialStateProvider.php @@ -0,0 +1,60 @@ + */ + #[Flow\InjectConfiguration('initialState')] + protected array $initialStateBeforeProcessing; + + public function getInitialState( + ActionRequest $actionRequest, + ?Node $documentNode, + ?Node $site, + User $user, + ): array { + return $this->configurationRenderingService->computeConfiguration( + $this->initialStateBeforeProcessing, + [ + 'request' => $actionRequest, + 'documentNode' => $documentNode, + 'site' => $site, + 'user' => $user, + 'clipboardNodes' => $this->clipboard->getSerializedNodeAddresses(), + 'clipboardMode' => $this->clipboard->getMode(), + ] + ); + } +} diff --git a/Classes/Infrastructure/ContentRepository/ConflictsFactory.php b/Classes/Infrastructure/ContentRepository/ConflictsFactory.php new file mode 100644 index 0000000000..214e7bcd1c --- /dev/null +++ b/Classes/Infrastructure/ContentRepository/ConflictsFactory.php @@ -0,0 +1,260 @@ +nodeTypeManager = $contentRepository->getNodeTypeManager(); + + $this->workspace = $contentRepository->findWorkspaceByName($workspaceName); + } + + public function fromWorkspaceRebaseFailed( + WorkspaceRebaseFailed $workspaceRebaseFailed + ): Conflicts { + /** @var array */ + $conflictsByKey = []; + + foreach ($workspaceRebaseFailed->conflictingEvents as $conflictingEvent) { + $conflict = $this->createConflict($conflictingEvent); + if (!array_key_exists($conflict->key, $conflictsByKey)) { + // deduplicate if the conflict affects the same node + $conflictsByKey[$conflict->key] = $conflict; + } + } + + return new Conflicts(...$conflictsByKey); + } + + private function createConflict( + ConflictingEvent $conflictingEvent + ): Conflict { + $nodeAggregateId = $conflictingEvent->getAffectedNodeAggregateId(); + $subgraph = $this->acquireSubgraph( + $conflictingEvent->getEvent(), + $nodeAggregateId + ); + $affectedSite = $nodeAggregateId + ? $subgraph?->findClosestNode( + $nodeAggregateId, + FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE) + ) + : null; + $affectedDocument = $nodeAggregateId + ? $subgraph?->findClosestNode( + $nodeAggregateId, + FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_DOCUMENT) + ) + : null; + $affectedNode = $nodeAggregateId + ? $subgraph?->findNodeById($nodeAggregateId) + : null; + + return new Conflict( + key: $affectedNode + ? $affectedNode->aggregateId->value + : Algorithms::generateUUID(), + affectedSite: $affectedSite + ? $this->createIconLabelForNode($affectedSite) + : null, + affectedDocument: $affectedDocument + ? $this->createIconLabelForNode($affectedDocument) + : null, + affectedNode: $affectedNode + ? $this->createIconLabelForNode($affectedNode) + : null, + typeOfChange: $this->createTypeOfChange( + $conflictingEvent->getEvent() + ), + reasonForConflict: $this->createReasonForConflictFromException( + $conflictingEvent->getException() + ) + ); + } + + private function acquireSubgraph( + EventInterface $event, + ?NodeAggregateId $nodeAggregateIdForDimensionFallback + ): ?ContentSubgraphInterface { + if ($this->workspace === null) { + return null; + } + + $dimensionSpacePoint = match ($event::class) { + NodeAggregateWasMoved::class => + // TODO it seems the event lost some information here from the intention + self::firstDimensionSpacePoint($event->succeedingSiblingsForCoverage->toDimensionSpacePointSet()), + NodePropertiesWereSet::class, + NodeAggregateWithNodeWasCreated::class => + $event->originDimensionSpacePoint->toDimensionSpacePoint(), + NodeReferencesWereSet::class => + // TODO it seems the event lost some information here from the intention + self::firstDimensionSpacePoint($event->affectedSourceOriginDimensionSpacePoints->toDimensionSpacePointSet()), + SubtreeWasTagged::class, + SubtreeWasUntagged::class => + // TODO it seems the event lost some information here from the intention + self::firstDimensionSpacePoint($event->affectedDimensionSpacePoints), + NodeAggregateWasRemoved::class => + // TODO it seems the event lost some information here from the intention + self::firstDimensionSpacePoint($event->affectedCoveredDimensionSpacePoints), + NodeAggregateTypeWasChanged::class => + null, + NodePeerVariantWasCreated::class => + $event->peerOrigin->toDimensionSpacePoint(), + NodeGeneralizationVariantWasCreated::class => + $event->generalizationOrigin->toDimensionSpacePoint(), + default => null + }; + + if ($dimensionSpacePoint === null) { + if ($nodeAggregateIdForDimensionFallback === null) { + return null; + } + + $nodeAggregate = $this->contentRepository + ->getContentGraph($this->workspace->workspaceName) + ->findNodeAggregateById($nodeAggregateIdForDimensionFallback); + + if ($nodeAggregate) { + $dimensionSpacePoint = $this->extractValidDimensionSpacePointFromNodeAggregate( + $nodeAggregate + ); + } + } + + if ($dimensionSpacePoint === null) { + return null; + } + + return $this->contentRepository->getContentSubgraph( + $this->workspace->workspaceName, + $dimensionSpacePoint, + ); + } + + private function extractValidDimensionSpacePointFromNodeAggregate( + NodeAggregate $nodeAggregate + ): ?DimensionSpacePoint { + $result = null; + + foreach ($nodeAggregate->coveredDimensionSpacePoints as $coveredDimensionSpacePoint) { + if ($this->preferredDimensionSpacePoint?->equals($coveredDimensionSpacePoint)) { + return $coveredDimensionSpacePoint; + } + $result ??= $coveredDimensionSpacePoint; + } + + return $result; + } + + private function createIconLabelForNode(Node $node): IconLabel + { + $nodeType = $this->nodeTypeManager->getNodeType($node->nodeTypeName); + + return new IconLabel( + icon: $nodeType?->getConfiguration('ui.icon') ?? 'questionmark', + label: $this->nodeLabelGenerator->getLabel($node), + ); + } + + private function createTypeOfChange( + EventInterface $event + ): ?TypeOfChange { + return match ($event::class) { + NodeAggregateWithNodeWasCreated::class, + NodePeerVariantWasCreated::class, + NodeGeneralizationVariantWasCreated::class => + TypeOfChange::NODE_HAS_BEEN_CREATED, + NodePropertiesWereSet::class, + NodeReferencesWereSet::class, + SubtreeWasTagged::class, + SubtreeWasUntagged::class, + NodeAggregateTypeWasChanged::class => + TypeOfChange::NODE_HAS_BEEN_CHANGED, + NodeAggregateWasMoved::class => + TypeOfChange::NODE_HAS_BEEN_MOVED, + NodeAggregateWasRemoved::class => + TypeOfChange::NODE_HAS_BEEN_DELETED, + default => null + }; + } + + private function createReasonForConflictFromException( + \Throwable $exception + ): ?ReasonForConflict { + return match ($exception::class) { + NodeAggregateCurrentlyDoesNotExist::class => + ReasonForConflict::NODE_HAS_BEEN_DELETED, + default => null + }; + } + + private static function firstDimensionSpacePoint(DimensionSpacePointSet $dimensionSpacePointSet): ?DimensionSpacePoint + { + foreach ($dimensionSpacePointSet->points as $point) { + return $point; + } + return null; + } +} diff --git a/Classes/Infrastructure/ContentRepository/CreationDialog/CreationDialogNodeTypePostprocessor.php b/Classes/Infrastructure/ContentRepository/CreationDialog/CreationDialogNodeTypePostprocessor.php new file mode 100644 index 0000000000..3d92d1de50 --- /dev/null +++ b/Classes/Infrastructure/ContentRepository/CreationDialog/CreationDialogNodeTypePostprocessor.php @@ -0,0 +1,302 @@ + + * @Flow\InjectConfiguration(package="Neos.Neos", path="userInterface.inspector.dataTypes") + */ + protected $dataTypesDefaultConfiguration; + + /** + * @var array + * @Flow\InjectConfiguration(package="Neos.Neos", path="userInterface.inspector.editors") + */ + protected $editorDefaultConfiguration; + + /** + * @param NodeType $nodeType (uninitialized) The node type to process + * @param array $configuration input configuration + * @param array $options The processor options + * @return void + */ + public function process(NodeType $nodeType, array &$configuration, array $options): void + { + $creationDialogElements = $configuration['ui']['creationDialog']['elements'] ?? []; + + if (!empty($configuration['properties'])) { + $creationDialogElements = $this->promotePropertiesIntoCreationDialog($configuration['properties'], $creationDialogElements); + } + + if (!empty($configuration['references'])) { + $creationDialogElements = $this->promoteReferencesIntoCreationDialog($configuration['references'], $creationDialogElements); + } + + $this->mergeDefaultCreationDialogElementEditors($creationDialogElements); + + if ($creationDialogElements !== []) { + $configuration['ui']['creationDialog']['elements'] = (new PositionalArraySorter($creationDialogElements))->toArray(); + } + } + + /** + * @param array $creationDialogElements + */ + private function mergeDefaultCreationDialogElementEditors(array &$creationDialogElements): void + { + foreach ($creationDialogElements as &$elementConfiguration) { + if (!isset($elementConfiguration['type'])) { + continue; + } + + $type = $elementConfiguration['type']; + $defaultConfigurationFromDataType = $this->dataTypesDefaultConfiguration[$type] ?? []; + + // FIRST STEP: Figure out which editor should be used + // - Default: editor as configured from the data type + // - Override: editor as configured from the property configuration. + if (isset($elementConfiguration['ui']['editor'])) { + $editor = $elementConfiguration['ui']['editor']; + } elseif (isset($defaultConfigurationFromDataType['editor'])) { + $editor = $defaultConfigurationFromDataType['editor']; + } else { + // No exception since the configuration could be a partial configuration overriding a property + // with showInCreationDialog flag set + continue; + } + + // SECOND STEP: Build up the full UI configuration by merging: + // - take configuration from editor defaults + // - take configuration from dataType + // - take configuration from creationDialog elements (NodeTypes) + $mergedUiConfiguration = $this->editorDefaultConfiguration[$editor] ?? []; + $mergedUiConfiguration = Arrays::arrayMergeRecursiveOverrule( + $mergedUiConfiguration, + $defaultConfigurationFromDataType + ); + $mergedUiConfiguration = Arrays::arrayMergeRecursiveOverrule( + $mergedUiConfiguration, + $elementConfiguration['ui'] ?? [] + ); + $elementConfiguration['ui'] = $mergedUiConfiguration; + $elementConfiguration['ui']['editor'] = $editor; + } + } + + /** + * @param array $properties + * @param array $explicitCreationDialogElements + * @return array + */ + private function promotePropertiesIntoCreationDialog(array $properties, array $explicitCreationDialogElements): array + { + foreach ($properties as $propertyName => $propertyConfiguration) { + if ( + !isset($propertyConfiguration['ui']['showInCreationDialog']) + || $propertyConfiguration['ui']['showInCreationDialog'] !== true + ) { + continue; + } + $creationDialogElement = $this->promotePropertyIntoCreationDialog($propertyName, $propertyConfiguration); + if (isset($explicitCreationDialogElements[$propertyName])) { + $creationDialogElement = Arrays::arrayMergeRecursiveOverrule( + $creationDialogElement, + $explicitCreationDialogElements[$propertyName] + ); + } + $explicitCreationDialogElements[$propertyName] = $creationDialogElement; + } + return $explicitCreationDialogElements; + } + + /** + * Converts a NodeType property configuration to the corresponding creationDialog "element" configuration + * + * @param string $propertyName + * @param array $propertyConfiguration + * @return array + */ + private function promotePropertyIntoCreationDialog(string $propertyName, array $propertyConfiguration): array + { + $dataType = $propertyConfiguration['type'] ?? 'string'; + $dataTypeDefaultConfiguration = $this->dataTypesDefaultConfiguration[$dataType] ?? []; + $convertedConfiguration = [ + 'type' => $dataType, + 'ui' => [ + 'label' => $propertyConfiguration['ui']['label'] ?? $propertyName, + ], + ]; + if (isset($propertyConfiguration['defaultValue'])) { + $convertedConfiguration['defaultValue'] = $propertyConfiguration['defaultValue']; + } + if (isset($propertyConfiguration['ui']['help'])) { + $convertedConfiguration['ui']['help'] = $propertyConfiguration['ui']['help']; + } + if (isset($propertyConfiguration['validation'])) { + $convertedConfiguration['validation'] = $propertyConfiguration['validation']; + } + if (isset($propertyConfiguration['ui']['inspector']['position'])) { + $convertedConfiguration['position'] = $propertyConfiguration['ui']['inspector']['position']; + } + if (isset($propertyConfiguration['ui']['inspector']['hidden'])) { + $convertedConfiguration['ui']['hidden'] = $propertyConfiguration['ui']['inspector']['hidden']; + } + + // todo maybe duplicated due to mergeDefaultCreationDialogElementEditors + $editor = $propertyConfiguration['ui']['inspector']['editor'] + ?? $dataTypeDefaultConfiguration['editor'] + ?? 'Neos.Neos/Inspector/Editors/TextFieldEditor'; + $editorOptions = $propertyConfiguration['ui']['inspector']['editorOptions'] ?? []; + if (isset($dataTypeDefaultConfiguration['editorOptions'])) { + $editorOptions = Arrays::arrayMergeRecursiveOverrule( + $dataTypeDefaultConfiguration['editorOptions'], + $editorOptions + ); + } + if (isset($this->editorDefaultConfiguration[$editor]['editorOptions'])) { + $editorOptions = Arrays::arrayMergeRecursiveOverrule( + $this->editorDefaultConfiguration[$editor]['editorOptions'], + $editorOptions + ); + } + + $convertedConfiguration['ui']['editor'] = $editor; + $convertedConfiguration['ui']['editorOptions'] = $editorOptions; + return $convertedConfiguration; + } + + /** + * @param array $references + * @param array $explicitCreationDialogElements + * @return array + */ + private function promoteReferencesIntoCreationDialog(array $references, array $explicitCreationDialogElements): array + { + foreach ($references as $referenceName => $referenceConfiguration) { + if ( + !isset($referenceConfiguration['ui']['showInCreationDialog']) + || $referenceConfiguration['ui']['showInCreationDialog'] !== true + ) { + continue; + } + + $creationDialogElement = $this->promoteReferenceIntoCreationDialog($referenceConfiguration); + if (isset($explicitCreationDialogElements[$referenceName])) { + $creationDialogElement = Arrays::arrayMergeRecursiveOverrule( + $creationDialogElement, + $explicitCreationDialogElements[$referenceName] + ); + } + $explicitCreationDialogElements[$referenceName] = $creationDialogElement; + } + return $explicitCreationDialogElements; + } + + /** + * Converts a NodeType reference configuration to the corresponding creationDialog "element" configuration + * + * @param array $referenceConfiguration + * @return array + */ + private function promoteReferenceIntoCreationDialog(array $referenceConfiguration): array + { + $maxAllowedItems = $referenceConfiguration['constraints']['maxItems'] ?? null; + $referenceMagicType = $maxAllowedItems === 1 ? 'reference' : 'references'; + /** + * For references, we add this magic type to the elements configuration {@see NodeCreationElements} + */ + $convertedConfiguration = [ + 'type' => $referenceMagicType, + ]; + if (isset($referenceConfiguration['ui']['label'])) { + $convertedConfiguration['ui']['label'] = $referenceConfiguration['ui']['label']; + } + if (isset($referenceConfiguration['defaultValue'])) { + $convertedConfiguration['defaultValue'] = $referenceConfiguration['defaultValue']; + } + if (isset($referenceConfiguration['ui']['help'])) { + $convertedConfiguration['ui']['help'] = $referenceConfiguration['ui']['help']; + } + if (isset($referenceConfiguration['ui']['inspector']['position'])) { + $convertedConfiguration['position'] = $referenceConfiguration['ui']['inspector']['position']; + } + if (isset($referenceConfiguration['ui']['inspector']['hidden'])) { + $convertedConfiguration['ui']['hidden'] = $referenceConfiguration['ui']['inspector']['hidden']; + } + /** + * the editor will be set based on the type above via {@see self::mergeDefaultCreationDialogElementEditors} + */ + if (isset($referenceConfiguration['ui']['inspector']['editor'])) { + $convertedConfiguration['ui']['editor'] = $referenceConfiguration['ui']['inspector']['editor']; + } + if (isset($referenceConfiguration['ui']['inspector']['editorOptions'])) { + $convertedConfiguration['ui']['editorOptions'] = $referenceConfiguration['ui']['inspector']['editorOptions']; + } + return $convertedConfiguration; + } +} diff --git a/Classes/Infrastructure/ContentRepository/CreationDialog/PromotedElementsCreationHandlerFactory.php b/Classes/Infrastructure/ContentRepository/CreationDialog/PromotedElementsCreationHandlerFactory.php new file mode 100644 index 0000000000..eea2b8355e --- /dev/null +++ b/Classes/Infrastructure/ContentRepository/CreationDialog/PromotedElementsCreationHandlerFactory.php @@ -0,0 +1,75 @@ +getNodeTypeManager()) implements NodeCreationHandlerInterface { + public function __construct( + private readonly NodeTypeManager $nodeTypeManager + ) { + } + + public function handle(NodeCreationCommands $commands, NodeCreationElements $elements): NodeCreationCommands + { + $nodeType = $this->nodeTypeManager->getNodeType($commands->first->nodeTypeName); + if (!$nodeType) { + return $commands; + } + $propertyValues = $commands->first->initialPropertyValues; + $initialReferences = $commands->first->references; + foreach ($elements as $elementName => $elementValue) { + // handle properties + if ($nodeType->hasProperty($elementName)) { + $propertyConfiguration = $nodeType->getProperties()[$elementName]; + if ( + ($propertyConfiguration['ui']['showInCreationDialog'] ?? false) === true + ) { + // a promoted element + $propertyValues = $propertyValues->withValue($elementName, $elementValue); + } + } + + // handle references + if ($nodeType->hasReference($elementName)) { + assert($elementValue instanceof NodeAggregateIds); + $referenceConfiguration = $nodeType->getReferences()[$elementName]; + if (($referenceConfiguration['ui']['showInCreationDialog'] ?? false) === true) { + $initialReferences = $initialReferences->withReference( + NodeReferencesForName::fromTargets( + ReferenceName::fromString($elementName), + $elementValue + ) + ); + } + } + } + + return $commands + ->withInitialPropertyValues($propertyValues) + ->withInitialReferences($initialReferences); + } + }; + } +} diff --git a/Classes/Infrastructure/ContentRepository/NodeTypeGroupsAndRolesProvider.php b/Classes/Infrastructure/ContentRepository/NodeTypeGroupsAndRolesProvider.php new file mode 100644 index 0000000000..a892a3bb88 --- /dev/null +++ b/Classes/Infrastructure/ContentRepository/NodeTypeGroupsAndRolesProvider.php @@ -0,0 +1,41 @@ + */ + #[Flow\InjectConfiguration(path: 'nodeTypeRoles')] + protected array $roles; + + /** @var array */ + #[Flow\InjectConfiguration(path: 'nodeTypes.groups', package: 'Neos.Neos')] + protected array $groups; + + public function getNodeTypes(): array + { + return [ + 'roles' => $this->roles, + 'groups' => $this->groups, + ]; + } +} diff --git a/Classes/Infrastructure/MVC/RoutesProvider.php b/Classes/Infrastructure/MVC/RoutesProvider.php new file mode 100644 index 0000000000..10cbad933a --- /dev/null +++ b/Classes/Infrastructure/MVC/RoutesProvider.php @@ -0,0 +1,181 @@ + + $helper->buildUiServiceRoute('change'), + 'publishChangesInSite' => + $helper->buildUiServiceRoute('publishChangesInSite'), + 'publishChangesInDocument' => + $helper->buildUiServiceRoute('publishChangesInDocument'), + 'discardAllChanges' => + $helper->buildUiServiceRoute('discardAllChanges'), + 'discardChangesInSite' => + $helper->buildUiServiceRoute('discardChangesInSite'), + 'discardChangesInDocument' => + $helper->buildUiServiceRoute('discardChangesInDocument'), + 'changeBaseWorkspace' => + $helper->buildUiServiceRoute('changeBaseWorkspace'), + 'syncWorkspace' => + $helper->buildUiServiceRoute('syncWorkspace'), + 'copyNodes' => + $helper->buildUiServiceRoute('copyNodes'), + 'cutNodes' => + $helper->buildUiServiceRoute('cutNodes'), + 'clearClipboard' => + $helper->buildUiServiceRoute('clearClipboard'), + 'flowQuery' => + $helper->buildUiServiceRoute('flowQuery'), + 'generateUriPathSegment' => + $helper->buildUiServiceRoute('generateUriPathSegment'), + 'getWorkspaceInfo' => + $helper->buildUiServiceRoute('getWorkspaceInfo'), + 'getAdditionalNodeMetadata' => + $helper->buildUiServiceRoute('getAdditionalNodeMetadata'), + 'reloadNodes' => + $helper->buildUiServiceRoute('reloadNodes'), + ]; + + $routes['core']['content'] = [ + 'imageWithMetadata' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Content', + actionName: 'imageWithMetaData' + ), + 'createImageVariant' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Content', + actionName: 'createImageVariant' + ), + 'uploadAsset' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Content', + actionName: 'uploadAsset' + ), + ]; + + $routes['core']['service'] = [ + 'assetProxies' => + $helper->buildCoreRoute( + controllerName: 'Service\\AssetProxies', + actionName: 'index' + ), + 'assets' => + $helper->buildCoreRoute( + controllerName: 'Service\\Assets', + actionName: 'index' + ), + 'nodes' => + $helper->buildCoreRoute( + controllerName: 'Service\\Nodes', + actionName: 'index' + ), + 'userPreferences' => + $helper->buildCoreRoute( + subPackageKey: 'Service', + controllerName: 'UserPreference', + actionName: 'index', + format: 'json', + ), + 'dataSource' => + $helper->buildCoreRoute( + subPackageKey: 'Service', + controllerName: 'DataSource', + actionName: 'index', + format: 'json', + ), + 'contentDimensions' => + $helper->buildCoreRoute( + controllerName: 'Service\\ContentDimensions', + actionName: 'index', + ), + 'impersonateStatus' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Impersonate', + actionName: 'status', + format: 'json', + ), + 'impersonateRestore' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Impersonate', + actionName: 'restoreWithResponse', + format: 'json', + ), + ]; + + $routes['core']['modules'] = [ + 'workspace' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Module', + actionName: 'index', + arguments: [ + 'module' => 'management/workspace' + ] + ), + 'userSettings' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Module', + actionName: 'index', + arguments: [ + 'module' => 'user/usersettings' + ] + ), + 'mediaBrowser' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Module', + actionName: 'index', + arguments: [ + 'module' => 'media/browser' + ] + ), + 'defaultModule' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Backend', + actionName: 'index', + ), + ]; + + $routes['core']['login'] = + $helper->buildCoreRoute( + controllerName: 'Login', + actionName: 'index', + format: 'json', + ); + + $routes['core']['logout'] = + $helper->buildCoreRoute( + controllerName: 'Login', + actionName: 'logout', + ); + + return $routes; + } +} diff --git a/Classes/Infrastructure/MVC/RoutesProviderHelper.php b/Classes/Infrastructure/MVC/RoutesProviderHelper.php new file mode 100644 index 0000000000..5ac1acca87 --- /dev/null +++ b/Classes/Infrastructure/MVC/RoutesProviderHelper.php @@ -0,0 +1,68 @@ +uriBuilder->reset() + ->setCreateAbsoluteUri(true) + ->uriFor( + actionName: $actionName, + controllerArguments: [], + controllerName: 'BackendService', + packageKey: 'Neos.Neos.Ui', + ); + } + + /** + * @param array $arguments + */ + public function buildCoreRoute( + string $controllerName, + string $actionName, + ?string $subPackageKey = null, + ?string $format = null, + array $arguments = [], + ): string { + $this->uriBuilder->reset() + ->setCreateAbsoluteUri(true); + + if ($format !== null) { + $this->uriBuilder->setFormat($format); + } + + return $this->uriBuilder->uriFor( + actionName: $actionName, + controllerArguments: $arguments, + controllerName: $controllerName, + packageKey: 'Neos.Neos', + subPackageKey: $subPackageKey, + ); + } +} diff --git a/Classes/Infrastructure/Neos/MenuProvider.php b/Classes/Infrastructure/Neos/MenuProvider.php new file mode 100644 index 0000000000..ae8273cfcb --- /dev/null +++ b/Classes/Infrastructure/Neos/MenuProvider.php @@ -0,0 +1,126 @@ +setRequest($actionRequest); + $controllerContext = new ControllerContext( + $actionRequest, + new ActionResponse(), + new Arguments(), + $uriBuilder + ); + + $modulesForMenu = $this->menuHelper->buildModuleList($controllerContext); + + $result = []; + foreach ($modulesForMenu as $moduleName => $module) { + if ($module['hideInMenu'] === true) { + continue; + } + + $result[$moduleName]['label'] = $module['label']; + $result[$moduleName]['icon'] = $module['icon']; + $result[$moduleName]['uri'] = $module['uri']; + $result[$moduleName]['target'] = 'Window'; + + $result[$moduleName]['children'] = match ($module['module']) { + 'content' => $this->buildChildrenForSites($controllerContext), + default => $this->buildChildrenForBackendModule($module), + }; + } + + return array_values($result); + } + + /** + * @return array + */ + private function buildChildrenForSites(ControllerContext $controllerContext): array + { + $sitesForMenu = $this->menuHelper->buildSiteList($controllerContext); + + $result = []; + foreach ($sitesForMenu as $index => $site) { + $name = $site['name']; + $name = is_string($name) ? $name : 'N/A'; + + $uri = $site['uri']; + $uri = is_string($uri) ? $uri : '#'; + + $active = $site['active']; + $active = is_bool($active) || is_numeric($active) + ? (bool) $active + : false; + + $result[$index]['icon'] = 'globe'; + $result[$index]['label'] = $name; + $result[$index]['uri'] = $uri; + $result[$index]['target'] = 'Window'; + $result[$index]['isActive'] = $active; + $result[$index]['skipI18n'] = true; + } + + return array_values($result); + } + + /** + * @param array{submodules:array} $module + * @return array + */ + private function buildChildrenForBackendModule(array $module): array + { + $result = []; + foreach ($module['submodules'] as $submoduleName => $submodule) { + if ($submodule['hideInMenu'] === true) { + continue; + } + + $result[$submoduleName]['icon'] = $submodule['icon']; + $result[$submoduleName]['label'] = $submodule['label']; + $result[$submoduleName]['uri'] = $submodule['uri']; + $result[$submoduleName]['position'] = $submodule['position']; + $result[$submoduleName]['isActive'] = true; + $result[$submoduleName]['target'] = 'Window'; + $result[$submoduleName]['skipI18n'] = false; + } + + $positionalArraySorter = new PositionalArraySorter($result); + $result = $positionalArraySorter->toArray(); + + return array_values($result); + } +} diff --git a/Classes/Infrastructure/Neos/UriPathSegmentNodeCreationHandlerFactory.php b/Classes/Infrastructure/Neos/UriPathSegmentNodeCreationHandlerFactory.php new file mode 100644 index 0000000000..92ae008ced --- /dev/null +++ b/Classes/Infrastructure/Neos/UriPathSegmentNodeCreationHandlerFactory.php @@ -0,0 +1,89 @@ +getNodeTypeManager(), $this->transliterationService) implements NodeCreationHandlerInterface { + public function __construct( + private readonly NodeTypeManager $nodeTypeManager, + private readonly TransliterationService $transliterationService + ) { + } + + public function handle(NodeCreationCommands $commands, NodeCreationElements $elements): NodeCreationCommands + { + if ( + !$this->nodeTypeManager->getNodeType($commands->first->nodeTypeName) + ?->isOfType('Neos.Neos:Document') + ) { + return $commands; + } + + // if specified, the uriPathSegment equals the title + $uriPathSegment = $elements->get('title'); + + // if not empty, we transliterate the uriPathSegment according to the language of the new node + if ($uriPathSegment !== null && $uriPathSegment !== '') { + $uriPathSegment = $this->transliterateText( + $commands->first->originDimensionSpacePoint->toDimensionSpacePoint(), + $uriPathSegment + ); + } else { + // alternatively we set it to a random string like `document-blog-022` + $nodeTypeSuffix = explode(':', $commands->first->nodeTypeName->value)[1] ?? ''; + $uriPathSegment = sprintf('%s-%03d', $nodeTypeSuffix, random_int(0, 999)); + } + $uriPathSegment = Transliterator::urlize($uriPathSegment); + $propertyValues = $commands->first->initialPropertyValues->withValue('uriPathSegment', $uriPathSegment); + + return $commands->withInitialPropertyValues($propertyValues); + } + + private function transliterateText(DimensionSpacePoint $dimensionSpacePoint, string $text): string + { + $languageDimensionValue = $dimensionSpacePoint->getCoordinate(new ContentDimensionId('language')); + if ($languageDimensionValue !== null) { + try { + $language = (new Locale($languageDimensionValue))->getLanguage(); + } catch (InvalidLocaleIdentifierException $e) { + // we don't need to do anything here; we'll just transliterate the text. + } + } + return $this->transliterationService->transliterate($text, $language ?? null); + } + }; + } +} diff --git a/Classes/NodeCreationHandler/ContentTitleNodeCreationHandler.php b/Classes/NodeCreationHandler/ContentTitleNodeCreationHandler.php deleted file mode 100644 index 36beb018a6..0000000000 --- a/Classes/NodeCreationHandler/ContentTitleNodeCreationHandler.php +++ /dev/null @@ -1,33 +0,0 @@ -getNodeType()->isOfType('Neos.Neos:Content')) { - if (isset($data['title'])) { - $node->setProperty('title', $data['title']); - } - } - } -} diff --git a/Classes/NodeCreationHandler/CreationDialogPropertiesCreationHandler.php b/Classes/NodeCreationHandler/CreationDialogPropertiesCreationHandler.php deleted file mode 100644 index bee4e9d547..0000000000 --- a/Classes/NodeCreationHandler/CreationDialogPropertiesCreationHandler.php +++ /dev/null @@ -1,51 +0,0 @@ - $propertyValue) { - $propertyConfiguration = $node->getNodeType()->getConfiguration('properties')[$propertyName] ?? null; - if (!isset($propertyConfiguration['ui']['showInCreationDialog']) || $propertyConfiguration['ui']['showInCreationDialog'] !== true) { - continue; - } - $propertyType = TypeHandling::normalizeType($propertyConfiguration['type'] ?? 'string'); - if ($propertyValue === null || ($propertyValue === '' && !TypeHandling::isSimpleType($propertyType))) { - continue; - } - $propertyValue = $this->nodePropertyConversionService->convert($node->getNodeType(), $propertyName, $propertyValue, $node->getContext()); - if (strncmp($propertyName, '_', 1) === 0) { - ObjectAccess::setProperty($node, substr($propertyName, 1), $propertyValue); - } else { - $node->setProperty($propertyName, $propertyValue); - } - } - } -} diff --git a/Classes/NodeCreationHandler/DocumentTitleNodeCreationHandler.php b/Classes/NodeCreationHandler/DocumentTitleNodeCreationHandler.php deleted file mode 100644 index 7d6460267d..0000000000 --- a/Classes/NodeCreationHandler/DocumentTitleNodeCreationHandler.php +++ /dev/null @@ -1,42 +0,0 @@ -getNodeType()->isOfType('Neos.Neos:Document')) { - if (isset($data['title'])) { - $node->setProperty('title', $data['title']); - } - $node->setProperty('uriPathSegment', $this->nodeUriPathSegmentGenerator->generateUriPathSegment($node, (isset($data['title']) ? $data['title'] : null))); - } - } -} diff --git a/Classes/NodeCreationHandler/NodeCreationHandlerInterface.php b/Classes/NodeCreationHandler/NodeCreationHandlerInterface.php deleted file mode 100644 index b5755c15b8..0000000000 --- a/Classes/NodeCreationHandler/NodeCreationHandlerInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - + */ + protected $supportedOptions = [ + 'title' => [null, 'The application title which will be used as the HTML .', 'string'], + ]; + + public function render(): StreamInterface + { + $result = '<!DOCTYPE html>'; + $result .= '<html lang="' . $this->renderLang() . '">'; + $result .= '<head>'; + $result .= $this->renderHead(); + $result .= '</head>'; + $result .= '<body>'; + $result .= $this->renderBody(); + $result .= '</body>'; + $result .= '</html>'; + + return $this->createStream($result); + } + + private function renderLang(): string + { + return $this->userService->getInterfaceLanguage(); + } + + private function renderHead(): string + { + $result = '<meta charset="UTF-8">'; + $result .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">'; + + $result .= '<title>' . $this->options['title'] . ''; + + $result .= $this->styleAndJavascriptInclusionService->getHeadStylesheets(); + $result .= $this->styleAndJavascriptInclusionService->getHeadScripts(); + + $result .= sprintf( + '', + $this->resourceManager->getPublicPackageResourceUriByPath( + 'resource://Neos.Neos.Ui/Public/Images/apple-touch-icon.png' + ) + ); + $result .= sprintf( + '', + $this->resourceManager->getPublicPackageResourceUriByPath( + 'resource://Neos.Neos.Ui/Public/Images/favicon-16x16.png' + ) + ); + $result .= sprintf( + '', + $this->resourceManager->getPublicPackageResourceUriByPath( + 'resource://Neos.Neos.Ui/Public/Images/favicon-32x32.png' + ) + ); + $result .= sprintf( + '', + $this->resourceManager->getPublicPackageResourceUriByPath( + 'resource://Neos.Neos.Ui/Public/Images/safari-pinned-tab.svg' + ) + ); + + $locale = new Locale($this->userService->getInterfaceLanguage()); + // @TODO: All endpoints should be treated this way and be isolated from + // initial data. + $result .= sprintf( + '', + $this->variables['initialData']['configuration']['endpoints']['translations'], + (string) $locale, + implode(',', $this->pluralsReader->getPluralForms($locale)), + ); + $result .= sprintf( + '', + json_encode($this->variables['initialData']), + ); + + return $result; + } + + private function renderBody(): string + { + $result = sprintf( + '
', + $this->securityContext->getCsrfProtectionToken(), + (string) $this->bootstrap->getContext(), + ); + $result .= $this->renderSplashScreen(); + $result .= '
'; + + return $result; + } + + private function renderSplashScreen(): string + { + return << + @keyframes color_change { + 0% { + filter: drop-shadow(0 0 0 #00adee) opacity(25%); + } + 100% { + filter: drop-shadow(0 0 5px #00adee) opacity(100%); + } + } + + .loadingIcon { + color: #00adee; + animation-name: color_change; + animation-duration: 1.2s; + animation-iteration-count: infinite; + animation-direction: alternate; + animation-timing-function: ease-in-out; + } + .splash { + width: 100vw; + height: 100vh; + background-color: #222222; + display: flex; + align-items: center; + justify-content: center; + font-size: 30px; + } + +
+ +
+ HTML; + } +} diff --git a/Classes/Service/NodeClipboard.php b/Classes/Service/NodeClipboard.php index 2a3ddfd95b..2f009b6795 100644 --- a/Classes/Service/NodeClipboard.php +++ b/Classes/Service/NodeClipboard.php @@ -11,87 +11,83 @@ * source code. */ +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\Flow\Annotations as Flow; -use Neos\ContentRepository\Domain\Model\NodeInterface; /** * This is a container for clipboard state that needs to be persisted server side * * @Flow\Scope("session") + * @internal */ class NodeClipboard { - const MODE_COPY = 'Copy'; - const MODE_MOVE = 'Move'; + public const MODE_COPY = 'Copy'; + public const MODE_MOVE = 'Move'; /** - * @var string + * @var array */ - protected $nodeContextPaths = []; + protected array $serializedNodeAddresses = []; /** - * @var string one of the NodeClipboard::MODE_* constants + * one of the NodeClipboard::MODE_* constants */ - protected $mode = ''; + protected string $mode = ''; /** * Save copied node to clipboard. * - * @param NodeInterface[] $nodes - * @return void + * @param array $nodeAddresses * @Flow\Session(autoStart=true) */ - public function copyNodes(array $nodes) + public function copyNodes(array $nodeAddresses): void { - $this->nodeContextPaths = array_map(function ($node) { - return $node->getContextPath(); - }, $nodes); + $this->serializedNodeAddresses = array_map( + fn (NodeAddress $nodeAddress) => $nodeAddress->toJson(), + $nodeAddresses + ); $this->mode = self::MODE_COPY; } /** - * Save cut nodes to clipboard. + * Save cut node to clipboard. * - * @param NodeInterface[] $nodes - * @return void + * @param array $nodeAddresses * @Flow\Session(autoStart=true) */ - public function cutNodes(array $nodes) + public function cutNodes(array $nodeAddresses): void { - $this->nodeContextPaths = array_map(function ($node) { - return $node->getContextPath(); - }, $nodes); + $this->serializedNodeAddresses = array_map( + fn (NodeAddress $nodeAddress) => $nodeAddress->toJson(), + $nodeAddresses + ); $this->mode = self::MODE_MOVE; } /** * Reset clipboard. * - * @return void * @Flow\Session(autoStart=true) */ - public function clear() + public function clear(): void { - $this->nodeContextPaths = []; + $this->serializedNodeAddresses = []; $this->mode = ''; } /** - * Get clipboard node. - * - * @return array $nodeContextPath + * @return array */ - public function getNodeContextPaths() + public function getSerializedNodeAddresses(): array { - return $this->nodeContextPaths ? $this->nodeContextPaths : []; + return $this->serializedNodeAddresses; } /** * Get clipboard mode. - * - * @return string $mode */ - public function getMode() + public function getMode(): string { return $this->mode; } diff --git a/Classes/Service/NodePolicyService.php b/Classes/Service/NodePolicyService.php deleted file mode 100644 index ee575cb351..0000000000 --- a/Classes/Service/NodePolicyService.php +++ /dev/null @@ -1,253 +0,0 @@ -get(PolicyService::class); - $usedPrivilegeClassNames = []; - foreach ($policyService->getPrivilegeTargets() as $privilegeTarget) { - $usedPrivilegeClassNames[$privilegeTarget->getPrivilegeClassName()] = true; - foreach (class_parents($privilegeTarget->getPrivilegeClassName()) as $parentPrivilege) { - if (is_a($parentPrivilege, PrivilegeInterface::class, true)) { - $usedPrivilegeClassNames[$parentPrivilege] = true; - } - } - } - - return $usedPrivilegeClassNames; - } - - /** - * @param NodeInterface $node - * @return array - */ - public function getNodePolicyInformation(NodeInterface $node): array - { - return [ - 'disallowedNodeTypes' => $this->getDisallowedNodeTypes($node), - 'canRemove' => $this->canRemoveNode($node), - 'canEdit' => $this->canEditNode($node), - 'disallowedProperties' => $this->getDisallowedProperties($node) - ]; - } - - /** - * @param NodeInterface $node - * @return bool - */ - public function isNodeTreePrivilegeGranted(NodeInterface $node): bool - { - if (!isset(self::getUsedPrivilegeClassNames($this->objectManager)[NodeTreePrivilege::class])) { - return true; - } - - return $this->privilegeManager->isGranted( - NodeTreePrivilege::class, - new NodePrivilegeSubject($node) - ); - } - - /** - * @param NodeInterface $node - * @return array - */ - public function getDisallowedNodeTypes(NodeInterface $node): array - { - $disallowedNodeTypes = []; - - if (!isset(self::getUsedPrivilegeClassNames($this->objectManager)[CreateNodePrivilege::class])) { - return $disallowedNodeTypes; - } - - $filteredNodeTypes = $this->getNodeRelatedNodeTypes($node); - - // filter the remaining nodeTypes via policy check - $filter = function ($nodeType) use ($node) { - return !$this->privilegeManager->isGranted( - CreateNodePrivilege::class, - new CreateNodePrivilegeSubject($node, $nodeType) - ); - }; - - $disallowedNodeTypeObjects = array_filter($filteredNodeTypes, $filter); - - $mapper = function ($nodeType) { - return $nodeType->getName(); - }; - - return array_values(array_map($mapper, $disallowedNodeTypeObjects)); - } - - /** - * For a given node $node this method returns the set of nodeTypes - * - if $node is auto-created and the nodeType is allowed as a grandchild by constraints nodeType definition - * - of allowed child nodeType's - * - of superType's - * - of the nodeType of $node itself - * @param NodeInterface $node - * @return array - */ - protected function getNodeRelatedNodeTypes(NodeInterface $node): array - { - // determine the set of configured node supertypes - $nodeNodeType = $node->getNodeType(); - - $superTypes = []; - $generateSuperTypes = static function (array $nodeTypes, &$superTypes) use (&$generateSuperTypes) { - foreach ($nodeTypes as $nodeType) { - $superTypes[$nodeType->getName()] = $nodeType; - $generateSuperTypes($nodeType->getDeclaredSuperTypes(), $superTypes); - } - }; - $generateSuperTypes($nodeNodeType->getDeclaredSuperTypes(), $superTypes); - - $parentNodeType = null; - // check for root node to avoid an exception for parentNodeType and improve performance in this case - if ($node->isRoot() !== true) { - $parentNode = $node->findParentNode(); - $parentNodeType = $parentNode->getNodeType(); - } - - // check if the node is auto-created - $isAutoCreated = false; - if ($parentNodeType && - array_key_exists((string)$node->getNodeName(), $parentNodeType->getAutoCreatedChildNodes())) { - $isAutoCreated = true; - } - - // filter the set of configured nodeTypes - $nodeName = (string)$node->getNodeName(); - - $constraintAndSuperTypeFilter = static function ($nodeType) use ( - $nodeName, - $nodeNodeType, - $superTypes, - $isAutoCreated, - $parentNodeType - ) { - // check if the nodeType is mentioned in the constraints - if ($isAutoCreated) { - if ($parentNodeType && $parentNodeType->allowsGrandchildNodeType($nodeName, $nodeType)) { - return true; - } - } elseif ($nodeNodeType->allowsChildNodeType($nodeType)) { - return true; - } elseif (isset($superTypes[$nodeType->getName()])) { // check if the nodeType is a supertype - return true; - } elseif ($nodeType->getName() === $nodeNodeType->getName()) { - return true; - } - - // ignore other nodeType - return false; - }; - - return array_filter($this->nodeTypeManager->getNodeTypes(), $constraintAndSuperTypeFilter); - } - - /** - * @param NodeInterface $node - * @return bool - */ - public function canRemoveNode(NodeInterface $node): bool - { - $canRemove = true; - if (isset(self::getUsedPrivilegeClassNames($this->objectManager)[RemoveNodePrivilege::class])) { - $canRemove = $this->privilegeManager->isGranted(RemoveNodePrivilege::class, new NodePrivilegeSubject($node)); - } - - return $canRemove; - } - - /** - * @param NodeInterface $node - * @return bool - */ - public function canEditNode(NodeInterface $node): bool - { - $canEdit = true; - if (isset(self::getUsedPrivilegeClassNames($this->objectManager)[EditNodePrivilege::class])) { - $canEdit = $this->privilegeManager->isGranted(EditNodePrivilege::class, new NodePrivilegeSubject($node)); - } - - return $canEdit; - } - - /** - * @param NodeInterface $node - * @return array - */ - public function getDisallowedProperties(NodeInterface $node): array - { - $disallowedProperties = []; - - if (!isset(self::getUsedPrivilegeClassNames($this->objectManager)[EditNodePropertyPrivilege::class])) { - return $disallowedProperties; - } - - $filter = function ($propertyName) use ($node) { - return !$this->privilegeManager->isGranted( - EditNodePropertyPrivilege::class, - new PropertyAwareNodePrivilegeSubject($node, null, $propertyName) - ); - }; - - $disallowedProperties = array_filter(array_keys($node->getNodeType()->getProperties()), $filter); - return $disallowedProperties; - } -} diff --git a/Classes/Service/NodePropertyValidationService.php b/Classes/Service/NodePropertyValidationService.php index 552fbd20f3..200ba9bb5d 100644 --- a/Classes/Service/NodePropertyValidationService.php +++ b/Classes/Service/NodePropertyValidationService.php @@ -22,6 +22,7 @@ /** * @Flow\Scope("singleton") + * @internal */ class NodePropertyValidationService { @@ -38,12 +39,11 @@ class NodePropertyValidationService protected $dateTimeConverter; /** - * @param $value * @param string $validatorName - * @param array $validatorConfiguration + * @param array $validatorConfiguration * @return bool */ - public function validate($value, string $validatorName, array $validatorConfiguration): bool + public function validate(mixed $value, string $validatorName, array $validatorConfiguration): bool { $validator = $this->resolveValidator($validatorName, $validatorConfiguration); @@ -70,7 +70,7 @@ public function validate($value, string $validatorName, array $validatorConfigur /** * @param string $validatorName - * @param array $validatorConfiguration + * @param array $validatorConfiguration * @return ValidatorInterface|null */ protected function resolveValidator(string $validatorName, array $validatorConfiguration) @@ -88,6 +88,8 @@ protected function resolveValidator(string $validatorName, array $validatorConfi return null; } - return new $fullQualifiedValidatorClassName($validatorConfiguration); + /** @var ValidatorInterface $validator */ + $validator = new $fullQualifiedValidatorClassName($validatorConfiguration); + return $validator; } } diff --git a/Classes/TypeConverter/ChangeCollectionConverter.php b/Classes/TypeConverter/ChangeCollectionConverter.php index c883369f85..b40b270967 100644 --- a/Classes/TypeConverter/ChangeCollectionConverter.php +++ b/Classes/TypeConverter/ChangeCollectionConverter.php @@ -11,15 +11,13 @@ * source code. */ -use Neos\Error\Messages\Error; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\Flow\Annotations as Flow; use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Flow\Property\PropertyMapper; -use Neos\Flow\Property\PropertyMappingConfigurationInterface; -use Neos\Flow\Property\TypeConverter\AbstractTypeConverter; use Neos\Flow\Reflection\ReflectionService; -use Neos\Neos\Ui\ContentRepository\Service\NodeService; +use Neos\Neos\Ui\ContentRepository\Service\NeosUiNodeService; use Neos\Neos\Ui\Domain\Model\ChangeCollection; use Neos\Neos\Ui\Domain\Model\ChangeInterface; use Neos\Neos\Ui\Domain\Model\Changes\Property; @@ -29,11 +27,12 @@ * An Object Converter for ChangeCollections. * * @Flow\Scope("singleton") + * @internal */ -class ChangeCollectionConverter extends AbstractTypeConverter +class ChangeCollectionConverter { /** - * @var array + * @var array */ protected $sourceTypes = ['array']; @@ -45,24 +44,27 @@ class ChangeCollectionConverter extends AbstractTypeConverter /** * @var integer */ - protected $priority = 1; + protected $priority = 5; /** - * @Flow\Inject(lazy=false) + * @Flow\Inject * @var PersistenceManagerInterface */ protected $persistenceManager; - protected $disallowedPayloadProperties = [ + /** + * @var array + */ + protected array $disallowedPayloadProperties = [ 'subject', 'reference' ]; /** - * @Flow\InjectConfiguration(path="changes.types") - * @var array + * @Flow\InjectConfiguration(package="Neos.Neos.Ui", path="changes.types") + * @var array> */ - protected $typeMap; + protected array $typeMap; /** * @Flow\Inject @@ -72,7 +74,7 @@ class ChangeCollectionConverter extends AbstractTypeConverter /** * @Flow\Inject - * @var NodeService + * @var NeosUiNodeService */ protected $nodeService; @@ -91,26 +93,16 @@ class ChangeCollectionConverter extends AbstractTypeConverter /** * Converts a accordingly formatted, associative array to a change collection * - * @param array $source - * @param string $targetType not used - * @param array $subProperties not used - * @param ?\Neos\Flow\Property\PropertyMappingConfigurationInterface $configuration not used - * @return mixed An object or \Neos\Error\Messages\Error if the input format is not supported or could not be converted for other reasons + * @param array> $source * @throws \Exception */ - public function convertFrom($source, $targetType, array $subProperties = [], ?PropertyMappingConfigurationInterface $configuration = null) - { - if (!is_array($source)) { - return new Error(sprintf('Cannot convert %s to ChangeCollection.', gettype($source))); - } - + public function convert( + array $source, + ContentRepositoryId $contentRepositoryId + ): ChangeCollection { $changeCollection = new ChangeCollection(); foreach ($source as $changeData) { - $convertedData = $this->convertChangeData($changeData); - - if ($convertedData instanceof Error) { - return $convertedData; - } + $convertedData = $this->convertChangeData($changeData, $contentRepositoryId); $changeCollection->add($convertedData); } @@ -121,52 +113,51 @@ public function convertFrom($source, $targetType, array $subProperties = [], ?Pr /** * Convert array to change interface * - * @param array $changeData - * @return ChangeInterface + * @param array $changeData */ - protected function convertChangeData($changeData) + protected function convertChangeData(array $changeData, ContentRepositoryId $contentRepositoryId): ChangeInterface { $type = $changeData['type']; if (!isset($this->typeMap[$type])) { - return new Error(sprintf('Could not convert change type %s, it is unknown to the system', $type)); + throw new \RuntimeException(sprintf('Could not convert change type %s, it is unknown to the system', $type)); } $changeClass = $this->typeMap[$type]; + /** @var ChangeInterface $changeClassInstance */ $changeClassInstance = $this->objectManager->get($changeClass); - $changeClassInstance->injectPersistenceManager($this->persistenceManager); - $subjectContextPath = $changeData['subject']; - $subject = $this->nodeService->getNodeFromContextPath($subjectContextPath, null, null, true); - if ($subject instanceof Error) { - return $subject; + + $subjectContextPath = $changeData['subject']; + $subject = $this->nodeService->findNodeBySerializedNodeAddress($subjectContextPath); + // we guard that `setSubject` gets a Node! + if (is_null($subject)) { + throw new \RuntimeException('Could not find node for subject "' . $subjectContextPath . '"', 1645657340); } $changeClassInstance->setSubject($subject); if (isset($changeData['reference']) && method_exists($changeClassInstance, 'setReference')) { $referenceContextPath = $changeData['reference']; - $reference = $this->nodeService->getNodeFromContextPath($referenceContextPath); - - if ($reference instanceof Error) { - return $reference; - } - + $reference = $this->nodeService->findNodeBySerializedNodeAddress($referenceContextPath); $changeClassInstance->setReference($reference); } if (isset($changeData['payload'])) { foreach ($changeData['payload'] as $propertyName => $value) { if (!in_array($propertyName, $this->disallowedPayloadProperties)) { - $methodParameters = $this->reflectionService->getMethodParameters($changeClass, ObjectAccess::buildSetterMethodName($propertyName)); + $methodParameters = $this->reflectionService->getMethodParameters( + $changeClass, + ObjectAccess::buildSetterMethodName($propertyName) + ); $methodParameter = current($methodParameters); $targetType = $methodParameter['type']; // Fixme: The type conversion runs depending on the target node property type inside Property::class // This is why we are not allowed to modify the value in any way. - // Without this condition the object was parsed to a string leading to fatal errors when changing images - // in the UI. + // Without this condition the object was parsed to a string leading to fatal errors + // when changing images in the UI. if ($propertyName !== 'value' && $targetType !== Property::class) { $value = $this->propertyMapper->convert($value, $targetType); } diff --git a/Classes/TypeConverter/UiDependentImageSerializer.php b/Classes/TypeConverter/UiDependentImageSerializer.php deleted file mode 100644 index bcd6d2cf1d..0000000000 --- a/Classes/TypeConverter/UiDependentImageSerializer.php +++ /dev/null @@ -1,45 +0,0 @@ -objectManager->get(ImageInterfaceArrayPresenter::class); - return $innerConverter->convertFrom($source, $targetType, $convertedChildProperties, $configuration); - } -} diff --git a/Classes/View/OutOfBandRenderingCapable.php b/Classes/View/OutOfBandRenderingCapable.php new file mode 100644 index 0000000000..d565b33845 --- /dev/null +++ b/Classes/View/OutOfBandRenderingCapable.php @@ -0,0 +1,32 @@ +setFusionPath($renderingEntryPoint); + } +} diff --git a/Classes/View/OutOfBandRenderingViewFactory.php b/Classes/View/OutOfBandRenderingViewFactory.php new file mode 100644 index 0000000000..682f5000ab --- /dev/null +++ b/Classes/View/OutOfBandRenderingViewFactory.php @@ -0,0 +1,57 @@ +viewObjectName)) { + throw new \DomainException( + 'Declared view for out of band rendering (' . $this->viewObjectName . ') does not exist', + 1697821296 + ); + } + $view = new $this->viewObjectName(); + if (!$view instanceof AbstractView) { + throw new \DomainException( + 'Declared view (' . $this->viewObjectName . ') does not implement ' . AbstractView::class + . ' required for out-of-band rendering', + 1697821429 + ); + } + if (!$view instanceof OutOfBandRenderingCapable) { + throw new \DomainException( + 'Declared view (' . $this->viewObjectName . ') does not implement ' . OutOfBandRenderingCapable::class + . ' required for out-of-band rendering', + 1697821364 + ); + } + + return $view; + } +} diff --git a/Configuration/NodeTypes.yaml b/Configuration/NodeTypes.yaml index a9296435b9..16968ddde1 100644 --- a/Configuration/NodeTypes.yaml +++ b/Configuration/NodeTypes.yaml @@ -1,16 +1,17 @@ 'Neos.Neos:Document': + postprocessors: + 'CreationDialogPostprocessor': + position: 'after NodeTypePresetPostprocessor' + postprocessor: 'Neos\Neos\Ui\Infrastructure\ContentRepository\CreationDialog\CreationDialogNodeTypePostprocessor' ui: - group: 'general' creationDialog: elements: title: - type: string - ui: - label: i18n - editor: 'Neos.Neos/Inspector/Editors/TextFieldEditor' - validation: - 'Neos.Neos/Validation/NotEmptyValidator': [] + position: 'start' properties: + title: + ui: + showInCreationDialog: true uriPathSegment: ui: inspector: @@ -19,14 +20,22 @@ title: "ClientEval:node.properties.title" options: nodeCreationHandlers: - documentTitle: - nodeCreationHandler: 'Neos\Neos\Ui\NodeCreationHandler\DocumentTitleNodeCreationHandler' + uriPathSegment: + factoryClassName: 'Neos\Neos\Ui\Infrastructure\Neos\UriPathSegmentNodeCreationHandlerFactory' + promotedElements: + factoryClassName: 'Neos\Neos\Ui\Infrastructure\ContentRepository\CreationDialog\PromotedElementsCreationHandlerFactory' + moveNodeStrategy: gatherAll 'Neos.Neos:Content': + postprocessors: + 'CreationDialogPostprocessor': + position: 'after NodeTypePresetPostprocessor' + postprocessor: 'Neos\Neos\Ui\Infrastructure\ContentRepository\CreationDialog\CreationDialogNodeTypePostprocessor' options: nodeCreationHandlers: - documentTitle: - nodeCreationHandler: 'Neos\Neos\Ui\NodeCreationHandler\ContentTitleNodeCreationHandler' + promotedElements: + factoryClassName: 'Neos\Neos\Ui\Infrastructure\ContentRepository\CreationDialog\PromotedElementsCreationHandlerFactory' + moveNodeStrategy: scatter 'Neos.Neos:ContentCollection': ui: diff --git a/Configuration/Objects.yaml b/Configuration/Objects.yaml new file mode 100644 index 0000000000..9c055c11f9 --- /dev/null +++ b/Configuration/Objects.yaml @@ -0,0 +1,33 @@ +# +# InitialData Providers for booting the UI +# +Neos\Neos\Ui\Domain\InitialData\CacheConfigurationVersionProviderInterface: + className: Neos\Neos\Ui\Infrastructure\Cache\CacheConfigurationVersionProvider + +Neos\Neos\Ui\Domain\InitialData\ConfigurationProviderInterface: + className: Neos\Neos\Ui\Infrastructure\Configuration\ConfigurationProvider + +Neos\Neos\Ui\Domain\InitialData\FrontendConfigurationProviderInterface: + className: Neos\Neos\Ui\Infrastructure\Configuration\FrontendConfigurationProvider + +Neos\Neos\Ui\Domain\InitialData\InitialStateProviderInterface: + className: Neos\Neos\Ui\Infrastructure\Configuration\InitialStateProvider + +Neos\Neos\Ui\Domain\InitialData\MenuProviderInterface: + className: Neos\Neos\Ui\Infrastructure\Neos\MenuProvider + +Neos\Neos\Ui\Domain\InitialData\NodeTypeGroupsAndRolesProviderInterface: + className: Neos\Neos\Ui\Infrastructure\ContentRepository\NodeTypeGroupsAndRolesProvider + +Neos\Neos\Ui\Domain\InitialData\RoutesProviderInterface: + className: Neos\Neos\Ui\Infrastructure\MVC\RoutesProvider + +Neos\Neos\Ui\Infrastructure\Cache\CacheConfigurationVersionProvider: + properties: + configurationCache: + object: + factoryObjectName: Neos\Flow\Cache\CacheManager + factoryMethodName: getCache + arguments: + 1: + value: Neos_Neos_Configuration_Version diff --git a/Configuration/Policy.yaml b/Configuration/Policy.yaml index 5fe391a9de..ad851cdbbf 100644 --- a/Configuration/Policy.yaml +++ b/Configuration/Policy.yaml @@ -6,9 +6,6 @@ privilegeTargets: 'Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege': - 'Neos.Neos.Ui:BackendLogin': - matcher: 'method(Neos\Neos\Ui\Controller\LoginController->(index|authenticate)Action()) || method(Neos\Flow\Security\Authentication\Controller\AbstractAuthenticationController->authenticateAction())' - 'Neos.Neos.Ui:Backend.GeneralAccess': matcher: 'method(Neos\Neos\Ui\Controller\BackendController->.*())' @@ -22,12 +19,6 @@ privilegeTargets: roles: - 'Neos.Flow:Everybody': - privileges: - - - privilegeTarget: 'Neos.Neos.Ui:BackendLogin' - permission: GRANT - 'Neos.Neos:AbstractEditor': privileges: - diff --git a/Configuration/Routes.Service.yaml b/Configuration/Routes.Service.yaml index e69d3f0ef9..2fcfd8ffb0 100644 --- a/Configuration/Routes.Service.yaml +++ b/Configuration/Routes.Service.yaml @@ -7,19 +7,43 @@ httpMethods: ['POST'] - - name: 'Publish' - uriPattern: 'publish' + name: 'Publish all changes in site' + uriPattern: 'publish-changes-in-site' defaults: '@controller': 'BackendService' - '@action': 'publish' + '@action': 'publishChangesInSite' httpMethods: ['POST'] - - name: 'Discard' - uriPattern: 'discard' + name: 'Publish all changes in document' + uriPattern: 'publish-changes-in-document' defaults: '@controller': 'BackendService' - '@action': 'discard' + '@action': 'publishChangesInDocument' + httpMethods: ['POST'] + +- + name: 'Discard all changes in workspace' + uriPattern: 'discard-all-changes' + defaults: + '@controller': 'BackendService' + '@action': 'discardAllChanges' + httpMethods: ['POST'] + +- + name: 'Discard all changes in site' + uriPattern: 'discard-changes-in-site' + defaults: + '@controller': 'BackendService' + '@action': 'discardChangesInSite' + httpMethods: ['POST'] + +- + name: 'Discard all changes in document' + uriPattern: 'discard-changes-in-document' + defaults: + '@controller': 'BackendService' + '@action': 'discardChangesInDocument' httpMethods: ['POST'] - @@ -30,6 +54,13 @@ '@action': 'changeBaseWorkspace' httpMethods: ['POST'] +- + name: 'Sync Workspace' + uriPattern: 'sync-workspace' + defaults: + '@controller': 'BackendService' + '@action': 'syncWorkspace' + httpMethods: ['POST'] - name: 'Copy nodes to clipboard' uriPattern: 'copy-nodes' @@ -54,14 +85,6 @@ '@action': 'clearClipboard' httpMethods: ['POST'] -- - name: 'Load Tree' - uriPattern: 'load-tree' - defaults: - '@controller': 'BackendService' - '@action': 'loadTree' - httpMethods: ['POST'] - - name: 'FlowQuery' uriPattern: 'flow-query' @@ -92,3 +115,11 @@ '@controller': 'BackendService' '@action': 'generateUriPathSegment' httpMethods: ['POST'] + +- + name: 'Reload Nodes' + uriPattern: 'reload-nodes' + defaults: + '@controller': 'BackendService' + '@action': 'reloadNodes' + httpMethods: ['POST'] diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 7692b72f7d..60e424e011 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -20,20 +20,20 @@ Neos: inspector: dataTypes: Neos\Media\Domain\Model\ImageInterface: - typeConverter: Neos\Neos\Ui\TypeConverter\UiDependentImageSerializer + typeConverter: Neos\Media\TypeConverter\ImageInterfaceArrayPresenter translation: autoInclude: 'Neos.Neos.Ui': + - Error - Main + - PublishingDialog + - SyncWorkspaceDialog fusion: autoInclude: Neos.Neos.Ui: true Ui: - splashScreen: - partial: 'SplashScreen' - # API: "start 100" and smaller numbers; "no numbers", ... resources: @@ -54,7 +54,7 @@ Neos: backgroundColor: '#ffffff' frontendConfiguration: - editPreviewModes: '${Neos.Ui.PositionalArraySorter.sort(Configuration.setting(''Neos.Neos.userInterface.editPreviewModes''))}' + editPreviewModes: '${Neos.Ui.RenderingMode.findAllSorted()}' # You may use this place to deliver some configuration to your custom UI components, e.g.: # 'Your.Own:Package': # someKey: someValue @@ -91,6 +91,11 @@ Neos: 'CR.Nodes.unfocus': 'u n' + # If enabled, will automatically synchronize (rebase) personal workspaces when used in the UI + # (currently: Neos\Neos\Ui\Controller\BackendController::indexAction()) + # if necessary (outdated) and possible (no own changes) + autoSyncPersonalWorkspaces: true + ################################# # INTERNAL CONFIG (no API) ################################# @@ -102,21 +107,20 @@ Neos: content: 'Neos.Neos:Content' contentCollection: 'Neos.Neos:ContentCollection' configurationDefaultEelContext: - Neos.Ui.Api: Neos\Neos\Ui\Fusion\Helper\ApiHelper - Neos.Ui.Workspace: Neos\Neos\Ui\Fusion\Helper\WorkspaceHelper - Neos.Ui.NodeInfo: Neos\Neos\Ui\Fusion\Helper\NodeInfoHelper Neos.Ui.ContentDimensions: Neos\Neos\Ui\Fusion\Helper\ContentDimensionsHelper + Neos.Ui.NodeInfo: Neos\Neos\Ui\Fusion\Helper\NodeInfoHelper + Neos.Ui.RenderingMode: Neos\Neos\Ui\Fusion\Helper\RenderingModeHelper Neos.Ui.StaticResources: Neos\Neos\Ui\Fusion\Helper\StaticResourcesHelper - Neos.Ui.PositionalArraySorter: Neos\Neos\Ui\Fusion\Helper\PositionalArraySorterHelper + Neos.Ui.Workspace: Neos\Neos\Ui\Fusion\Helper\WorkspaceHelper documentNodeInformation: metaData: - documentNode: '${q(documentNode).property("_contextPath")}' - siteNode: '${q(site).property(''_contextPath'')}' - previewUrl: '${Neos.Ui.NodeInfo.createRedirectToNode(controllerContext, documentNode)}' + documentNode: '${Neos.Ui.NodeInfo.serializedNodeAddress(documentNode)}' + siteNode: '${Neos.Ui.NodeInfo.serializedNodeAddress(site)}' + previewUrl: '${Neos.Ui.NodeInfo.createRedirectToNode(documentNode, request)}' contentDimensions: - active: '${documentNode.context.dimensions}' - allowedPresets: '${Neos.Ui.Api.emptyArrayToObject(Neos.Ui.ContentDimensions.allowedPresetsByName(documentNode.context.dimensions))}' - documentNodeSerialization: '${Neos.Ui.NodeInfo.renderNodeWithPropertiesAndChildrenInformation(documentNode, controllerContext)}' + active: '${Neos.Ui.ContentDimensions.dimensionSpacePointArray(documentNode.dimensionSpacePoint)}' + allowedPresets: '${Neos.Ui.ContentDimensions.allowedPresetsByName(documentNode.dimensionSpacePoint, documentNode.contentRepositoryId)}' + documentNodeSerialization: '${Neos.Ui.NodeInfo.renderNodeWithPropertiesAndChildrenInformation(documentNode, request)}' initialState: changes: pending: { } @@ -124,20 +128,20 @@ Neos: failed: { } cr: nodes: - byContextPath: '${Neos.Ui.NodeInfo.defaultNodesForBackend(site, documentNode, controllerContext)}' - siteNode: '${q(site).property(''_contextPath'')}' - documentNode: '${q(documentNode).property(''_contextPath'')}' + byContextPath: '${Neos.Ui.NodeInfo.defaultNodesForBackend(site, documentNode, request)}' + siteNode: '${Neos.Ui.NodeInfo.serializedNodeAddress(site)}' + documentNode: '${Neos.Ui.NodeInfo.serializedNodeAddress(documentNode)}' clipboard: '${clipboardNodes || []}' clipboardMode: '${clipboardMode || null}' contentDimensions: - byName: '${Neos.Ui.ContentDimensions.contentDimensionsByName()}' - active: '${documentNode.context.dimensions}' - allowedPresets: '${Neos.Ui.Api.emptyArrayToObject(Neos.Ui.ContentDimensions.allowedPresetsByName(documentNode.context.dimensions))}' + byName: '${Neos.Ui.ContentDimensions.contentDimensionsByName(documentNode.contentRepositoryId)}' + active: '${Neos.Ui.ContentDimensions.dimensionSpacePointArray(documentNode.dimensionSpacePoint)}' + allowedPresets: '${Neos.Ui.ContentDimensions.allowedPresetsByName(documentNode.dimensionSpacePoint, documentNode.contentRepositoryId)}' workspaces: - personalWorkspace: '${Neos.Ui.Workspace.getPersonalWorkspace()}' + personalWorkspace: '${Neos.Ui.Workspace.getPersonalWorkspace(documentNode.contentRepositoryId)}' ui: contentCanvas: - src: '${Neos.Ui.NodeInfo.uri(documentNode, controllerContext)}' + src: '${Neos.Ui.NodeInfo.previewUri(documentNode, request)}' backgroundColor: '${Configuration.setting(''Neos.Neos.Ui.contentCanvas.backgroundColor'')}' debugMode: false editPreviewMode: '${q(user).property("preferences.preferences")["contentEditing.editPreviewMode"] || Configuration.setting(''Neos.Neos.userInterface.defaultEditPreviewMode'')}' @@ -176,7 +180,6 @@ Neos: preferences: interfaceLanguage: '${q(user).property(''preferences.interfaceLanguage'') || Configuration.setting(''Neos.Neos.userInterface.defaultLanguage'')}' settings: - isAutoPublishingEnabled: false targetWorkspace: 'live' changes: types: @@ -191,6 +194,8 @@ Neos: 'Neos.Neos.Ui:MoveBefore': Neos\Neos\Ui\Domain\Model\Changes\MoveBefore 'Neos.Neos.Ui:MoveAfter': Neos\Neos\Ui\Domain\Model\Changes\MoveAfter 'Neos.Neos.Ui:MoveInto': Neos\Neos\Ui\Domain\Model\Changes\MoveInto + outOfBandRendering: + viewObjectName: 'Neos\Neos\Ui\View\OutOfBandRenderingFusionView' Flow: security: authentication: @@ -207,6 +212,4 @@ Neos: position: 'before Neos.Neos' Fusion: defaultContext: - Neos.Ui.Workspace: Neos\Neos\Ui\Fusion\Helper\WorkspaceHelper Neos.Ui.StaticResources: Neos\Neos\Ui\Fusion\Helper\StaticResourcesHelper - Neos.Ui.PositionalArraySorter: Neos\Neos\Ui\Fusion\Helper\PositionalArraySorterHelper diff --git a/Configuration/Views.yaml b/Configuration/Views.yaml deleted file mode 100644 index 1e29893dac..0000000000 --- a/Configuration/Views.yaml +++ /dev/null @@ -1,7 +0,0 @@ -- - requestFilter: 'isPackage("Neos.Neos.Ui") && isController("Backend")' - viewObjectName: 'Neos\Fusion\View\FusionView' - options: - fusionPathPatterns: - - 'resource://Neos.Neos.Ui/Private/Fusion/Backend' - diff --git a/Makefile b/Makefile index b42c73ec6c..1fce7bc9b0 100644 --- a/Makefile +++ b/Makefile @@ -115,6 +115,10 @@ test-e2e: bash Tests/IntegrationTests/e2e.sh --browser chrome:--disable-search-engine-choice-screen ## Executes integration tests locally in a docker-compose setup. +# +# Note: On mac os you might need those two additional `/etc/hosts` entries: +# 127.0.0.1 onedimension.localhost +# 127.0.0.1 twodimensions.localhost test-e2e-docker: build-e2e-testing @bash Tests/IntegrationTests/e2e-docker.sh $(or $(browser),chrome:--disable-search-engine-choice-screen) diff --git a/README.md b/README.md index fb3c061f01..10807fef05 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,9 @@ Release roadmap is [available here](https://www.neos.io/features/release-process That means: * All bugfixes go to the lowest maintained branch -* All new features go only to the 8.4 branch -* New minor and major releases are made in sync with Neos/Flow. Bugfix releases may be available independantly +* All new features go only to the 8.4 and 9.0 branch +* New minor and major releases are made in sync with Neos/Flow. Bugfix releases may be available independently + ### Currently maintained versions @@ -54,12 +55,13 @@ composer update neos/neos-ui ### Installing latest development -For trying out the new UI, we recommend you to run the regularily released beta releases. +For trying out the new UI, we recommend you to run the regularly released beta releases. However, if you want to stay on bleeding-edge, or want to help out developing, you'll -need the `8.3.x-dev` release. You can install the latest release using: +need the `9.0.x-dev` release. You can install the latest release using: + ```bash -composer require neos/neos-ui-compiled:8.4.x-dev neos/neos-ui:8.4.x-dev +composer require neos/neos-ui-compiled:9.0.x-dev neos/neos-ui:9.0.x-dev ``` ## Contributing @@ -151,6 +153,8 @@ git checkout 8.3 && git fetch && git reset --hard origin/8.3 && git merge --no-f # review and `git commit` git checkout 8.4 && git fetch && git reset --hard origin/8.4 && git merge --no-ff --no-commit origin/8.3 # review and `git commit` +git checkout 9.0 && git fetch && git reset --hard origin/9.0 && git merge --no-ff --no-commit origin/8.4 +# review and `git commit` ``` #### Development commands @@ -204,18 +208,6 @@ To speed up the e2e-test workflow/feedback loop you can start the system under t * Observe Flow exceptions and logs in build artifacts. * You can trigger a SSH enabled build via the CircleCI interface and then login. -###### Just the end to end tests fail - -It can happen that end to end tests fail caused by cached sources. So if you change PHP code for instance and don't adjust the composer.json it can happen that your new code change is not used because it is not part of the cache. In this case we need to flush the CircleCI caches manualy. - -We have introduced an environment variable called CACHE_VERSION. We need to change the variable to to new timestamp for instance to invalidate the caches. - -1. go to https://app.circleci.com/settings/project/github/neos/neos-ui and login -2. open the project settings and choose `Environment Variables` -3. Delete the `CACHE_VERSION` and create a new one with the value of the current timestamp - -Retrigger the build and it should work. - #### Releasing You only need to trigger the jenkins release with the version you want to release. diff --git a/Resources/Private/Fusion/Backend/Component/ModuleMenu.fusion b/Resources/Private/Fusion/Backend/Component/ModuleMenu.fusion deleted file mode 100644 index 024315ea5a..0000000000 --- a/Resources/Private/Fusion/Backend/Component/ModuleMenu.fusion +++ /dev/null @@ -1,64 +0,0 @@ -prototype(Neos.Neos.Ui:Component.ModuleMenu) < prototype(Neos.Fusion:Map) { - items = ${modulesForMenu} - itemName = 'module' - itemKey = 'moduleName' - - keyRenderer = ${moduleName} - itemRenderer = Neos.Fusion:DataStructure { - @if.moduleNotHidden = ${module.hideInMenu != true} - label = ${module.label} - icon = ${module.icon} - uri = ${module.uri} - target = 'Window' - - children = Neos.Fusion:Case { - sites { - condition = ${module.module == 'content'} - renderer = Neos.Fusion:Map { - items = ${sitesForMenu} - itemName = 'currentSiteInMenu' - iterationName = 'iteration' - - keyRenderer = ${iteration.index} - itemRenderer = Neos.Fusion:DataStructure { - icon = 'globe' - label = ${currentSiteInMenu.name} - uri = ${currentSiteInMenu.uri} - target = 'Window' - isActive = ${currentSiteInMenu.active} - skipI18n = true - } - } - } - - submodules { - condition = ${true} - renderer = Neos.Fusion:Map { - items = ${module.submodules} - itemName = 'submodule' - itemKey = 'submoduleName' - iterationName = 'iteration' - - keyRenderer = ${submoduleName} - itemRenderer = Neos.Fusion:DataStructure { - @if.moduleNotHidden = ${submodule.hideInMenu != true} - icon = ${submodule.icon} - label = ${submodule.label} - uri = ${submodule.uri} - position = ${submodule.position} - isActive = true - target = 'Window' - skipI18n = false - } - - @process.filterHiddenSubmodules = ${Array.filter(value, (x, index) => x != null)} - @process.sort = ${Array.slice(Neos.Ui.PositionalArraySorter.sort(value), 0)} - @process.values = ${Array.values(value)} - } - } - } - } - - @process.filterHiddenModules = ${Array.values(Array.filter(value, (x, index) => x != null))} - @process.json = ${Json.stringify(value)} -} diff --git a/Resources/Private/Fusion/Backend/Root.fusion b/Resources/Private/Fusion/Backend/Root.fusion deleted file mode 100644 index 7f46b1c757..0000000000 --- a/Resources/Private/Fusion/Backend/Root.fusion +++ /dev/null @@ -1,286 +0,0 @@ -include: resource://Neos.Fusion/Private/Fusion/Root.fusion -include: resource://Neos.Neos/Private/Fusion/Prototypes/NodeUri.fusion -include: resource://Neos.Neos.Ui/Private/Fusion/Prototypes/RenderConfiguration.fusion -include: resource://Neos.Neos.Ui/Private/Fusion/Backend/Component/* - -backend = Neos.Fusion:Template { - templatePath = 'resource://Neos.Neos.Ui/Private/Templates/Backend/Index.html' - - headScripts = ${headScripts} - headStylesheets = ${headStylesheets} - splashScreenPartial = ${splashScreenPartial} - - headIcons = Neos.Fusion:Join { - appleTouchIcon = Neos.Fusion:Tag { - tagName = 'link' - attributes { - href = Neos.Fusion:ResourceUri { - path = 'resource://Neos.Neos.Ui/Public/Images/apple-touch-icon.png' - } - sizes = '180x180' - rel = 'apple-touch-icon' - } - } - favicon16 = Neos.Fusion:Tag { - tagName = 'link' - attributes { - href = Neos.Fusion:ResourceUri { - path = 'resource://Neos.Neos.Ui/Public/Images/favicon-16x16.png' - } - sizes = '16x16' - rel = 'icon' - type = 'image/png' - } - } - favicon32 = Neos.Fusion:Tag { - tagName = 'link' - attributes { - href = Neos.Fusion:ResourceUri { - path = 'resource://Neos.Neos.Ui/Public/Images/favicon-32x32.png' - } - sizes = '32x32' - rel = 'icon' - type = 'image/png' - } - } - safariPinnedTab = Neos.Fusion:Tag { - tagName = 'link' - attributes { - href = Neos.Fusion:ResourceUri { - path = 'resource://Neos.Neos.Ui/Public/Images/safari-pinned-tab.svg' - } - rel = 'mask-icon' - color = '#00adee' - } - } - } - - configuration = Neos.Fusion:DataStructure { - nodeTree = ${Configuration.setting('Neos.Neos.userInterface.navigateComponent.nodeTree')} - structureTree = ${Configuration.setting('Neos.Neos.userInterface.navigateComponent.structureTree')} - allowedTargetWorkspaces = ${Neos.Ui.Workspace.getAllowedTargetWorkspaces()} - endpoints = Neos.Fusion:DataStructure { - nodeTypeSchema = Neos.Fusion:UriBuilder { - package = 'Neos.Neos' - controller = 'Backend\\Schema' - action = 'nodeTypeSchema' - absolute = true - arguments = Neos.Fusion:DataStructure { - # TODO: dirty hack to not have to re-implement neos:backend.configurationCacheVersion VH - version = Neos.Fusion:Template { - templatePath = 'resource://Neos.Neos.Ui/Private/Templates/Backend/ConfigurationVersion.html' - @process.trim = ${String.trim(value)} - } - } - } - translations = Neos.Fusion:UriBuilder { - package = 'Neos.Neos' - controller = 'Backend\\Backend' - action = 'xliffAsJson' - absolute = true - arguments = Neos.Fusion:DataStructure { - locale = ${interfaceLanguage} - - # TODO: dirty hack to not have to re-implement neos:backend.configurationCacheVersion VH - version = Neos.Fusion:Template { - templatePath = 'resource://Neos.Neos.Ui/Private/Templates/Backend/ConfigurationVersion.html' - @process.trim = ${String.trim(value)} - } - } - } - } - @process.json = ${Json.stringify(value)} - } - - routes = Neos.Fusion:DataStructure { - prototype(Neos.Fusion:UriBuilder) { - absolute = true - } - - ui = Neos.Fusion:DataStructure { - service = Neos.Fusion:DataStructure { - prototype(Neos.Fusion:UriBuilder) { - package = 'Neos.Neos.Ui' - controller = 'BackendService' - } - - change = Neos.Fusion:UriBuilder { - action = 'change' - } - publish = Neos.Fusion:UriBuilder { - action = 'publish' - } - discard = Neos.Fusion:UriBuilder { - action = 'discard' - } - changeBaseWorkspace = Neos.Fusion:UriBuilder { - action = 'changeBaseWorkspace' - } - copyNodes = Neos.Fusion:UriBuilder { - action = 'copyNodes' - } - cutNodes = Neos.Fusion:UriBuilder { - action = 'cutNodes' - } - clearClipboard = Neos.Fusion:UriBuilder { - action = 'clearClipboard' - } - loadTree = Neos.Fusion:UriBuilder { - action = 'loadTree' - } - flowQuery = Neos.Fusion:UriBuilder { - action = 'flowQuery' - } - generateUriPathSegment = Neos.Fusion:UriBuilder { - action = 'generateUriPathSegment' - } - getWorkspaceInfo = Neos.Fusion:UriBuilder { - action = 'getWorkspaceInfo' - } - getAdditionalNodeMetadata = Neos.Fusion:UriBuilder { - action = 'getAdditionalNodeMetadata' - } - } - } - core = Neos.Fusion:DataStructure { - prototype(Neos.Fusion:UriBuilder) { - package = 'Neos.Neos' - } - - content = Neos.Fusion:DataStructure { - prototype(Neos.Fusion:UriBuilder) { - controller = 'Backend\\Content' - } - - imageWithMetadata = Neos.Fusion:UriBuilder { - action = 'imageWithMetaData' - } - createImageVariant = Neos.Fusion:UriBuilder { - action = 'createImageVariant' - } - loadMasterPlugins = Neos.Fusion:UriBuilder { - action = 'masterPlugins' - } - loadPluginViews = Neos.Fusion:UriBuilder { - action = 'pluginViews' - } - uploadAsset = Neos.Fusion:UriBuilder { - action = 'uploadAsset' - } - } - service = Neos.Fusion:DataStructure { - assetProxies = Neos.Fusion:UriBuilder { - controller = 'Service\\AssetProxies' - action = 'index' - } - assets = Neos.Fusion:UriBuilder { - controller = 'Service\\Assets' - action = 'index' - } - nodes = Neos.Fusion:UriBuilder { - controller = 'Service\\Nodes' - action = 'index' - } - userPreferences = Neos.Fusion:UriBuilder { - subpackage = 'Service' - controller = 'UserPreference' - action = 'index' - format = 'json' - } - dataSource = Neos.Fusion:UriBuilder { - subpackage = 'Service' - controller = 'DataSource' - action = 'index' - format = 'json' - } - contentDimensions = Neos.Fusion:UriBuilder { - package = 'Neos.Neos' - controller = 'Service\\ContentDimensions' - action = 'index' - } - impersonateStatus = Neos.Fusion:UriBuilder { - package = 'Neos.Neos' - controller = 'Backend\\Impersonate' - action = 'status' - format = 'json' - } - impersonateRestore = Neos.Fusion:UriBuilder { - package = 'Neos.Neos' - controller = 'Backend\\Impersonate' - action = 'restoreWithResponse' - format = 'json' - } - } - modules = Neos.Fusion:DataStructure { - prototype(Neos.Fusion:UriBuilder) { - controller = 'Backend\\Module' - } - workspaces = Neos.Fusion:UriBuilder { - action = 'index' - arguments { - module = 'management/workspaces' - } - } - userSettings = Neos.Fusion:UriBuilder { - controller = 'Backend\\Module' - action = 'index' - arguments { - module = 'user/usersettings' - } - } - mediaBrowser = Neos.Fusion:UriBuilder { - controller = 'Backend\\Module' - action = 'index' - arguments { - module = 'media/browser' - } - } - defaultModule = Neos.Fusion:UriBuilder { - package = 'Neos.Neos' - controller = 'Backend\\Backend' - action = 'index' - absolute = true - } - } - login = Neos.Fusion:UriBuilder { - controller = 'Login' - action = 'index' - format = 'json' - } - logout = Neos.Fusion:UriBuilder { - controller = 'Login' - action = 'logout' - } - } - @process.json = ${Json.stringify(value)} - } - - frontendConfiguration = Neos.Neos.Ui:RenderConfiguration { - path = 'frontendConfiguration' - @process.json = ${Json.stringify(value)} - } - - nodeTypes = Neos.Fusion:DataStructure { - roles = ${Configuration.setting('Neos.Neos.Ui.nodeTypeRoles')} - groups = ${Neos.Ui.PositionalArraySorter.sort(Configuration.setting('Neos.Neos.nodeTypes.groups'))} - - @process.json = ${Json.stringify(value)} - } - - menu = Neos.Neos.Ui:Component.ModuleMenu - - initialState = Neos.Neos.Ui:RenderConfiguration { - path = 'initialState' - context { - documentNode = ${documentNode} - site = ${site} - user = ${user} - clipboardNodes = ${clipboardNodes} - clipboardMode = ${clipboardMode} - } - - @process.json = ${Json.stringify(value)} - } - - env = ${Configuration.setting('Neos.Flow.core.context')} -} diff --git a/Resources/Private/Fusion/Prototypes/Page.fusion b/Resources/Private/Fusion/Prototypes/Page.fusion index b8ea315e93..b07bb56216 100644 --- a/Resources/Private/Fusion/Prototypes/Page.fusion +++ b/Resources/Private/Fusion/Prototypes/Page.fusion @@ -19,9 +19,7 @@ prototype(Neos.Neos:Page) { @process.json = ${Json.stringify(value)} @process.wrapInJsObject = ${''} - @if { - inBackend = ${documentNode.context.inBackend} - } + @if.inBackend = ${renderingMode.isEdit || renderingMode.isPreview} // We need to ensure the JS backend information is always up to date, especially // when child nodes change. Otherwise errors like the following might happen: @@ -36,12 +34,12 @@ prototype(Neos.Neos:Page) { mode = 'cached' entryIdentifier { jsBackendInfo = 'javascriptBackendInformation' - documentNode = ${documentNode} - inBackend = ${documentNode.context.inBackend} + documentNode = ${Neos.Caching.entryIdentifierForNode(documentNode)} + inBackend = ${renderingMode.isEdit || renderingMode.isPreview} } entryTags { 1 = ${Neos.Caching.nodeTag(documentNode)} - 2 = ${documentNode.context.inBackend ? Neos.Caching.descendantOfTag(documentNode) : null} + 2 = ${(renderingMode.isEdit || renderingMode.isPreview) ? Neos.Caching.descendantOfTag(documentNode) : null} } } } @@ -53,20 +51,18 @@ prototype(Neos.Neos:Page) { compiledResourcePackage = ${Neos.Ui.StaticResources.compiledResourcePackage()} sectionName = 'guestFrameApplication' - @if { - inBackend = ${documentNode.context.inBackend} - } + @if.inBackend = ${renderingMode.isEdit || renderingMode.isPreview} } } neosBackendContainer = '
' neosBackendContainer.@position = 'before closingBodyTag' - neosBackendContainer.@if.inBackend = ${documentNode.context.inBackend} + neosBackendContainer.@if.inBackend = ${renderingMode.isEdit || renderingMode.isPreview} neosBackendNotification = Neos.Fusion:Template { @position = 'before closingBodyTag' templatePath = 'resource://Neos.Neos.Ui/Private/Templates/Backend/GuestNotificationScript.html' - @if.inBackend = ${documentNode.context.inBackend} + @if.inBackend = ${renderingMode.isEdit || renderingMode.isPreview} } @exceptionHandler = 'Neos\\Neos\\Ui\\Fusion\\ExceptionHandler\\PageExceptionHandler' diff --git a/Resources/Private/Fusion/Prototypes/Shortcut.fusion b/Resources/Private/Fusion/Prototypes/Shortcut.fusion deleted file mode 100644 index 0dfae44205..0000000000 --- a/Resources/Private/Fusion/Prototypes/Shortcut.fusion +++ /dev/null @@ -1,17 +0,0 @@ -prototype(Neos.Neos:Page) { - head { - shortcutCSS = Neos.Fusion:Tag { - @position = 'after guestFrameApplication' - - tagName = 'style' - content = Neos.Fusion:Value { - value = '#neos-shortcut {position: fixed;top: 0;left: 0;width: 100%;height: 100%;background-color: #323232;z-index: 9999;font-family: "Noto Sans", sans-serif;-webkit-font-smoothing: antialiased;}#neos-shortcut p {position: relative;margin: 0 auto;width: 500px;height: 60px;top: 50%;margin-top: -30px;color: #fff;font-size: 22px;line-height: 1.4;text-align: center;}#neos-shortcut p a {color: #00b5ff;text-decoration: none;}#neos-shortcut p a:hover {color: #39c6ff;}' - } - - @if { - inBackend = ${documentNode.context.inBackend} - isShortcut = ${q(node).is('[instanceof Neos.Neos:Shortcut]')} - } - } - } -} diff --git a/Resources/Private/Templates/Backend/Index.html b/Resources/Private/Templates/Backend/Index.html deleted file mode 100644 index 2305dc53e8..0000000000 --- a/Resources/Private/Templates/Backend/Index.html +++ /dev/null @@ -1,24 +0,0 @@ - -{namespace neos=Neos\Neos\ViewHelpers} - - - - Neos CMS - - - - - - - - - {headStylesheets -> f:format.raw()} - {headScripts -> f:format.raw()} - {headIcons -> f:format.raw()} - - -
- -
- - diff --git a/Resources/Private/Templates/Backend/Partials/SplashScreen.html b/Resources/Private/Templates/Backend/Partials/SplashScreen.html deleted file mode 100644 index 9d519f72a7..0000000000 --- a/Resources/Private/Templates/Backend/Partials/SplashScreen.html +++ /dev/null @@ -1,33 +0,0 @@ - -
- -
diff --git a/Resources/Private/Translations/de/Main.xlf b/Resources/Private/Translations/de/Main.xlf index 7b01d2c1e1..e8853537c7 100644 --- a/Resources/Private/Translations/de/Main.xlf +++ b/Resources/Private/Translations/de/Main.xlf @@ -574,6 +574,18 @@ Current value Aktueller Wert + + Maximum + Maximum + + + Minimum + Minimum + + + Current value + Aktueller Wert + Below Unten @@ -590,25 +602,25 @@ below unterhalb + + Copy technical details + Technische Details kopieren + + + Technical details copied + Technische Details kopiert + Reload Neos UI - Neos UI neu laden - - - Please reload the application, or contact your system administrator with the given details. - Bitte laden Sie die Anwendung neu, oder wenden Sie sich mit den angegebenen Details an Ihren Systemadministrator. + Neos Benutzeroberfläche neu laden Sorry, but the Neos UI could not recover from this error. - Es tut uns leid, aber die Neos UI konnte sich von diesem Fehler nicht wiederherstellen. - - - Technical details copied - Technische Details kopiert + Es tut uns leid, aber die Neos Benutzeroberfläche konnte von diesem Fehler nicht wiederhergestellt werden. - - Copy technical details - Technische Details kopieren + + Please reload the application, or contact your system administrator with the given details. + Bitte laden Sie die Anwendung neu, oder wenden Sie sich mit den angegebenen Details an Ihren Systemadministrator. For more information about the error please refer to the JavaScript console. @@ -622,18 +634,22 @@ Inside Innen - - Collapse All - Alle Ordner zuklappen - Add Hinzufügen + + Collapse All + Alle Ordner zuklappen + Copy node type to clipboard Knotentyp in die Zwischenablage kopieren + + Synchronize personal workspace + Persönlichen Arbeitsbereich synchronisieren + Invalid value Ungültiger Wert diff --git a/Resources/Private/Translations/en/Error.xlf b/Resources/Private/Translations/en/Error.xlf new file mode 100644 index 0000000000..ebb9952b7e --- /dev/null +++ b/Resources/Private/Translations/en/Error.xlf @@ -0,0 +1,10 @@ + + + + + + An unkown error ocurred. + + + + diff --git a/Resources/Private/Translations/en/Main.xlf b/Resources/Private/Translations/en/Main.xlf index f884b9ceef..b796789ee3 100644 --- a/Resources/Private/Translations/en/Main.xlf +++ b/Resources/Private/Translations/en/Main.xlf @@ -326,6 +326,14 @@ Discarded {0} changes. + + Switched base workspace to "{workspace}". + + + Your personal workspace currently contains unpublished changes. + In order to switch to a different target workspace you need to either publish or discard pending changes first. + + Syncronize with the title property @@ -347,6 +355,9 @@ Delete {amount} nodes + + Synchronize personal workspace + Minimum @@ -374,6 +385,12 @@ For more information about the error please refer to the JavaScript console. + + Node could not be published, probably because of a missing parentNode. Please check that the parentNode has been published. + + + Node could not be published, probably because the parentNode does not exist in the current dimension. Please check that the parentNode has been published. + Collapse All diff --git a/Resources/Private/Translations/en/PublishingDialog.xlf b/Resources/Private/Translations/en/PublishingDialog.xlf new file mode 100644 index 0000000000..3c86b9ce2b --- /dev/null +++ b/Resources/Private/Translations/en/PublishingDialog.xlf @@ -0,0 +1,223 @@ + + + + + + Publish all changes in workspace "{scopeTitle}" + + + Are you sure that you want to publish all {numberOfChanges} change(s) in workspace "{scopeTitle}" to workspace "{targetWorkspaceName}"? Be careful: This cannot be undone! + + + No, cancel + + + Yes, publish + + + Publishing all changes in workspace "{scopeTitle}"... + + + Please wait while {numberOfChanges} change(s) are being published. This may take a while. + + + Changes in workspace "{scopeTitle}" could not be published + + + Try again + + + Cancel + + + All changes in workspace "{scopeTitle}" were published + + + All {numberOfChanges} change(s) in workspace "{scopeTitle}" were successfully published to workspace "{targetWorkspaceName}". + + + OK + + + Publish all changes in site "{scopeTitle}" + + + Are you sure that you want to publish all {numberOfChanges} change(s) in site "{scopeTitle}" from workspace "{sourceWorkspaceName}" to workspace "{targetWorkspaceName}"? Be careful: This cannot be undone! + + + No, cancel + + + Yes, publish + + + Publishing all changes in site "{scopeTitle}"... + + + Please wait while {numberOfChanges} change(s) are being published. This may take a while. + + + Changes in site "{scopeTitle}" could not be published + + + Try again + + + Cancel + + + Changes in site "{scopeTitle}" were published + + + {numberOfChanges} change(s) in site "{scopeTitle}" were successfully published to workspace "{targetWorkspaceName}". + + + OK + + + Publish all changes in document "{scopeTitle}" + + + Are you sure that you want to publish all {numberOfChanges} change(s) in document "{scopeTitle}" from workspace "{sourceWorkspaceName}" to workspace "{targetWorkspaceName}"? Be careful: This cannot be undone! + + + No, cancel + + + Yes, publish + + + Publishing all changes in document "{scopeTitle}"... + + + Please wait while {numberOfChanges} change(s) are being published. This may take a while. + + + Changes in document "{scopeTitle}" could not be published + + + Try again + + + Cancel + + + Changes in document "{scopeTitle}" were published + + + {numberOfChanges} change(s) in document "{scopeTitle}" were sucessfully published to workspace "{targetWorkspaceName}". + + + OK + + + Discard all changes in workspace "{scopeTitle}" + + + Are you sure that you want to discard all {numberOfChanges} change(s) in workspace "{scopeTitle}"? Be careful: This cannot be undone! + + + No, cancel + + + Yes, discard + + + Discarding all changes in workspace "{scopeTitle}"... + + + Please wait while {numberOfChanges} change(s) are being discarded. This may take a while. + + + Changes in workspace "{scopeTitle}" could not be discarded + + + Try again + + + Cancel + + + All changes in workspace "{scopeTitle}" were discarded + + + All {numberOfChanges} change(s) in workspace "{scopeTitle}" were sucessfully discarded. + + + OK + + + Discard all changes in site "{scopeTitle}" + + + Are you sure that you want to discard all {numberOfChanges} change(s) in site "{scopeTitle}" from workspace "{sourceWorkspaceName}"? Be careful: This cannot be undone! + + + No, cancel + + + Yes, discard + + + Discarding all changes in site "{scopeTitle}"... + + + Please wait while {numberOfChanges} change(s) are being discarded. This may take a while. + + + Changes in site "{scopeTitle}" could not be discarded + + + Try again + + + Cancel + + + Changes in site "{scopeTitle}" were discarded + + + {numberOfChanges} change(s) were sucessfully discarded. + + + OK + + + Discard all changes in document "{scopeTitle}" + + + Are you sure that you want to discard all {numberOfChanges} change(s) in document "{scopeTitle}" from workspace "{sourceWorkspaceName}"? Be careful: This cannot be undone! + + + No, cancel + + + Yes, discard + + + Discarding all changes in document "{scopeTitle}"... + + + Please wait while {numberOfChanges} change(s) are being published. This may take a while. + + + Changes in document "{scopeTitle}" could not be discarded + + + Try again + + + Cancel + + + Changes in document "{scopeTitle}" were discarded + + + {numberOfChanges} change(s) were sucessfully discarded. + + + OK + + + + diff --git a/Resources/Private/Translations/en/SyncWorkspaceDialog.xlf b/Resources/Private/Translations/en/SyncWorkspaceDialog.xlf new file mode 100644 index 0000000000..510206d11b --- /dev/null +++ b/Resources/Private/Translations/en/SyncWorkspaceDialog.xlf @@ -0,0 +1,141 @@ + + + + + + Synchronize workspace "{workspaceName}" with "{baseWorkspaceName}" + + + Workspace "{baseWorkspaceName}" has been modified. You need to synchronize your workspace "{workspaceName}" with it in order to see those changes and avoid conflicts. Do you wish to proceed? + + + No, cancel + + + Yes, synchronize now + + + Synchronizing workspace "{workspaceName}"... + + + Please wait, while workspace "{workspaceName}" is being synchronized with recent changes in workspace "{baseWorkspaceName}". This may take a while. + + + Conflicts between workspace "{workspaceName}" and "{baseWorkspaceName}" + + + Workspace "{baseWorkspaceName}" contains modifications that are in conflict with the changes in workspace "{workspaceName}". + + + Show information about {numberOfConflicts} conflict(s) + + + In order to proceed, you need to decide what to do with the conflicting changes: + + + Drop conflicting changes + + + This will save all non-conflicting changes, while every conflicting change will be lost. + + + Discard workspace "{workspaceName}" + + + This will discard all changes in your workspace, including those on other sites. + + + Cancel Synchronization + + + Accept choice and continue + + + Discard all changes in workspace "{workspaceName}" + + + You are about to discard all {numberOfChanges} change(s) in workspace "{workspaceName}". This includes all changes on other sites. + + Do you wish to proceed? Be careful: This cannot be undone! + + + No, cancel + + + Yes, discard everything + + + You are about to drop the following changes: + + + Do you wish to proceed? Be careful: This cannot be undone! + + + No, cancel + + + Yes, drop those changes + + + Drop conflicting changes in workspace "{workspaceName}" + + + "{label}" has been edited. + + + "{label}" has been created. + + + "{label}" has been deleted. + + + "{label}" has been moved. + + + "{label}" or one of its ancestor nodes has been deleted. + + + Affected Site + + + Affected Document + + + What was changed? + + + Why is there a conflict? + + + Unknown Node + + + Unknown Document + + + Unknown Site + + + Workspace "{workspaceName}" is up-to-date + + + Workspace "{workspaceName}" has been successfully synchronized with all recent changes in workspace "{baseWorkspaceName}". + + + OK + + + Workspace "{workspaceName}" could not be synchronized + + + Workspace "{workspaceName}" could not be synchronized with the recent changes in workspace "{baseWorkspaceName}". + + + Cancel + + + Try again + + + + diff --git a/Resources/Private/Translations/es/Main.xlf b/Resources/Private/Translations/es/Main.xlf index af863e1df7..f05bd7f052 100644 --- a/Resources/Private/Translations/es/Main.xlf +++ b/Resources/Private/Translations/es/Main.xlf @@ -330,11 +330,11 @@ Escape Inspector - Inspector de Escapes + Dejar al inspector Resume Inspector - Inspector de currículum + Resumen del Inspector NodeCreationDialog Back @@ -442,14 +442,42 @@ {tabName} – {amountOfErrors} validation issues {tabName} - {amountOfErrors} problemas de validación - - Paragraph - Párrafo + + Above + Sobre Headline 1 Titular 1 + + Below + Abajo + + + Inside + Dentro + + + inside + dentro + + + above + arriba + + + below + abajo + + + Paragraph + Párrafo + + + Headline 2 + Titular 2 + Headline 3 Titular 3 @@ -470,49 +498,17 @@ Preformatted Preformateado - - Headline 2 - Titular 2 - - - Downloadable - Puede descargarse - - - Above - Sobre - Blockquote cita en bloque - - Below - Abajo - - - Inside - Dentro - - - inside - dentro - - - above - arriba - - - below - abajo - - - Please reload the application, or contact your system administrator with the given details. - Por favor, vuelve a cargar la aplicación o pónte en contacto con el administrador del sistema con los datos facilitados. + + Downloadable + Puede descargarse - - For more information about the error please refer to the JavaScript console. - Para obtener más información sobre el error, consulta la consola JavaScript. + + Copy technical details + Copiar la información técnica Technical details copied @@ -526,13 +522,13 @@ Sorry, but the Neos UI could not recover from this error. Lo sentimos, pero la interfaz de usuario de Neos no ha podido recuperarse de este error. - - Copy technical details - Copiar la información técnica + + Please reload the application, or contact your system administrator with the given details. + Por favor, vuelve a cargar la aplicación o pónte en contacto con el administrador del sistema con los datos facilitados. - - Format options - Opciones del formato + + For more information about the error please refer to the JavaScript console. + Para obtener más información sobre el error, consulta la consola JavaScript. Format options @@ -550,6 +546,38 @@ Collapse All Contraer todo + + Synchronize personal workspace + Sincronizar el espacio de trabajo personal + + + Copy node type to clipboard + Copiar el tipo de nodo al portapapeles + + + Collapse All + Contraer todo + + + Node could not be published, probably because of a missing parentNode. Please check that the parentNode has been published. + No se pudo publicar el nodo, probablemente debido a que falta un nodo principal. Verifique que parentNode haya sido publicado. + + + Node could not be published, probably because the parentNode does not exist in the current dimension. Please check that the parentNode has been published. + No se pudo publicar el nodo, probablemente porque el parentNode no existe en la dimensión actual. Verifique que parentNode haya sido publicado. + + + Switched base workspace to "{workspace}". + Cambiado el espacio de trabajo en base a "{workspace}". + + + Your personal workspace currently contains unpublished changes. + In order to switch to a different target workspace you need to either publish or discard pending changes first. + + Su espacio de trabajo personal contiene actualmente cambios sin publicar. + Para cambiar a un espacio de trabajo de destino diferente, primero debe publicar o descartar los cambios pendientes. + + Invalid value Valor incorrecto diff --git a/Resources/Private/Translations/fi/Main.xlf b/Resources/Private/Translations/fi/Main.xlf index ff4a677928..0bbc26e01c 100644 --- a/Resources/Private/Translations/fi/Main.xlf +++ b/Resources/Private/Translations/fi/Main.xlf @@ -352,17 +352,29 @@ Hylätty {0} muutosta. - - Add - Lisää + + Your personal workspace currently contains unpublished changes. + In order to switch to a different target workspace you need to either publish or discard pending changes first. + + Henkilökohtainen työtilasi sisältää tällä hetkellä julkaisemattomia muutoksia. + Jos haluat vaihtaa toiseen kohdetyötilaan, sinun on ensin joko julkaistava tai hylättävä odottavat muutokset. + + + + Reload Neos UI + Lataa Neos-käyttöliittymä uudelleen + + + Sorry, but the Neos UI could not recover from this error. + Anteeksi, mutta Neos-käyttöliittymä ei voinut toipua tästä virheestä. Above Yllä - - Below - Alla + + below + alla Inside @@ -376,9 +388,9 @@ above yllä - - below - alla + + Below + Alla Paragraph @@ -408,10 +420,6 @@ Headline 6 Otsikko 6 - - Preformatted - Esimuotoiltu - Blockquote Lohkolainaus @@ -420,6 +428,22 @@ Downloadable Ladattavissa + + Node could not be published, probably because of a missing parentNode. Please check that the parentNode has been published. + Solmua ei voitu julkaista, luultavasti puuttuvan vanhempisolmun takia. Tarkista, että parentNode on julkaistu. + + + Node could not be published, probably because the parentNode does not exist in the current dimension. Please check that the parentNode has been published. + Solmua ei voitu julkaista, koska vanhempisolmua ei ole olemassa nykyisessä dimensiossa. Tarkista, että parentNode on julkaistu. + + + Technical details copied + Tekniset yksityiskohdat kopioitu + + + Copy node type to clipboard + Kopioi solmun tyyppi leikepöydälle + Format options Muotovaihtoehdot @@ -428,10 +452,6 @@ {tabName} – {amountOfErrors} validation issue {tabName} – {amountOfErrors} vahvistusongelma - - {tabName} – {amountOfErrors} validation issues - {tabName} – {amountOfErrors} vahvistusongelmaa - Syncronize with the title property Synkronoi otsikkoominaisuuden kanssa @@ -444,6 +464,38 @@ content elements selected sisältöelementit valittuina + + Current value + Nykyinen arvo + + + Please reload the application, or contact your system administrator with the given details. + Lataa sovellus uudelleen tai ota yhteyttä järjestelmänvalvojaan annetuilla yksityistiedoilla. + + + Add + Lisää + + + Preformatted + Esimuotoiltu + + + {tabName} – {amountOfErrors} validation issues + {tabName} – {amountOfErrors} vahvistusongelmaa + + + Delete {amount} nodes + Poista {amount} solmua + + + Invalid value + Virheellinen arvo + + + Switched base workspace to "{workspace}". + Vaihdettiin perustyötilaksi "{workspace}". + documents selected asiakirjat valittuina @@ -456,42 +508,22 @@ Select a single content element in order to be able to edit its properties Valitse yksittäinen sisältöelementti, jotta voit muokata sen ominaisuuksia - - Delete {amount} nodes - Poista {amount} solmua - - - Maximum - Maksimi - - - Current value - Nykyinen arvo + + Synchronize personal workspace + Synkronoi henkilökohtainen työtila Minimum Minimi + + Maximum + Maksimi + Copy technical details Kopioi tekniset yksityiskohdat - - Technical details copied - Tekniset yksityiskohdat kopioitu - - - Reload Neos UI - Lataa Neos-käyttöliittymä uudelleen - - - Sorry, but the Neos UI could not recover from this error. - Anteeksi, mutta Neos-käyttöliittymä ei voinut toipua tästä virheestä. - - - Please reload the application, or contact your system administrator with the given details. - Lataa sovellus uudelleen tai ota yhteyttä järjestelmänvalvojaan annetuilla yksityistiedoilla. - For more information about the error please refer to the JavaScript console. Lisätietoja virheestä on JavaScript-konsolissa. @@ -500,14 +532,6 @@ Collapse All Supista kaikki - - Copy node type to clipboard - Kopioi solmun tyyppi leikepöydälle - - - Invalid value - Virheellinen arvo - diff --git a/Resources/Private/Translations/lv/Main.xlf b/Resources/Private/Translations/lv/Main.xlf index 6f163ef3f2..312142c19a 100644 --- a/Resources/Private/Translations/lv/Main.xlf +++ b/Resources/Private/Translations/lv/Main.xlf @@ -355,10 +355,6 @@ Discarded {0} changes. - - Inside - Iekšpusē - Above Virs @@ -367,6 +363,10 @@ Below Zem + + Inside + Iekšpusē + inside iekšpusē @@ -375,26 +375,26 @@ above virs + + Paragraph + Paragrāfs + Headline 1 Virsraksts 1 + + below + zem + Headline 2 Virsraksts 2 - - Paragraph - Paragrāfs - Headline 3 Virsraksts 3 - - below - zem - Headline 4 Virsraksts 4 diff --git a/Resources/Private/Translations/nl/Main.xlf b/Resources/Private/Translations/nl/Main.xlf index 54f076bdd3..79ea9fa13a 100644 --- a/Resources/Private/Translations/nl/Main.xlf +++ b/Resources/Private/Translations/nl/Main.xlf @@ -439,9 +439,9 @@ Downloadable Downloadbaar - - below - onder + + Blockquote + Blokcitaat Above @@ -451,21 +451,45 @@ Below Onder - - above - boven - Inside Binnen + + below + onder + inside binnen - - Blockquote - Blokcitaat + + above + boven + + + Copy technical details + Kopieer technische details + + + Technical details copied + Technische details gekopieerd + + + Reload Neos UI + Neos UI herladen + + + Sorry, but the Neos UI could not recover from this error. + Sorry, maar de Neos UI kon niet hersteld worden van deze fout. + + + Please reload the application, or contact your system administrator with the given details. + Herlaad u alstublieft de applicatie of geef de verschafte details door aan uw systeembeheerder. + + + For more information about the error please refer to the JavaScript console. + Meer informatie over deze fout kunt u vinden in de JavaScript console. Please reload the application, or contact your system administrator with the given details. @@ -499,18 +523,42 @@ Add Voeg toe - - Collapse All - Alles inklappen + + Synchronize personal workspace + Synchroniseer persoonlijk werkblad Copy node type to clipboard Kopieer node type naar klembord + + Collapse All + Alles inklappen + + + Node could not be published, probably because of a missing parentNode. Please check that the parentNode has been published. + De node kon niet worden gepubliceerd. Waarschijnlijk door een ontbrekende parent node. Controleer of de parent node is gepubliceerd. + + + Your personal workspace currently contains unpublished changes. + In order to switch to a different target workspace you need to either publish or discard pending changes first. + + Je persoonlijke workspace bevat niet gepubliceerde wijzigingen. +Om te wisselen naar een andere worksapce , zul je eerst je wijzingen moeten publiceren of ongedaan moeten maken. + + Invalid value Ongeldige waarde + + Node could not be published, probably because the parentNode does not exist in the current dimension. Please check that the parentNode has been published. + De node kon niet worden gepubliceerd. Waarschijnlijk omdat de parent node zich niet in de huidige dimensie bevindt. Controleer of de parent node is gepubliceerd. + + + Switched base workspace to "{workspace}". + Base workspace is gewisseld naar "{workspace}". + diff --git a/Resources/Private/Translations/pt/Main.xlf b/Resources/Private/Translations/pt/Main.xlf index f0cf21483c..b1ac58a4da 100644 --- a/Resources/Private/Translations/pt/Main.xlf +++ b/Resources/Private/Translations/pt/Main.xlf @@ -395,22 +395,6 @@ Current value Valor atual - - Copy technical details - Copiar detalhes técnicos - - - Sorry, but the Neos UI could not recover from this error. - Lamentamos, mas o Neos UI não consegue recuperar deste erro. - - - Please reload the application, or contact your system administrator with the given details. - Por favor recarregue a aplicação, ou envie ao seu administrador de sistema os detalhes apresentados. - - - Add - Adicionar - Above Por Cima @@ -419,6 +403,30 @@ Below Por Baixo + + Headline 1 + Título 1 + + + Headline 2 + Título 2 + + + Format options + Opções de formatação + + + Synchronize personal workspace + Sincronizar espaço de trabalho pessoal + + + Technical details copied + Os detalhes técnicos foram copiados + + + Add + Adicionar + above por cima @@ -431,17 +439,9 @@ Paragraph Parágrafo - - Headline 1 - Título 1 - - - Headline 2 - Título 2 - - - Headline 3 - Título 3 + + Headline 5 + Título 5 Headline 6 @@ -455,58 +455,82 @@ Downloadable Descarregável - - Format options - Opções de formatação + + Headline 3 + Título 3 + + + Headline 4 + Título 4 + + + Copy technical details + Copiar detalhes técnicos Reload Neos UI Recarregar Neos UI + + Sorry, but the Neos UI could not recover from this error. + Lamentamos, mas o Neos UI não consegue recuperar deste erro. + + + Please reload the application, or contact your system administrator with the given details. + Por favor recarregue a aplicação, ou envie ao seu administrador de sistema os detalhes apresentados. + For more information about the error please refer to the JavaScript console. Para mais informações sobre este erro por favor consulte a consola de JavaScript. - - Headline 4 - Título 4 - - - Headline 5 - Título 5 - - - Technical details copied - Os detalhes técnicos foram copiados - Inside No Interior - - inside - no interior - Blockquote Citação + + Published {0} changes to "{1}". + Foram publicadas {0} alterações em "{1}". + + + inside + no interior + Published {0} change to "{1}". Foi publicada {0} alteração em "{1}". - - Published {0} changes to "{1}". - Foram publicadas {0} alterações em "{1}". + + Node could not be published, probably because of a missing parentNode. Please check that the parentNode has been published. + Não foi possível publicar o nó, possivelmente por causa de um parentNode em falta. Por favor, verifique que o parentNode foi publicado. - - Copy node type to clipboard - Copiar o tipo de nó para a área de transferência + + Node could not be published, probably because the parentNode does not exist in the current dimension. Please check that the parentNode has been published. + Não foi possível publicar o nó, possivelmente o parentNode não pertence à dimensão atual. Por favor, verifique que o parentNode foi publicado. + + + Switched base workspace to "{workspace}". + Área de trabalho mudada para "{workspace}". + + + Your personal workspace currently contains unpublished changes. + In order to switch to a different target workspace you need to either publish or discard pending changes first. + + A sua área de trabalho pessoal conteḿ alterações pendentes. + Antes de mudar para uma nova área de trabalho, precisa de publicar ou descartar as alterações realizadas. + Collapse All Colapsar Tudo + + Copy node type to clipboard + Copiar o tipo de nó para a área de transferência + Invalid value Valor Inválido diff --git a/Resources/Private/Translations/pt_PT/Main.xlf b/Resources/Private/Translations/pt_PT/Main.xlf index db0c01819f..673f37874f 100644 --- a/Resources/Private/Translations/pt_PT/Main.xlf +++ b/Resources/Private/Translations/pt_PT/Main.xlf @@ -352,21 +352,57 @@ Foram descartadas {0} alterações. - - Add - Adicionar + + For more information about the error please refer to the JavaScript console. + Para mais informações sobre este erro por favor consulte a consola de JavaScript. - - Paragraph - Parágrafo + + below + por baixo - - Headline 1 - Título 1 + + Headline 2 + Título 2 - - Headline 3 - Título 3 + + Minimum + Mínimo + + + Node could not be published, probably because the parentNode does not exist in the current dimension. Please check that the parentNode has been published. + O Nó não pode ser publicado, provavelmente por faltar um parente na dimensão atual. Por favor confirme que o parente foi publicado. + + + {tabName} – {amountOfErrors} validation issue + {tabName} – {amountOfErrors} falha de validação + + + {tabName} – {amountOfErrors} validation issues + {tabName} – {amountOfErrors} falhas de validação + + + Select a single document in order to be able to edit its properties + Selecione um único documento para alterar as suas propriedades + + + Sorry, but the Neos UI could not recover from this error. + Lamentamos, mas o Neos UI não consegue recuperar deste erro. + + + Select a single content element in order to be able to edit its properties + Selecione um único elemento de conteúdo para editar as suas propriedades + + + Current value + Valor atual + + + Synchronize personal workspace + Sincronizar a Área de Trabalho pessoal + + + Add + Adicionar Above @@ -376,25 +412,9 @@ Below Por Baixo - - Inside - No Interior - - - inside - no interior - - - above - por cima - - - below - por baixo - - - Headline 4 - Título 4 + + Headline 1 + Título 1 Headline 5 @@ -416,70 +436,46 @@ Downloadable Descarregável + + Inside + No Interior + + + inside + no interior + + + above + por cima + Format options Opções de formatação - - {tabName} – {amountOfErrors} validation issue - {tabName} – {amountOfErrors} falha de validação - - - {tabName} – {amountOfErrors} validation issues - {tabName} – {amountOfErrors} falhas de validação + + Paragraph + Parágrafo - - Syncronize with the title property - Sincronizar com a propriedade "Título" + + Headline 4 + Título 4 - - Select a single document in order to be able to edit its properties - Selecione um único documento para alterar as suas propriedades + + Headline 3 + Título 3 Delete {amount} nodes Apagar {amount} nós - - Technical details copied - Os detalhes técnicos foram copiados + + Syncronize with the title property + Sincronizar com a propriedade "Título" Reload Neos UI Recarregar Neos UI - - Sorry, but the Neos UI could not recover from this error. - Lamentamos, mas o Neos UI não consegue recuperar deste erro. - - - Please reload the application, or contact your system administrator with the given details. - Por favor recarregue a aplicação, ou envie ao seu administrador de sistema os detalhes apresentados. - - - For more information about the error please refer to the JavaScript console. - Para mais informações sobre este erro por favor consulte a consola de JavaScript. - - - Collapse All - Colapsar Tudo - - - Copy node type to clipboard - Copiar o tipo de nó para a área de transferência - - - Headline 2 - Título 2 - - - Select a single content element in order to be able to edit its properties - Selecione um único elemento de conteúdo para editar as suas propriedades - - - Current value - Valor atual - nodes nós @@ -492,10 +488,6 @@ content elements selected elementos de conteúdo selecionados - - Minimum - Mínimo - Maximum Máximo @@ -504,6 +496,38 @@ Copy technical details Copiar detalhes técnicos + + Technical details copied + Os detalhes técnicos foram copiados + + + Please reload the application, or contact your system administrator with the given details. + Por favor recarregue a aplicação, ou envie ao seu administrador de sistema os detalhes apresentados. + + + Node could not be published, probably because of a missing parentNode. Please check that the parentNode has been published. + O Nó não pode ser publicado, provavelmente por faltar um parente. Por favor confirme que o parente foi publicado. + + + Switched base workspace to "{workspace}". + Área de Trabalho mudada para "{workspace}". + + + Collapse All + Colapsar Tudo + + + Copy node type to clipboard + Copiar o tipo de nó para a área de transferência + + + Your personal workspace currently contains unpublished changes. + In order to switch to a different target workspace you need to either publish or discard pending changes first. + + A sua Área de Trabalho contém alterações não publicadas. + De modo a definir uma nova Área de Trabalho deverá publicar ou descartar as alterações pendentes. + + Invalid value Valor inválido diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/Configuration/Settings.yaml b/Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/Configuration/Settings.yaml deleted file mode 100644 index b31328203b..0000000000 --- a/Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/Configuration/Settings.yaml +++ /dev/null @@ -1,53 +0,0 @@ -Neos: - ContentRepository: - contentDimensions: - language: - label: 'Language' - icon: icon-language - default: en_US - defaultPreset: en_US - presets: - en_US: - label: 'English (US)' - values: - - en_US - uriSegment: en - group: NON-EU - en_UK: - label: 'English (UK)' - values: - - en_UK - - en_US - uriSegment: uk - group: NON-EU - de: - label: German - values: - - de - uriSegment: de - group: EU - fr: - label: French - values: - - fr - uriSegment: fr - group: EU - nl: - label: Dutch - values: - - nl - - de - uriSegment: nl - group: EU - da: - label: Danish - values: - - da - uriSegment: da - group: EU - lv: - label: Latvian - values: - - lv - uriSegment: lv - group: EU diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/Resources/Private/Content/Resources/6791d1beca5fa4ab78f1b2b1a44d43692f99b5dc b/Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/Resources/Private/Content/Resources/6791d1beca5fa4ab78f1b2b1a44d43692f99b5dc deleted file mode 100644 index 49b2fb4403..0000000000 Binary files a/Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/Resources/Private/Content/Resources/6791d1beca5fa4ab78f1b2b1a44d43692f99b5dc and /dev/null differ diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/Resources/Private/Content/Sites.xml b/Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/Resources/Private/Content/Sites.xml deleted file mode 100644 index 0b506d9d48..0000000000 --- a/Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/Resources/Private/Content/Sites.xml +++ /dev/null @@ -1,1085 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/createNewNodes.e2e.js b/Tests/IntegrationTests/Fixtures/1Dimension/createNewNodes.e2e.js index fb0b0f9fdc..33f025fea8 100644 --- a/Tests/IntegrationTests/Fixtures/1Dimension/createNewNodes.e2e.js +++ b/Tests/IntegrationTests/Fixtures/1Dimension/createNewNodes.e2e.js @@ -1,6 +1,6 @@ import {Selector, RequestLogger} from 'testcafe'; import {ReactSelector} from 'testcafe-react-selectors'; -import {beforeEach, subSection, checkPropTypes} from '../../utils'; +import {beforeEach, subSection, checkPropTypes, typeTextInline, clearInlineText} from '../../utils'; import {Page} from '../../pageModel'; /* global fixture:true */ @@ -170,41 +170,43 @@ test('Can create content node from inside InlineUI', async t => { subSection('Type something inside of it'); await Page.waitForIframeLoading(t); + + await typeTextInline(t, '.test-headline:last-child [contenteditable="true"]', headlineTitle, 'heading1'); await t - .switchToIframe(contentIframeSelector) - .typeText(Selector('.test-headline h1'), headlineTitle) - .expect(Selector('.neos-contentcollection').withText(headlineTitle).exists).ok('Typed headline text exists'); + // .selectEditableContent(lastEditableElement, lastEditableElement) + // .pressKey(headlineTitle.split('').join(' ')) + .expect(Selector('.neos-contentcollection').withText(headlineTitle).exists).ok('Typed headline text exists') + .switchToMainWindow(); subSection('Inline validation'); // We have to wait for ajax requests to be triggered, since they are debounced for 0.5s await t.wait(1600); await changeRequestLogger.clear(); + await clearInlineText(t, Selector('.test-headline [contenteditable="true"]').nth(-1), true); await t - .expect(Selector('.test-headline h1').exists).ok('Validation tooltip appeared') - .click('.test-headline h1') - .pressKey('ctrl+a delete') .switchToMainWindow() .wait(1600) .expect(ReactSelector('InlineValidationTooltips').exists).ok('Validation tooltip appeared'); await t .expect(changeRequestLogger.count(() => true)).eql(0, 'No requests were fired with invalid state'); + await typeTextInline(t, '.test-headline:last-child [contenteditable="true"]', 'Some text', 'heading1'); await t - .switchToIframe(contentIframeSelector) - .typeText(Selector('.test-headline h1'), 'Some text') - .wait(1600); + .wait(1600) + .switchToMainWindow(); await t.expect(changeRequestLogger.count(() => true)).eql(1, 'Request fired when field became valid'); - subSection('Create a link to node'); - const linkTargetPage = 'Link target'; - await t - .doubleClick('.test-headline h1') - .switchToMainWindow() - .click(ReactSelector('EditorToolbar LinkButton')) - .typeText(ReactSelector('EditorToolbar LinkButton TextInput'), linkTargetPage) - .click(ReactSelector('EditorToolbar ContextDropDownContents NodeOption')) - .switchToIframe(contentIframeSelector) - .expect(Selector('.test-headline h1 a').withAttribute('href').exists).ok('Newly inserted link exists') - .switchToMainWindow(); + // 'This test is currently failing due to a bug in testcafe regarding the editable content selection' + subSection('Skipped: Create a link to node'); + // const linkTargetPage = 'Link target'; + // await t + // .doubleClick('.test-headline h1') + // .switchToMainWindow() + // .click(ReactSelector('EditorToolbar LinkButton')) + // .typeText(ReactSelector('EditorToolbar LinkButton TextInput'), linkTargetPage) + // .click(ReactSelector('EditorToolbar ContextDropDownContents NodeOption')) + // .switchToIframe(contentIframeSelector) + // .expect(Selector('.test-headline h1 a').withAttribute('href').exists).ok('Newly inserted link exists') + // .switchToMainWindow(); }); test('Inline CKEditor mode `paragraph: false` works as expected', async t => { @@ -219,12 +221,8 @@ test('Inline CKEditor mode `paragraph: false` works as expected', async t => { subSection('Insert text into the inline text and press enter'); await Page.waitForIframeLoading(t); + await typeTextInline(t, '.test-inline-headline:last-child [contenteditable="true"]', 'Foo Bar
Bun Buz'); await t - .switchToIframe(contentIframeSelector) - .typeText(Selector('.test-inline-headline [contenteditable="true"]'), 'Foo Bar') - .click(Selector('.test-inline-headline [contenteditable="true"]')) - .pressKey('enter') - .typeText(Selector('.test-inline-headline [contenteditable="true"]'), 'Bun Buz') .expect(Selector('.neos-contentcollection').withText('Foo Bar').exists).ok('Inserted text exists'); await t.switchToMainWindow(); @@ -246,7 +244,7 @@ test('Supports secondary inspector view for element editors', async t => { await t .click(imageEditor.findReact('IconButton').withProps('icon', 'camera')) .switchToIframe(Selector('[name="neos-media-selection-screen"]', {timeout: 2000})) - .click(Selector('.neos-thumbnail')); + .click(Selector('.asset').withText('neos_primary.png')); await t.switchToMainWindow(); diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/discarding.e2e.js b/Tests/IntegrationTests/Fixtures/1Dimension/discarding.e2e.js index ec99d3640c..965decd577 100644 --- a/Tests/IntegrationTests/Fixtures/1Dimension/discarding.e2e.js +++ b/Tests/IntegrationTests/Fixtures/1Dimension/discarding.e2e.js @@ -18,7 +18,7 @@ test('Discarding: create multiple nodes nested within each other and then discar await t .click(Selector('#neos-PageTree-AddNode')) .click(ReactSelector('InsertModeSelector').find('#into')) - .click(ReactSelector('NodeTypeItem')) + .click(ReactSelector('NodeTypeItem').find('button>span>span').withText('Page_Test')) .typeText(Selector('#neos-NodeCreationDialog-Body input'), pageTitleToCreate) .click(Selector('#neos-NodeCreationDialog-CreateNew')); await Page.waitForIframeLoading(); @@ -27,7 +27,7 @@ test('Discarding: create multiple nodes nested within each other and then discar await t .click(Selector('#neos-PageTree-AddNode')) .click(ReactSelector('InsertModeSelector').find('#into')) - .click(ReactSelector('NodeTypeItem')) + .click(ReactSelector('NodeTypeItem').find('button>span>span').withText('Page_Test')) .typeText(Selector('#neos-NodeCreationDialog-Body input'), pageTitleToCreate) .click(Selector('#neos-NodeCreationDialog-CreateNew')); await Page.waitForIframeLoading(); @@ -38,7 +38,7 @@ test('Discarding: create multiple nodes nested within each other and then discar .expect(ReactSelector('Provider').getReact(({props}) => { const reduxState = props.store.getState(); return reduxState.cr.nodes.documentNode; - })).eql('/sites/neos-test-site@user-admin;language=en_US', 'After discarding we are back to the main page'); + })).eql(JSON.stringify({contentRepositoryId:"onedimension",workspaceName:"admin-admington",dimensionSpacePoint:{"language":"en_US"},aggregateId:"f676459d-ca77-44bc-aeea-44114814c279"}), 'After discarding we are back to the main page'); }); test('Discarding: create a document node and then discard it', async t => { @@ -46,7 +46,7 @@ test('Discarding: create a document node and then discard it', async t => { subSection('Create a document node'); await t .click(Selector('#neos-PageTree-AddNode')) - .click(ReactSelector('NodeTypeItem')) + .click(ReactSelector('NodeTypeItem').find('button>span>span').withText('Page_Test')) .typeText(Selector('#neos-NodeCreationDialog-Body input'), pageTitleToCreate) .click(Selector('#neos-NodeCreationDialog-CreateNew')) .expect(Page.treeNode.withText(pageTitleToCreate).exists).ok('Node with the given title appeared in the tree') @@ -78,7 +78,7 @@ test('Discarding: delete a document node and then discard deletion', async t => await Page.goToPage(pageTitleToDelete); await t .switchToIframe('[name="neos-content-main"]') - .expect(Selector('.test-headline h1').withText(headlineOnDeletedPage).exists).ok('Navigated to the page and see the headline inline') + .expect(Selector('.neos-contentcollection').withText(headlineOnDeletedPage).exists).ok('Navigated to the page and see the headline inline') .switchToMainWindow(); subSection('Delete that page'); @@ -102,7 +102,7 @@ test('Discarding: create a content node and then discard it', async t => { .click(Selector('#neos-ContentTree-ToggleContentTree')) .click(Page.treeNode.withText('Content Collection (main)')) .click(Selector('#neos-ContentTree-AddNode')) - .click(ReactSelector('NodeTypeItem').find('button>span>span').withText('Headline')); + .click(ReactSelector('NodeTypeItem').find('button>span>span').withText('Headline_Test')); await Page.waitForIframeLoading(t); await t .switchToIframe('[name="neos-content-main"]') diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/inspector.e2e.js b/Tests/IntegrationTests/Fixtures/1Dimension/inspector.e2e.js index 74fd38a800..a641745eab 100644 --- a/Tests/IntegrationTests/Fixtures/1Dimension/inspector.e2e.js +++ b/Tests/IntegrationTests/Fixtures/1Dimension/inspector.e2e.js @@ -14,31 +14,32 @@ test('Can edit the page title via inspector', async t => { const InspectorUriPathSegmentProperty = Selector( '#__neos__editor__property---uriPathSegment' ); - await Page.waitForIframeLoading(t); - subSection('Rename home page via inspector'); + await Page.goToPage('Discarding') + + subSection('Rename Discarding page via inspector'); await t .expect(InspectorTitleProperty.value) - .eql('Home') + .eql('Discarding') .expect(InspectorUriPathSegmentProperty.value) - .eql('home') + .eql('discarding') .click(InspectorTitleProperty) .typeText(InspectorTitleProperty, '-привет!') .expect(InspectorTitleProperty.value) - .eql('Home-привет!') + .eql('Discarding-привет!') .wait(200) .click(Selector('#neos-UriPathSegmentEditor-sync')) .expect(InspectorUriPathSegmentProperty.value) - .eql('home-privet') + .eql('discarding-privet') .click(Selector('#neos-Inspector-Discard')) .expect(InspectorTitleProperty.value) - .eql('Home') + .eql('Discarding') .typeText(InspectorTitleProperty, '-1') .click(Selector('#neos-Inspector-Apply')) .expect(InspectorTitleProperty.value) - .eql('Home-1'); + .eql('Discarding-1'); await Page.waitForIframeLoading(t); - await t.expect(InspectorTitleProperty.value).eql('Home-1'); + await t.expect(InspectorTitleProperty.value).eql('Discarding-1'); subSection('Test unapplied changes dialog - resume'); await t @@ -51,14 +52,14 @@ test('Can edit the page title via inspector', async t => { .expect(Selector('#neos-UnappliedChangesDialog').exists) .notOk() .expect(InspectorTitleProperty.value) - .eql('Home-1-2'); + .eql('Discarding-1-2'); subSection('Test unapplied changes dialog - discard'); await t .click(Selector('#neos-Inspector'), {offsetX: -400}) // hack to click into the iframe even with overlaying changes div in dom .click(Selector('#neos-UnappliedChangesDialog-discard')) .expect(InspectorTitleProperty.value) - .eql('Home-1'); + .eql('Discarding-1'); subSection('Test unapplied changes dialog - apply'); await t @@ -66,7 +67,7 @@ test('Can edit the page title via inspector', async t => { .click(Selector('#neos-Inspector'), {offsetX: -400}) // hack to click into the iframe even with overlaying changes div in dom .click(Selector('#neos-UnappliedChangesDialog-apply')) .expect(InspectorTitleProperty.value) - .eql('Home-1-3') + .eql('Discarding-1-3') .click(Selector('#neos-Inspector'), {offsetX: -400}) // hack to click into the iframe even with overlaying changes div in dom .expect(Selector('#neos-UnappliedChangesDialog').exists) .notOk(); diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/inspectorValidation.e2e.js b/Tests/IntegrationTests/Fixtures/1Dimension/inspectorValidation.e2e.js index 419431c4b3..5b47fb7a59 100644 --- a/Tests/IntegrationTests/Fixtures/1Dimension/inspectorValidation.e2e.js +++ b/Tests/IntegrationTests/Fixtures/1Dimension/inspectorValidation.e2e.js @@ -31,7 +31,8 @@ test('Remove homepage title to get one error', async t => { .ok('The badge shows one validation error in Props'); }); -test('Remove homepage title and URI segment to get two errors', async t => { +test('Remove Page title and URI segment to get two errors', async t => { + await Page.goToPage('Discarding') subSection('Clean title and uri path segment field'); await t .typeText(InspectorTitleProperty, ' ', {replace: true}) diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/issue-3184.e2e.js b/Tests/IntegrationTests/Fixtures/1Dimension/issue-3184.e2e.js index 8e8d20e7b2..80d5c34028 100644 --- a/Tests/IntegrationTests/Fixtures/1Dimension/issue-3184.e2e.js +++ b/Tests/IntegrationTests/Fixtures/1Dimension/issue-3184.e2e.js @@ -145,6 +145,6 @@ test( await t.switchToMainWindow(); await t .expect(Selector('[role="treeitem"] [role="button"][class*="isFocused"]').textContent) - .eql('MultiC'); + .eql('MultiA'); } ); diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/nodeTreePresets.e2e.js b/Tests/IntegrationTests/Fixtures/1Dimension/nodeTreePresets.e2e.js index fc165ed3e6..9000497137 100644 --- a/Tests/IntegrationTests/Fixtures/1Dimension/nodeTreePresets.e2e.js +++ b/Tests/IntegrationTests/Fixtures/1Dimension/nodeTreePresets.e2e.js @@ -63,6 +63,8 @@ const SETTINGS_WITH_NODE_TREE_PRESETS = { } }; +fixture.skip`TODO Reimplement / Fix Node Tree Presets for 9.0 https://github.com/neos/neos-ui/issues/3702`; + test('Node tree preset "default" removes all blog related nodes and only loads nodes with depth <= 2', async (t) => { // // Assert that all documents with a depth > 2 are no longer visible in diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/richTextEditor.e2e.js b/Tests/IntegrationTests/Fixtures/1Dimension/richTextEditor.e2e.js index 7af31fa28c..a3073b342f 100644 --- a/Tests/IntegrationTests/Fixtures/1Dimension/richTextEditor.e2e.js +++ b/Tests/IntegrationTests/Fixtures/1Dimension/richTextEditor.e2e.js @@ -1,6 +1,6 @@ import {Selector} from 'testcafe'; import {ReactSelector} from 'testcafe-react-selectors'; -import {beforeEach, checkPropTypes} from './../../utils.js'; +import {beforeEach, checkPropTypes, typeTextInline} from './../../utils.js'; import {Page} from './../../pageModel'; /* global fixture:true */ @@ -14,11 +14,11 @@ test('Can crop an image', async t => { await Page.waitForIframeLoading(t); const rteInspectorEditor = await ReactSelector('InspectorEditorEnvelope').withProps('id', 'rte'); - const ckeContent = await Selector('.ck-content p'); + // const ckeContent = await Selector('.ck-content p'); await t .click(rteInspectorEditor.findReact('Button')); + await typeTextInline(t, '.ck-content p', testContent, '', false); await t - .typeText(ckeContent, testContent) .wait(400) .click(Selector('#neos-Inspector-Apply')); await Page.waitForIframeLoading(t); diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/syncing.e2e.js b/Tests/IntegrationTests/Fixtures/1Dimension/syncing.e2e.js new file mode 100644 index 0000000000..9d81a68027 --- /dev/null +++ b/Tests/IntegrationTests/Fixtures/1Dimension/syncing.e2e.js @@ -0,0 +1,426 @@ +import {Selector} from 'testcafe'; +import {ReactSelector, waitForReact} from 'testcafe-react-selectors'; +import { + checkPropTypes, + adminUserOnOneDimensionTestSite, + editorUserOnOneDimensionTestSite, + typeTextInline, + subSection +} from './../../utils.js'; +import { + Page, + PublishDropDown +} from './../../pageModel'; + +/* global fixture:true */ + +fixture`Syncing` + .afterEach(() => checkPropTypes()); + +fixture.skip`TODO After soft removals conflict resolution never imposes a conflict, maybe remove conflict resolution in the Neos ui???`; + +test('Syncing: Create a conflict state between two editors and choose "Discard all" as a resolution strategy during rebase', async t => { + await prepareContentElementConflictBetweenAdminAndEditor(t); + await chooseDiscardAllAsResolutionStrategy(t); + await performResolutionStrategy(t); + await finishDiscard(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #1'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #2'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #3'); +}); + +test('Syncing: Create a conflict state between two editors and choose "Drop conflicting changes" as a resolution strategy during rebase', async t => { + await prepareContentElementConflictBetweenAdminAndEditor(t); + await chooseDropConflictingChangesAsResolutionStrategy(t); + await performResolutionStrategy(t); + await finishSynchronization(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #1'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #2'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #3'); +}); + +test('Syncing: Create a conflict state between two editors, start and cancel resolution, then restart and choose "Drop conflicting changes" as a resolution strategy during rebase', async t => { + await prepareContentElementConflictBetweenAdminAndEditor(t); + await cancelResolutionDuringStrategyChoice(t); + await startSynchronization(t); + await assertThatConflictResolutionHasStarted(t); + await chooseDropConflictingChangesAsResolutionStrategy(t); + await performResolutionStrategy(t); + await finishSynchronization(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #1'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #2'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #3'); +}); + +test('Syncing: Create a conflict state between two editors and switch between "Drop conflicting changes" and "Discard all" as a resolution strategy during rebase', async t => { + await prepareContentElementConflictBetweenAdminAndEditor(t); + + // switch back and forth + await chooseDiscardAllAsResolutionStrategy(t); + await cancelResolutionStrategy(t); + await chooseDropConflictingChangesAsResolutionStrategy(t); + await cancelResolutionStrategy(t); + await chooseDiscardAllAsResolutionStrategy(t); + + await performResolutionStrategy(t); + await finishDiscard(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #1'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #2'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #3'); +}); + +test('Publish + Syncing: Create a conflict state between two editors, then try to publish the site and choose "Drop conflicting changes" as a resolution strategy during automatic rebase', async t => { + await prepareDocumentConflictBetweenAdminAndEditor(t); + await startPublishAll(t); + await assertThatConflictResolutionHasStarted(t); + await chooseDropConflictingChangesAsResolutionStrategy(t); + await performResolutionStrategy(t); + await finishPublish(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'This page will be deleted during sync'); +}); + +test('Publish + Syncing: Create a conflict state between two editors, then try to publish the document and choose "Drop conflicting changes" as a resolution strategy during automatic rebase', async t => { + await prepareDocumentConflictBetweenAdminAndEditor(t); + await publishDocument(t); + await assertThatConflictResolutionHasStarted(t); + await chooseDropConflictingChangesAsResolutionStrategy(t); + await performResolutionStrategy(t); + await finishPublish(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'This page will be deleted during sync'); +}); + +test('Publish + Syncing: Create a conflict state between two editors, then try to publish the site and choose "Discard all" as a resolution strategy', async t => { + await prepareDocumentConflictBetweenAdminAndEditor(t); + await startPublishAll(t); + await assertThatConflictResolutionHasStarted(t); + await chooseDiscardAllAsResolutionStrategy(t); + await performResolutionStrategy(t); + await finishDiscard(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'This page will be deleted during sync'); +}); + +async function prepareContentElementConflictBetweenAdminAndEditor(t) { + await loginAsEditorOnceToInitializeAContentStreamForTheirWorkspaceIfNeeded(t); + + // + // Login as "admin" + // + await as(t, adminUserOnOneDimensionTestSite, async () => { + await PublishDropDown.discardAll(); + + // + // Create a hierarchy of document nodes + // + await createDocumentNode(t, 'Home', 'into', 'Sync Demo #1'); + await createDocumentNode(t, 'Sync Demo #1', 'into', 'Sync Demo #2'); + await createDocumentNode(t, 'Sync Demo #2', 'into', 'Sync Demo #3'); + + // + // Publish everything + // + await PublishDropDown.publishAll(); + }); + + // + // Login as "editor" + // + await as(t, editorUserOnOneDimensionTestSite, async () => { + // + // Sync changes from "admin" + // + await t.wait(2000); + await t.eval(() => location.reload(true)); + await waitForReact(30000); + await Page.waitForIframeLoading(); + await startSynchronization(t); + await t.wait(1000); + + // + // Assert that all 3 documents are now visible in the document tree + // + await t.expect(Page.treeNode.withExactText('Sync Demo #1').exists) + .ok('[🗋 Sync Demo #1] cannot be found in the document tree of user "editor".'); + await t.expect(Page.treeNode.withExactText('Sync Demo #2').exists) + .ok('[🗋 Sync Demo #2] cannot be found in the document tree of user "editor".'); + await t.expect(Page.treeNode.withExactText('Sync Demo #3').exists) + .ok('[🗋 Sync Demo #3] cannot be found in the document tree of user "editor".'); + }); + + // + // Login as "admin" again + // + await as(t, adminUserOnOneDimensionTestSite, async () => { + // + // Create a headline node in [🗋 Sync Demo #3] + // + subSection('Create a headline node in the document'); + await Page.goToPage('Sync Demo #3'); + await t + .switchToMainWindow(); + + await openContentTree(t); + + await t + .wait(1000) + .click(Page.treeNode.withText('Content Collection (main)')) + .click(Page.treeNode.withText('Content Collection (main)')) + .wait(1000) + .click(Selector('#neos-ContentTree-AddNode')) + .click(Selector('button#into')) + .click(ReactSelector('NodeTypeItem').find('button>span>span').withText('Headline_Test')) + await Page.waitForIframeLoading(t); + + subSection('Type something inside of it'); + await typeTextInline(t, '.test-headline:last-child [contenteditable="true"]', 'Hello from Page "Sync Demo #3"!'); + await t + .expect(Selector('.neos-contentcollection').withText('Hello from Page "Sync Demo #3"!').exists).ok('Typed headline text exists') + .switchToMainWindow(); + }); + + // + // Login as "editor" again + // + await as(t, editorUserOnOneDimensionTestSite, async () => { + // + // Delete page [🗋 Sync Demo #1] + // + await deleteDocumentNode(t, 'Sync Demo #1'); + + // + // Publish everything + // + await PublishDropDown.publishAll(); + }); + + // + // Login as "admin" again and visit [🗋 Sync Demo #3] + // + await as(t, adminUserOnOneDimensionTestSite, async () => { + await Page.goToPage('Sync Demo #3'); + + // + // Sync changes from "editor" + // + await startSynchronization(t); + await assertThatConflictResolutionHasStarted(t); + }); +} + +async function prepareDocumentConflictBetweenAdminAndEditor(t) { + await loginAsEditorOnceToInitializeAContentStreamForTheirWorkspaceIfNeeded(t); + + await as(t, adminUserOnOneDimensionTestSite, async () => { + await PublishDropDown.discardAll(); + await createDocumentNode(t, 'Home', 'into', 'This page will be deleted during sync'); + await PublishDropDown.publishAll(); + + subSection('Create a headline node in the document'); + await Page.waitForIframeLoading(t); + + await t + .switchToMainWindow(); + + await openContentTree(t); + + await t + .wait(1000) + .click(Page.treeNode.withText('Content Collection (main)')) + .click(Page.treeNode.withText('Content Collection (main)')) + .wait(1000) + .click(Selector('#neos-ContentTree-AddNode')) + .click(Selector('button#into')) + .click(ReactSelector('NodeTypeItem').find('button>span>span').withText('Headline_Test')); + await Page.waitForIframeLoading(t); + + subSection('Type something inside of it'); + await typeTextInline(t, '.test-headline:last-child', 'This change will not be published.'); + await t + .wait(2000) + .switchToMainWindow(); + }); + + await as(t, editorUserOnOneDimensionTestSite, async () => { + await t.wait(2000); + await t.eval(() => location.reload(true)); + await waitForReact(30000); + await Page.waitForIframeLoading(); + await startSynchronization(t); + await t.wait(1000); + await finishSynchronization(t); + + await t.expect(Page.treeNode.withExactText('This page will be deleted during sync').exists) + .ok('[🗋 This page will be deleted during sync] cannot be found in the document tree of user "editor".'); + + await deleteDocumentNode(t, 'This page will be deleted during sync'); + await PublishDropDown.publishAll(); + }); + + await switchToRole(t, adminUserOnOneDimensionTestSite); + await Page.goToPage('This page will be deleted during sync'); +} + +let editHasLoggedInAtLeastOnce = false; +async function loginAsEditorOnceToInitializeAContentStreamForTheirWorkspaceIfNeeded(t) { + if (editHasLoggedInAtLeastOnce) { + return; + } + + await as(t, editorUserOnOneDimensionTestSite, async () => { + await Page.waitForIframeLoading(); + await t.wait(2000); + editHasLoggedInAtLeastOnce = true; + }); +} + +async function as(t, role, asyncCallback) { + await switchToRole(t, role); + await asyncCallback(); +} + +async function switchToRole(t, role) { + // We need to add a time buffer here, otherwise `t.useRole` might interrupt + // some long-running background process, errororing like this: + // > Error: NetworkError when attempting to fetch resource. + await t.wait(2000); + await t.useRole(role); + await waitForReact(30000); + await Page.goToPage('Home'); +} + +async function createDocumentNode(t, referencePageTitle, insertMode, pageTitleToCreate) { + await Page.goToPage(referencePageTitle); + await t + .click(Selector('#neos-PageTree-AddNode')) + .click(ReactSelector('InsertModeSelector').find('#' + insertMode)) + .click(ReactSelector('NodeTypeItem').find('button>span>span').withText('Page_Test')) + .typeText(Selector('#neos-NodeCreationDialog-Body input'), pageTitleToCreate) + .click(Selector('#neos-NodeCreationDialog-CreateNew')); + await Page.waitForIframeLoading(); +} + +async function deleteDocumentNode(t, pageTitleToDelete) { + await Page.goToPage(pageTitleToDelete); + await t.click(Selector('#neos-PageTree-DeleteSelectedNode')); + await t.click(Selector('#neos-DeleteNodeModal-Confirm')); + await Page.waitForIframeLoading(); +} + +async function startPublishAll(t) { + await t.click(PublishDropDown.publishDropdown) + await t.click(PublishDropDown.publishDropdownPublishAll); + await t.click(Selector('#neos-PublishDialog-Confirm')); +} + +async function publishDocument(t) { + await t.click(Selector('#neos-PublishDropDown-Publish')) +} + +async function finishPublish(t) { + await assertThatPublishingHasFinishedWithoutError(t); + await t.click(Selector('#neos-PublishDialog-Acknowledge')); + await t.wait(2000); +} + +async function finishDiscard(t) { + await t.click(Selector('#neos-DiscardDialog-Acknowledge')); + await t.wait(2000); +} + +async function startSynchronization(t) { + await t.click(Selector('#neos-workspace-rebase')); + await t.click(Selector('#neos-SyncWorkspace-Confirm')); +} + +async function cancelResolutionDuringStrategyChoice(t) { + await t.click(Selector('#neos-SelectResolutionStrategy-Cancel')); +} + +async function chooseDiscardAllAsResolutionStrategy(t) { + await t.click(Selector('#neos-SelectResolutionStrategy-SelectBox')); + await t.click(Selector('[role="button"]').withText('Discard workspace "admin-admington"')); + await t.click(Selector('#neos-SelectResolutionStrategy-Accept')); +} + +async function chooseDropConflictingChangesAsResolutionStrategy(t) { + await t.click(Selector('#neos-SelectResolutionStrategy-SelectBox')); + await t.click(Selector('[role="button"]').withText('Drop conflicting changes')); + await t.click(Selector('#neos-SelectResolutionStrategy-Accept')); +} + +async function performResolutionStrategy(t) { + await t.click(Selector('#neos-ResolutionStrategyConfirmation-Confirm')); +} + +async function cancelResolutionStrategy(t) { + await t.click(Selector('#neos-ResolutionStrategyConfirmation-Cancel')); +} + +async function finishSynchronization(t) { + await assertThatSynchronizationHasFinishedWithoutError(t); + await t.click(Selector('#neos-SyncWorkspace-Acknowledge')); +} + +async function assertThatConflictResolutionHasStarted(t) { + await t.expect(Selector('#neos-SelectResolutionStrategy-SelectBox').exists) + .ok('Select box for resolution strategy slection is not available', { + timeout: 30000 + }); +} + +async function assertThatSynchronizationHasFinishedWithoutError(t) { + await t.expect(Selector('#neos-SyncWorkspace-Acknowledge').exists) + .ok('Acknowledge button for "Sync Workspace" is not available.', { + timeout: 30000 + }); + await t.expect(Selector('#neos-SyncWorkspace-Retry').exists) + .notOk('An error occurred during "Sync Workspace".', { + timeout: 30000 + }); +} + +async function assertThatPublishingHasFinishedWithoutError(t) { + await t.expect(Selector('#neos-PublishDialog-Acknowledge').exists) + .ok('Acknowledge button for "Publishing" is not available.', { + timeout: 30000 + }); + await t.expect(Selector('#neos-PublishDialog-Retry').exists) + .notOk('An error occurred during "Publishing".', { + timeout: 30000 + }); +} + +async function assertThatWeAreOnPage(t, pageTitle) { + await t + .expect(Selector('[role="treeitem"] [role="button"][class*="isFocused"]').textContent) + .eql(pageTitle); +} + +async function assertThatWeCannotSeePageInTree(t, pageTitle) { + await t.expect(Page.treeNode.withExactText(pageTitle).exists) + .notOk(`[🗋 ${pageTitle}] can still be found in the document tree of user "admin".`); +} + +async function openContentTree(t) { + const contentTree = ReactSelector('ToggleContentTree'); + const isPanelOpen = await contentTree.getReact(({props}) => props.isPanelOpen); + + if (!isPanelOpen) { + await t + .pressKey('t') + .pressKey('c'); + } +} diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/treeMultiselect.e2e.js b/Tests/IntegrationTests/Fixtures/1Dimension/treeMultiselect.e2e.js index 2714737440..a06f967e2e 100644 --- a/Tests/IntegrationTests/Fixtures/1Dimension/treeMultiselect.e2e.js +++ b/Tests/IntegrationTests/Fixtures/1Dimension/treeMultiselect.e2e.js @@ -26,7 +26,7 @@ test('Move multiple nodes via toolbar', async t => { .expect(ReactSelector('Provider').getReact(({props}) => { const reduxState = props.store.getState(); return reduxState.cr.nodes.documentNode; - })).eql('/sites/neos-test-site/node-knm2pltb5454z/node-18qsaeidy6765/node-e8tw6sparbtp3@user-admin;language=en_US', 'Node B\'s context path changed'); + })).eql(JSON.stringify({contentRepositoryId:"onedimension",workspaceName:"admin-admington",dimensionSpacePoint:{"language":"en_US"},aggregateId:"5b0d6ac0-40ab-47e8-b79e-39de6c0700df"}), 'Node B\'s node address changed'); await t.click(Page.getTreeNodeButton('Home')) }); @@ -43,7 +43,7 @@ test('Move multiple nodes via DND, CMD-click', async t => { .expect(ReactSelector('Provider').getReact(({props}) => { const reduxState = props.store.getState(); return reduxState.cr.nodes.documentNode; - })).eql('/sites/neos-test-site/node-knm2pltb5454z/node-18qsaeidy6765/node-e8tw6sparbtp3@user-admin;language=en_US', 'Node B\'s context path changed'); + })).eql(JSON.stringify({contentRepositoryId:"onedimension",workspaceName:"admin-admington",dimensionSpacePoint:{"language":"en_US"},aggregateId:"5b0d6ac0-40ab-47e8-b79e-39de6c0700df"}), 'Node B\'s node address changed'); await t.click(Page.getTreeNodeButton('Home')) }); @@ -60,6 +60,6 @@ test('Move multiple nodes via DND, SHIFT-click', async t => { .expect(ReactSelector('Provider').getReact(({props}) => { const reduxState = props.store.getState(); return reduxState.cr.nodes.documentNode; - })).eql('/sites/neos-test-site/node-knm2pltb5454z/node-18qsaeidy6765/node-oml0cxaompt29@user-admin;language=en_US', 'Node C\'s context path changed'); + })).eql(JSON.stringify({contentRepositoryId:"onedimension",workspaceName:"admin-admington",dimensionSpacePoint:{"language":"en_US"},aggregateId:"84eb0340-ba34-4fdb-98b1-da503f967121"}), 'Node C\'s node address changed'); await t.click(Page.getTreeNodeButton('Home')) }); diff --git a/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Configuration/Settings.yaml b/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Configuration/Settings.yaml deleted file mode 100644 index 4881edaed5..0000000000 --- a/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Configuration/Settings.yaml +++ /dev/null @@ -1,103 +0,0 @@ -Neos: - ContentRepository: - contentDimensions: - 'country': - default: 'deu' - defaultPreset: 'deu' - label: 'Country' - icon: 'icon-globe' - presets: - 'deu': - label: 'Germany' - values: ['deu'] - uriSegment: 'deu' - 'aut': - label: 'Austria' - values: ['aut', 'deu'] - uriSegment: 'aut' - 'lux': - label: 'Luxembourg' - values: ['lux', 'deu'] - uriSegment: 'lux' - 'dnk': - label: 'Denmark' - values: ['dnk'] - uriSegment: 'dnk' - 'language': - label: 'Language' - icon: icon-language - default: en_US - defaultPreset: en_US - presets: - en_US: - label: 'English (US)' - values: - - en_US - uriSegment: en - constraints: - country: - '*': false - 'deu': true - 'aut': true - en_UK: - label: 'English (UK)' - values: - - en_UK - - en_US - uriSegment: uk - constraints: - country: - '*': false - 'deu': true - 'aut': true - de: - label: German - values: - - de - uriSegment: de - constraints: - country: - '*': false - 'deu': true - 'aut': true - 'lux': true - fr: - label: French - values: - - fr - uriSegment: fr - constraints: - country: - '*': false - 'deu': true - 'aut': true - nl: - label: Dutch - values: - - nl - - de - uriSegment: nl - constraints: - country: - '*': false - 'deu': true - 'aut': true - da: - label: Danish - values: - - da - uriSegment: da - constraints: - country: - '*': false - 'dnk': true - lv: - label: Latvian - values: - - lv - uriSegment: lv - constraints: - country: - '*': false - 'deu': true - 'aut': true diff --git a/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Resources/Private/Content/Resources/6791d1beca5fa4ab78f1b2b1a44d43692f99b5dc b/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Resources/Private/Content/Resources/6791d1beca5fa4ab78f1b2b1a44d43692f99b5dc deleted file mode 100644 index 49b2fb4403..0000000000 Binary files a/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Resources/Private/Content/Resources/6791d1beca5fa4ab78f1b2b1a44d43692f99b5dc and /dev/null differ diff --git a/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Resources/Private/Content/Sites.xml b/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Resources/Private/Content/Sites.xml deleted file mode 100644 index 36d80113ab..0000000000 --- a/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Resources/Private/Content/Sites.xml +++ /dev/null @@ -1,315 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/IntegrationTests/Fixtures/2Dimension/switchingDimensions.e2e.js b/Tests/IntegrationTests/Fixtures/2Dimension/switchingDimensions.e2e.js index 8c3e6abcb3..bd9b4e4c26 100644 --- a/Tests/IntegrationTests/Fixtures/2Dimension/switchingDimensions.e2e.js +++ b/Tests/IntegrationTests/Fixtures/2Dimension/switchingDimensions.e2e.js @@ -1,14 +1,19 @@ -import {beforeEach, subSection, checkPropTypes} from './../../utils'; +import {subSection, checkPropTypes, getUrl, adminUserOnTwoDimensionsTestSite} from './../../utils'; import {Selector} from 'testcafe'; -import {ReactSelector} from 'testcafe-react-selectors'; +import {ReactSelector, waitForReact} from 'testcafe-react-selectors'; import { - Page + Page, PublishDropDown } from './../../pageModel'; /* global fixture:true */ fixture`Switching dimensions` - .beforeEach(beforeEach) + .beforeEach(async t => { + await t.useRole(adminUserOnTwoDimensionsTestSite); + await waitForReact(30000); + await PublishDropDown.discardAll(); + await Page.goToPage('Home'); + }) .afterEach(() => checkPropTypes()); test('Switching dimensions', async t => { diff --git a/Tests/IntegrationTests/Fixtures/2Dimension/switchingSites.e2e.js b/Tests/IntegrationTests/Fixtures/2Dimension/switchingSites.e2e.js new file mode 100644 index 0000000000..2f9418b416 --- /dev/null +++ b/Tests/IntegrationTests/Fixtures/2Dimension/switchingSites.e2e.js @@ -0,0 +1,59 @@ +import {subSection, checkPropTypes, getUrl, adminUserOnOneDimensionTestSite} from './../../utils'; +import {Selector} from 'testcafe'; +import {waitForReact} from 'testcafe-react-selectors'; +import {Page} from './../../pageModel'; + +/* global fixture:true */ + +fixture`Switching sites` + .afterEach(() => checkPropTypes()); + +test('Switching from Neos.Test.OneDimension to Neos.Test.TwoDimensions and back', async t => { + subSection('Log in @ Neos.Test.OneDimension'); + await t.navigateTo('http://onedimension.localhost:8081/neos'); + await t + .typeText('#username', 'admin') + .typeText('#password', 'admin') + .click('button.neos-login-btn'); + await waitForReact(30000); + await Page.goToPage('Home'); + + subSection('Switch to Neos.Test.TwoDimensions via main menu'); + await t.click(Selector('#neos-MenuToggler')); + await t.click(Selector('[href*="twodimensions"] button')); + + await t.expect(getUrl()).contains('twodimensions.localhost', 'Switch to Neos.Test.TwoDimensions was successful'); + + subSection('Switch back to Neos.Test.OneDimension via main menu'); + await waitForReact(30000); + await Page.goToPage('Home'); + await t.click(Selector('#neos-MenuToggler')); + await t.click(Selector('[href*="onedimension"] button')); + + await t.expect(getUrl()).contains('onedimension.localhost', 'Switch to Neos.Test.OneDimension was successful'); +}); + +test('Switching from Neos.Test.TwoDimensions to Neos.Test.OneDimension and back', async t => { + subSection('Log in @ Neos.Test.TwoDimensions'); + await t.navigateTo('http://twodimensions.localhost:8081/neos'); + await t + .typeText('#username', 'admin') + .typeText('#password', 'admin') + .click('button.neos-login-btn'); + await waitForReact(30000); + await Page.goToPage('Home'); + + subSection('Switch to Neos.Test.OneDimension via main menu'); + await t.click(Selector('#neos-MenuToggler')); + await t.click(Selector('[href*="onedimension"] button')); + + await t.expect(getUrl()).contains('onedimension.localhost', 'Switch to Neos.Test.OneDimension was successful'); + + subSection('Switch back to Neos.Test.TwoDimensions via main menu'); + await waitForReact(30000); + await Page.goToPage('Home'); + await t.click(Selector('#neos-MenuToggler')); + await t.click(Selector('[href*="twodimensions"] button')); + + await t.expect(getUrl()).contains('twodimensions.localhost', 'Switch to Neos.Test.TwoDimensions was successful'); +}); diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/RemoveAdditionalSettings/Controller/RemoveAdditionalSettingsController.php b/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/RemoveAdditionalSettings/Controller/RemoveAdditionalSettingsController.php deleted file mode 100644 index badea48a01..0000000000 --- a/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/RemoveAdditionalSettings/Controller/RemoveAdditionalSettingsController.php +++ /dev/null @@ -1,72 +0,0 @@ -setDispatched(true); - $response->setContentType('application/json'); - - try { - $command = RemoveAdditionalSettingsCommand::fromArray($request->getArguments()); - $this->commandHandler->handle($command); - - $response->setStatusCode(200); - $response->setContent( - json_encode( - ['success' => true], - JSON_THROW_ON_ERROR - ) - ); - } catch (\InvalidArgumentException $e) { - $response->setStatusCode(400); - $response->setContent( - json_encode( - ['error' => [ - 'type' => $e::class, - 'code' => $e->getCode(), - 'message' => $e->getMessage(), - ]], - JSON_THROW_ON_ERROR - ) - ); - } catch (\Exception $e) { - $response->setStatusCode(500); - $response->setContent( - json_encode( - ['error' => [ - 'type' => $e::class, - 'code' => $e->getCode(), - 'message' => $e->getMessage(), - ]], - JSON_THROW_ON_ERROR - ) - ); - } - } -} diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/WriteAdditionalSettings/Controller/WriteAdditionalSettingsController.php b/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/WriteAdditionalSettings/Controller/WriteAdditionalSettingsController.php deleted file mode 100644 index cfbed1e39a..0000000000 --- a/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/WriteAdditionalSettings/Controller/WriteAdditionalSettingsController.php +++ /dev/null @@ -1,72 +0,0 @@ -setDispatched(true); - $response->setContentType('application/json'); - - try { - $command = WriteAdditionalSettingsCommand::fromArray($request->getArguments()); - $this->commandHandler->handle($command); - - $response->setStatusCode(200); - $response->setContent( - json_encode( - ['success' => true], - JSON_THROW_ON_ERROR - ) - ); - } catch (\InvalidArgumentException $e) { - $response->setStatusCode(400); - $response->setContent( - json_encode( - ['error' => [ - 'type' => $e::class, - 'code' => $e->getCode(), - 'message' => $e->getMessage(), - ]], - JSON_THROW_ON_ERROR - ) - ); - } catch (\Exception $e) { - $response->setStatusCode(500); - $response->setContent( - json_encode( - ['error' => [ - 'type' => $e::class, - 'code' => $e->getCode(), - 'message' => $e->getMessage(), - ]], - JSON_THROW_ON_ERROR - ) - ); - } - } -} diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/NodeCreationHandler/ImagePropertyNodeCreationHandler.php b/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/NodeCreationHandler/ImagePropertyNodeCreationHandler.php deleted file mode 100644 index b7d724a386..0000000000 --- a/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/NodeCreationHandler/ImagePropertyNodeCreationHandler.php +++ /dev/null @@ -1,47 +0,0 @@ -propertyMapper->buildPropertyMappingConfiguration(); - $propertyMappingConfiguration->forProperty('*')->allowAllProperties(); - $propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED, true); - - if (isset($data['image'])) { - $image = $this->propertyMapper->convert($data['image'], ImageInterface::class, $propertyMappingConfiguration); - $node->setProperty('image', $image); - } - } -} diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/NodeTypes/Document/PageWithImage.yaml b/Tests/IntegrationTests/SharedNodeTypesPackage/NodeTypes/Document/PageWithImage.yaml deleted file mode 100644 index 5fae514863..0000000000 --- a/Tests/IntegrationTests/SharedNodeTypesPackage/NodeTypes/Document/PageWithImage.yaml +++ /dev/null @@ -1,72 +0,0 @@ -'Neos.TestNodeTypes:Document.PageWithImage': - superTypes: - 'Neos.Neos:Document': true - options: - nodeCreationHandlers: - image: - nodeCreationHandler: 'Neos\TestNodeTypes\NodeCreationHandler\ImagePropertyNodeCreationHandler' - ui: - label: PageWithImage_Test - icon: icon-file-o - position: 100 - creationDialog: - elements: - image: - type: Neos\Media\Domain\Model\ImageInterface - ui: - label: Image - editor: Neos.Neos/Inspector/Editors/ImageEditor - editorOptions: - fileUploadLabel: Neos.Neos:Main:choose - maximumFileSize: - features: - crop: true - upload: true - mediaBrowser: true - resize: false - crop: - aspectRatio: - options: - square: - width: 1 - height: 1 - label: Square - fourFive: - width: 4 - height: 5 - fiveSeven: - width: 5 - height: 7 - twoThree: - width: 2 - height: 3 - fourThree: - width: 4 - height: 3 - sixteenNine: - width: 16 - height: 9 - enableOriginal: true - allowCustom: true - locked: - width: 1 - height: 1 - childNodes: - main: - type: 'Neos.Neos:ContentCollection' - properties: - image: - type: Neos\Media\Domain\Model\ImageInterface - ui: - label: 'Image' - reloadIfChanged: true - inspector: - group: 'document' - editorOptions: - features: - crop: true - crop: - aspectRatio: - locked: - width: 1 - height: 1 diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Root.fusion b/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Root.fusion deleted file mode 100644 index 9c5165e776..0000000000 --- a/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Root.fusion +++ /dev/null @@ -1,4 +0,0 @@ -include: **/*.fusion -page = Neos.Fusion:Renderer { - type = ${documentNode.nodeType.name} -} diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/composer.json b/Tests/IntegrationTests/SharedNodeTypesPackage/composer.json deleted file mode 100644 index e0af4bd7c2..0000000000 --- a/Tests/IntegrationTests/SharedNodeTypesPackage/composer.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "neos/test-nodetypes", - "description": "Some dummy nodetypes", - "type": "neos-package", - "require": { - "neos/neos": "*", - "neos/neos-ui": "*" - }, - "extra": { - "neos": { - "package-key": "Neos.TestNodeTypes" - } - }, - "autoload": { - "psr-4": { - "Neos\\TestNodeTypes\\": "Classes" - } - } -} diff --git a/Tests/IntegrationTests/TestDistribution/Configuration/Settings.yaml b/Tests/IntegrationTests/TestDistribution/Configuration/Settings.yaml index 3e4d93c451..34e45a207f 100644 --- a/Tests/IntegrationTests/TestDistribution/Configuration/Settings.yaml +++ b/Tests/IntegrationTests/TestDistribution/Configuration/Settings.yaml @@ -7,7 +7,7 @@ Neos: driver: pdo_mysql dbname: neos user: root - host: 127.0.0.1 + host: '%env(string):DB_HOST%' password: not_a_real_password reflection: ignoredTags: diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Configuration/Settings.yaml b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Configuration/Settings.yaml new file mode 100644 index 0000000000..15fd38c074 --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Configuration/Settings.yaml @@ -0,0 +1,46 @@ +Neos: + ContentRepositoryRegistry: + contentRepositories: + onedimension: + preset: default + contentDimensions: + language: + label: 'Language' + values: + 'en_US': + label: English (US) + group: NON-EU + specializations: + 'en_UK': + group: NON-EU + label: English (UK) + 'de': + label: German + group: EU + specializations: + 'nl': + group: EU + label: Dutch + 'fr': + group: EU + label: French + 'da': + group: EU + label: Danish + 'lv': + group: EU + label: Latvian + Neos: + sites: + 'neos-test-onedimension': + contentRepository: 'onedimension' + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\AutoUriPathResolverFactory + + userInterface: + navigateComponent: + nodeTree: + # must be at least 3, so when moving document nodes into each other + # they will still be shown and we can assert this + loadingDepth: 3 diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/Assets/ee3d239e-48b0-4f99-90be-054301b91792.json b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/Assets/ee3d239e-48b0-4f99-90be-054301b91792.json new file mode 100644 index 0000000000..e5d089b54b --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/Assets/ee3d239e-48b0-4f99-90be-054301b91792.json @@ -0,0 +1,14 @@ +{ + "identifier": "ee3d239e-48b0-4f99-90be-054301b91792", + "type": "IMAGE", + "title": "", + "copyrightNotice": "", + "caption": "", + "assetSourceIdentifier": "neos", + "resource": { + "filename": "neos_primary.png", + "collectionName": "persistent", + "mediaType": "image\/png", + "sha1": "aac28f51e5ca842e2646e88e7d242ac3c27e1f25" + } +} \ No newline at end of file diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/ImageVariants/50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1.json b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/ImageVariants/50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1.json new file mode 100644 index 0000000000..af47865fee --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/ImageVariants/50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1.json @@ -0,0 +1,20 @@ +{ + "identifier": "50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1", + "originalAssetIdentifier": "ee3d239e-48b0-4f99-90be-054301b91792", + "name": "", + "width": 126, + "height": 126, + "presetIdentifier": null, + "presetVariantName": null, + "imageAdjustments": [ + { + "type": "CROP_IMAGE", + "properties": { + "x": 328, + "y": 0, + "width": 126, + "height": 126 + } + } + ] +} \ No newline at end of file diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/Resources/Private/Content/Resources/aac28f51e5ca842e2646e88e7d242ac3c27e1f25 b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/Resources/aac28f51e5ca842e2646e88e7d242ac3c27e1f25 similarity index 100% rename from Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/Resources/Private/Content/Resources/aac28f51e5ca842e2646e88e7d242ac3c27e1f25 rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/Resources/aac28f51e5ca842e2646e88e7d242ac3c27e1f25 diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/events.jsonl b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/events.jsonl new file mode 100644 index 0000000000..cad742aaf6 --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/events.jsonl @@ -0,0 +1,58 @@ +{"identifier":"152f7ffe-b2a5-4237-beab-c4db9fa15587","type":"RootNodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"2fdbb3ea-1270-49bc-8f8b-cbfc563ef9a6","nodeTypeName":"Neos.Neos:Sites","coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"},{"language":"de"},{"language":"nl"},{"language":"fr"},{"language":"da"},{"language":"lv"}],"nodeAggregateClassification":"root"},"metadata":[]} +{"identifier":"c977a059-ba63-4132-a190-baa6ced4f614","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","nodeTypeName":"Neos.TestNodeTypes:Document.HomePage","originDimensionSpacePoint":{"language":"fr"},"coveredDimensionSpacePoints":[{"language":"fr"}],"parentNodeAggregateId":"2fdbb3ea-1270-49bc-8f8b-cbfc563ef9a6","nodeName":"neos-test-onedimension","initialPropertyValues":{"title":{"value":"Accueil","type":"string"},"uriPathSegment":{"value":"home","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"7e1e6a07-b80f-4cad-8fe6-519d34b86cad","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","sourceOrigin":{"language":"fr"},"peerOrigin":{"language":"en_UK"},"peerCoverage":[{"language":"en_UK"}]},"metadata":[]} +{"identifier":"37c01008-80f0-4252-8ff5-d44154cec8e7","type":"NodePropertiesWereSet","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","originDimensionSpacePoint":{"language":"en_UK"},"affectedDimensionSpacePoints":[{"language":"en_UK"}],"propertyValues":{"title":{"value":"Home","type":"string"},"uriPathSegment":{"value":"home","type":"string"},"image":{"value":{"__flow_object_type":"Neos\\Media\\Domain\\Model\\ImageVariant","__identifier":"50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1"},"type":"Neos\\Media\\Domain\\Model\\ImageInterface"}},"propertiesToUnset":[]},"metadata":[]} +{"identifier":"c30068ba-258b-4393-b0de-aaf37d43fe69","type":"NodeGeneralizationVariantWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","sourceOrigin":{"language":"en_UK"},"generalizationOrigin":{"language":"en_US"},"generalizationCoverage":[{"language":"en_US"}]},"metadata":[]} +{"identifier":"6a8125af-fcfa-45e3-8b23-bb99ae36e33e","type":"NodePropertiesWereSet","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","originDimensionSpacePoint":{"language":"en_US"},"affectedDimensionSpacePoints":[{"language":"en_US"}],"propertyValues":{"title":{"value":"Home","type":"string"},"uriPathSegment":{"value":"home","type":"string"},"image":{"value":{"__flow_object_type":"Neos\\Media\\Domain\\Model\\ImageVariant","__identifier":"50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1"},"type":"Neos\\Media\\Domain\\Model\\ImageInterface"}},"propertiesToUnset":[]},"metadata":[]} +{"identifier":"8d7b8db3-9f48-494c-b7c7-5e94a830e7be","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","sourceOrigin":{"language":"en_US"},"peerOrigin":{"language":"da"},"peerCoverage":[{"language":"da"}]},"metadata":[]} +{"identifier":"ca5803cb-c243-48f9-9670-bf7deef64622","type":"NodePropertiesWereSet","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","originDimensionSpacePoint":{"language":"da"},"affectedDimensionSpacePoints":[{"language":"da"}],"propertyValues":{"title":{"value":"Hjem","type":"string"},"uriPathSegment":{"value":"home","type":"string"}},"propertiesToUnset":[]},"metadata":[]} +{"identifier":"425419f2-c507-4f84-acac-bae008eb4c01","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","sourceOrigin":{"language":"da"},"peerOrigin":{"language":"lv"},"peerCoverage":[{"language":"lv"}]},"metadata":[]} +{"identifier":"51c5ceca-360f-45b4-b6fb-b6aa8ed8c2ff","type":"NodePropertiesWereSet","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","originDimensionSpacePoint":{"language":"lv"},"affectedDimensionSpacePoints":[{"language":"lv"}],"propertyValues":{"title":{"value":"M\u0101jas","type":"string"},"uriPathSegment":{"value":"home","type":"string"}},"propertiesToUnset":[]},"metadata":[]} +{"identifier":"76ebb9af-137f-4da1-8ba3-3db36815819f","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","sourceOrigin":{"language":"lv"},"peerOrigin":{"language":"nl"},"peerCoverage":[{"language":"nl"}]},"metadata":[]} +{"identifier":"91744471-1590-4b7d-8907-749bf5d80a2c","type":"NodePropertiesWereSet","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","originDimensionSpacePoint":{"language":"nl"},"affectedDimensionSpacePoints":[{"language":"nl"}],"propertyValues":{"title":{"value":"Huis","type":"string"},"uriPathSegment":{"value":"home","type":"string"}},"propertiesToUnset":[]},"metadata":[]} +{"identifier":"899ef4e2-d6ab-4794-a24c-0ef2152d2bd5","type":"NodeGeneralizationVariantWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","sourceOrigin":{"language":"nl"},"generalizationOrigin":{"language":"de"},"generalizationCoverage":[{"language":"de"}]},"metadata":[]} +{"identifier":"ed48a226-9ea5-4439-bd3c-db8491649512","type":"NodePropertiesWereSet","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","originDimensionSpacePoint":{"language":"de"},"affectedDimensionSpacePoints":[{"language":"de"}],"propertyValues":{"title":{"value":"Startseite","type":"string"},"uriPathSegment":{"value":"home","type":"string"}},"propertiesToUnset":[]},"metadata":[]} +{"identifier":"297537f3-8ef7-408c-bf86-aed3f36e3dc9","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"64cdaa71-bdb1-4a07-a661-bb1787b8d077","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","sourceOrigin":{"language":"en_US"},"peerOrigin":{"language":"de"},"peerCoverage":[{"language":"de"},{"language":"nl"}]},"metadata":[]} +{"identifier":"c0b3f4e5-c3ff-4430-a906-a1f564fe1fd8","type":"NodeSpecializationVariantWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","sourceOrigin":{"language":"en_US"},"specializationOrigin":{"language":"en_UK"},"specializationCoverage":[{"language":"en_UK"}]},"metadata":[]} +{"identifier":"4414f130-d8fc-422c-b304-708c8fec2777","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","sourceOrigin":{"language":"en_UK"},"peerOrigin":{"language":"da"},"peerCoverage":[{"language":"da"}]},"metadata":[]} +{"identifier":"3bd5280c-f131-4401-bcb0-3dd09ce3bef9","type":"NodeSpecializationVariantWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","sourceOrigin":{"language":"de"},"specializationOrigin":{"language":"nl"},"specializationCoverage":[{"language":"nl"}]},"metadata":[]} +{"identifier":"4ffc3cc4-f657-4273-be53-88f4f74d3c62","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","sourceOrigin":{"language":"nl"},"peerOrigin":{"language":"lv"},"peerCoverage":[{"language":"lv"}]},"metadata":[]} +{"identifier":"fec825fe-47ab-4db7-8dcb-608c8530e74c","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","sourceOrigin":{"language":"lv"},"peerOrigin":{"language":"fr"},"peerCoverage":[{"language":"fr"}]},"metadata":[]} +{"identifier":"74255fc9-675a-4a7d-8d67-1e03cd9d4037","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"ce9e7dd2-2f7e-4cd7-94dc-e2dbd5bdec73","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","nodeName":"node-g1gxvdbn8vq07","initialPropertyValues":{"uriPathSegment":{"value":"tree-search","type":"string"},"title":{"value":"Tree search","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"f0acc011-ec4f-42fc-b6ec-fde3f5f5581d","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"000b42ff-c9d3-4b5e-b6de-56d35832dc0e","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","nodeName":"node-0t1w0st5k6ffr","initialPropertyValues":{"uriPathSegment":{"value":"create-new-nodes","type":"string"},"title":{"value":"Create new nodes","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"766f6f5c-700b-4698-8894-8f6b25c7160b","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"5d9a965c-884b-41af-9f21-ae305c2af2af","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","nodeName":"node-8apiz7zen1sig","initialPropertyValues":{"uriPathSegment":{"value":"discarding","type":"string"},"title":{"value":"Discarding","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"5a8a66d2-15b1-4137-8ce7-8bb76a27fc20","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"af9db7c2-3cf0-4091-978e-1a0f81766242","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","nodeName":"node-w0p249o10vma4","initialPropertyValues":{"uriPathSegment":{"value":"switching-dimensions","type":"string"},"title":{"value":"Switching dimensions","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"a49501ec-91bc-4172-a3a6-1e81daf81700","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f25d1518-60c2-4842-b2b0-bbd449697e77","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","nodeName":"node-xel5da127kla5","initialPropertyValues":{"uriPathSegment":{"value":"select-boxes","type":"string"},"title":{"value":"Select Boxes","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"1b83da25-010b-447e-b621-b145eaa1c5cc","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"4fbd958a-3b24-4001-8342-5ceaf0750ffb","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","nodeName":"node-knm2pltb5454z","initialPropertyValues":{"uriPathSegment":{"value":"tree-multiselect","type":"string"},"title":{"value":"Tree multiselect","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"866cec45-ae52-41f2-877e-3e533c0fc534","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"53c72be8-befb-409f-b2b6-ad54b68cbe05","nodeTypeName":"Neos.TestNodeTypes:Content.Headline","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","nodeName":"node-chdxek8m9mgp8","initialPropertyValues":{"title":{"value":"Content node to delete","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"9a11d10c-c3b6-41b7-9955-a66641b6e09a","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"d995d0af-2716-cbf0-a9a0-4547c87980d1","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"000b42ff-c9d3-4b5e-b6de-56d35832dc0e","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"2d239a21-4201-4b9c-9ddd-cd5fa936c650","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"edee62d3-4561-4c0c-bce0-41324b0df5db","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"000b42ff-c9d3-4b5e-b6de-56d35832dc0e","nodeName":"node-32bltfl4ugqi1","initialPropertyValues":{"uriPathSegment":{"value":"link-target","type":"string"},"title":{"value":"Link target","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"80f77eb0-d2d9-4d1b-8f27-7d8167426fcd","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"794b2be9-805d-fe98-e173-ee015811d3fc","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"edee62d3-4561-4c0c-bce0-41324b0df5db","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"6c9ed736-f124-43d9-a897-1d73abf279ec","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"9344756a-a5d1-9de0-0627-a701b27e62bf","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"5d9a965c-884b-41af-9f21-ae305c2af2af","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"26f906ac-12bd-4807-999a-f4f228c93a81","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"6c66e888-70ba-4e6c-838d-a13b642505ac","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"5d9a965c-884b-41af-9f21-ae305c2af2af","nodeName":"node-bezmi9v7mq8gm","initialPropertyValues":{"uriPathSegment":{"value":"node-to-delete","type":"string"},"title":{"value":"Node to delete","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"d3569ac5-f207-4559-864e-0ba01eed6382","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"868eb7da-ab60-15ef-2dc0-472dd8c346cd","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"6c66e888-70ba-4e6c-838d-a13b642505ac","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"21642ec4-9e43-49ac-bd71-a27b44e2813b","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f4be0477-85ad-4e7b-bc6d-b4f8d5da448c","nodeTypeName":"Neos.TestNodeTypes:Content.Headline","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"868eb7da-ab60-15ef-2dc0-472dd8c346cd","nodeName":"node-1jek82tofuzch","initialPropertyValues":{"title":{"value":"

I'll be deleted<\/h1>","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"0a3ae1e5-cd04-40f6-8c19-5aec91064320","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"487ef58b-7d6d-9e3f-b6ee-3552784ac0eb","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"ce9e7dd2-2f7e-4cd7-94dc-e2dbd5bdec73","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"c467d473-c42c-418f-9b42-9a04502e180a","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"ae7baddc-656a-4e57-9886-17d8783dae65","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"ce9e7dd2-2f7e-4cd7-94dc-e2dbd5bdec73","nodeName":"node-r6au40dkxh5k6","initialPropertyValues":{"uriPathSegment":{"value":"not-searched-page","type":"string"},"title":{"value":"Not searched page","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"a6969708-7689-42e9-ab39-eb869fd7891a","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"ecb3c490-e4ce-450b-ba59-584615d3ef81","nodeTypeName":"Neos.Neos:Shortcut","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"ce9e7dd2-2f7e-4cd7-94dc-e2dbd5bdec73","nodeName":"node-2oekw9xt2aqzh","initialPropertyValues":{"uriPathSegment":{"value":"not-searched-shortcut","type":"string"},"targetMode":{"value":"firstChildNode","type":"string"},"title":{"value":"Not searched shortcut","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"75190929-8ead-4d22-9e4c-57f9b5f52c5e","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"18b037dc-c06c-4d96-a991-893256a32e89","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"ce9e7dd2-2f7e-4cd7-94dc-e2dbd5bdec73","nodeName":"node-a8hbzgg4vij8o","initialPropertyValues":{"uriPathSegment":{"value":"searchme-page","type":"string"},"title":{"value":"Searchme page","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"d8bcef2d-d054-44ae-9c92-a56070954add","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"5a056a16-1bd6-4fca-b586-108e2130d55a","nodeTypeName":"Neos.Neos:Shortcut","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"ce9e7dd2-2f7e-4cd7-94dc-e2dbd5bdec73","nodeName":"node-2as0mtmis37be","initialPropertyValues":{"uriPathSegment":{"value":"searchme-shortcut","type":"string"},"targetMode":{"value":"firstChildNode","type":"string"},"title":{"value":"Searchme shortcut","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"1078453a-67a7-40b7-abde-6d4fa1e3ce85","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"eb217a26-d864-d456-822d-7476cef3e215","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"18b037dc-c06c-4d96-a991-893256a32e89","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"47f38ec7-69dc-4ab9-937c-b58b2450ffe5","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"e22df8b3-cc42-b6a1-184a-7778d1bf2750","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"ae7baddc-656a-4e57-9886-17d8783dae65","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"61ebdba4-8097-43be-910d-f44e809e1111","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"74263f87-2e7c-c916-c122-6a6cf4704f22","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"4fbd958a-3b24-4001-8342-5ceaf0750ffb","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"ddfa4633-add1-4bb4-86be-033cfd7fce84","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"1efe592f-2e2f-4737-b212-bbb42626e848","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"4fbd958a-3b24-4001-8342-5ceaf0750ffb","nodeName":"node-18qsaeidy6765","initialPropertyValues":{"uriPathSegment":{"value":"a","type":"string"},"title":{"value":"MultiA","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"7d859cb6-bb96-481b-999c-1f0e0f1914c1","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"5b0d6ac0-40ab-47e8-b79e-39de6c0700df","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"4fbd958a-3b24-4001-8342-5ceaf0750ffb","nodeName":"node-e8tw6sparbtp3","initialPropertyValues":{"uriPathSegment":{"value":"b","type":"string"},"title":{"value":"MultiB","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"7039e6be-dea2-48bb-abc6-605fdb7be762","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"84eb0340-ba34-4fdb-98b1-da503f967121","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"4fbd958a-3b24-4001-8342-5ceaf0750ffb","nodeName":"node-oml0cxaompt29","initialPropertyValues":{"uriPathSegment":{"value":"c","type":"string"},"title":{"value":"MultiC","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"30c8c801-884e-46f1-890c-8c9ee331300b","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f6bf1a78-9e2b-45ba-887c-9b7e43890a44","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"4fbd958a-3b24-4001-8342-5ceaf0750ffb","nodeName":"node-szu9u5yzpleck","initialPropertyValues":{"uriPathSegment":{"value":"d","type":"string"},"title":{"value":"MultiD","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"434cc297-b87c-43a9-b3c8-125a4147b8af","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"41dbc005-0327-8a86-0f98-8531112577b8","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"1efe592f-2e2f-4737-b212-bbb42626e848","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"2fab7955-21fe-421d-9809-64e21ca6c5b9","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"2969680f-75ff-13b6-efbd-f5a01fbb73e5","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"5b0d6ac0-40ab-47e8-b79e-39de6c0700df","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"a061f2ae-6fdd-49a4-ae2c-3f6e9ba57fdc","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"2ff43bf3-a953-d69d-0103-3ce80568e642","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"84eb0340-ba34-4fdb-98b1-da503f967121","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"7345ce26-df70-45c9-b464-f1d6c6d86959","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"f59c6680-06a4-4bc5-e563-9605bd4add99","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"f6bf1a78-9e2b-45ba-887c-9b7e43890a44","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"afb6efc4-7688-4d2a-a0e1-7c9ac53616ec","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"e91e184f-eb71-4b54-90dc-e8d8dd997f51","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"af9db7c2-3cf0-4091-978e-1a0f81766242","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"d483b7ad-1cc0-47e1-b70c-93906c060ae3","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"45270070-0d7c-4a89-90a8-a43ef454edf1","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"af9db7c2-3cf0-4091-978e-1a0f81766242","nodeName":"node-8ws6hkn4o36q1","initialPropertyValues":{"uriPathSegment":{"value":"translated-page","type":"string"},"title":{"value":"Translated page","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"9f60f1f3-b053-4854-9a04-8569b00f7649","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"555c9f80-7682-4916-a4c3-851ab5ea0559","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"af9db7c2-3cf0-4091-978e-1a0f81766242","nodeName":"node-f8e3ze7jvbxkl","initialPropertyValues":{"uriPathSegment":{"value":"untranslated-page","type":"string"},"title":{"value":"Untranslated page","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"01b8a28e-a2ed-4f8f-bad9-1354965a2d8c","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"d1d3bfdc-ce6a-cf2e-4eeb-02c11bbe87f4","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"45270070-0d7c-4a89-90a8-a43ef454edf1","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"c43fa247-68c4-4dd9-88f3-15e169680bfc","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"a54789f4-26df-de1d-c5dc-18a15556b40b","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"555c9f80-7682-4916-a4c3-851ab5ea0559","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"a30c607a-8767-44c7-b2a6-3d0b97bd478c","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"1fd64db3-c35f-4604-ad25-de0ef6d2bb32","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"f25d1518-60c2-4842-b2b0-bbd449697e77","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"c4a26082-8fac-4f7b-bad8-d07bfa68e9dd","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"7ebcc393-4120-4d29-a0d8-8bc708ecb3f8","workspaceName":"live","nodeAggregateId":"3938292e-bf62-472a-a701-baf6e9ff395f","nodeTypeName":"Neos.TestNodeTypes:Document.SelectBoxTestPage.OpensAboveInInspector","originDimensionSpacePoint":{"language":"en_US"},"coveredDimensionSpacePoints":[{"language":"en_US"},{"language":"en_UK"}],"parentNodeAggregateId":"f25d1518-60c2-4842-b2b0-bbd449697e77","nodeName":"node-hg78tr7bvmmn4r","initialPropertyValues":{"uriPathSegment":{"value":"select-box-opens-above-in-inspector","type":"string"},"title":{"value":"SelectBox opens above in Inspector","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/sites.json b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/sites.json new file mode 100644 index 0000000000..483b580c36 --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content/sites.json @@ -0,0 +1,17 @@ +[ + { + "name": "Neos.Test.OneDimension", + "nodeName": "neos-test-onedimension", + "siteResourcesPackageKey": "Neos.Test.OneDimension", + "online": true, + "domains": [ + { + "hostname": "onedimension.localhost", + "scheme": null, + "port": 8081, + "active": true, + "primary": true + } + ] + } +] diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/composer.json b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/composer.json similarity index 56% rename from Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/composer.json rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/composer.json index fa7065d535..9e82c50165 100644 --- a/Tests/IntegrationTests/Fixtures/1Dimension/SitePackage/composer.json +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.OneDimension/composer.json @@ -1,6 +1,6 @@ { - "name": "neos/test-site", - "description": "A Neos website", + "name": "neos/test-onedimension", + "description": "A Neos website with one dimension", "type": "neos-site", "require": { "neos/neos": "*", @@ -9,7 +9,7 @@ }, "extra": { "neos": { - "package-key": "Neos.TestSite" + "package-key": "Neos.Test.OneDimension" } } } diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Configuration/Settings.yaml b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Configuration/Settings.yaml new file mode 100644 index 0000000000..bc751b79d0 --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Configuration/Settings.yaml @@ -0,0 +1,93 @@ +Neos: + ContentRepositoryRegistry: + contentRepositories: + twodimensions: + preset: default + contentDimensions: + country: + label: 'Country' + icon: 'icon-globe' + values: + 'deu': + label: 'Germany' + specializations: + 'aut': + label: 'Austria' + 'lux': + label: 'Luxembourg' + 'dnk': + label: 'Denmark' + + 'language': + label: 'Language' + icon: icon-language + values: + 'en_US': + label: 'English (US)' + constraints: + 'country': + '*': false + 'deu': true + 'aut': true + specializations: + 'en_UK': + label: 'English (UK)' + 'de': + label: German + constraints: + 'country': + '*': false + 'deu': true + 'aut': true + 'lux': true + 'fr': + label: French + constraints: + 'country': + '*': false + 'deu': true + 'aut': true + 'nl': + label: Dutch + constraints: + 'country': + '*': false + 'deu': true + 'aut': true + 'da': + label: Danish + constraints: + 'country': + '*': false + 'dnk': true + 'lv': + label: Latvian + constraints: + 'country': + '*': false + 'deu': true + 'aut': true + Neos: + sites: + 'neos-test-twodimensions': + contentRepository: 'twodimensions' + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\UriPathResolverFactory + options: + segments: + - dimensionIdentifier: 'country' + dimensionValueMapping: + 'deu': 'deu' + 'aut': 'aut' + 'lux': 'lux' + 'dnk': 'dnk' + - dimensionIdentifier: 'language' + dimensionValueMapping: + 'en_US': 'en' + 'en_UK': 'uk' + 'de': 'de' + 'fr': 'fr' + 'nl': 'nl' + 'da': 'da' + 'lv': 'lv' diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/Assets/ee3d239e-48b0-4f99-90be-054301b91792.json b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/Assets/ee3d239e-48b0-4f99-90be-054301b91792.json new file mode 100644 index 0000000000..e5d089b54b --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/Assets/ee3d239e-48b0-4f99-90be-054301b91792.json @@ -0,0 +1,14 @@ +{ + "identifier": "ee3d239e-48b0-4f99-90be-054301b91792", + "type": "IMAGE", + "title": "", + "copyrightNotice": "", + "caption": "", + "assetSourceIdentifier": "neos", + "resource": { + "filename": "neos_primary.png", + "collectionName": "persistent", + "mediaType": "image\/png", + "sha1": "aac28f51e5ca842e2646e88e7d242ac3c27e1f25" + } +} \ No newline at end of file diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/ImageVariants/50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1.json b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/ImageVariants/50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1.json new file mode 100644 index 0000000000..af47865fee --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/ImageVariants/50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1.json @@ -0,0 +1,20 @@ +{ + "identifier": "50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1", + "originalAssetIdentifier": "ee3d239e-48b0-4f99-90be-054301b91792", + "name": "", + "width": 126, + "height": 126, + "presetIdentifier": null, + "presetVariantName": null, + "imageAdjustments": [ + { + "type": "CROP_IMAGE", + "properties": { + "x": 328, + "y": 0, + "width": 126, + "height": 126 + } + } + ] +} \ No newline at end of file diff --git a/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Resources/Private/Content/Resources/aac28f51e5ca842e2646e88e7d242ac3c27e1f25 b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/Resources/aac28f51e5ca842e2646e88e7d242ac3c27e1f25 similarity index 100% rename from Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Resources/Private/Content/Resources/aac28f51e5ca842e2646e88e7d242ac3c27e1f25 rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/Resources/aac28f51e5ca842e2646e88e7d242ac3c27e1f25 diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/events.jsonl b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/events.jsonl new file mode 100644 index 0000000000..9d43e426a3 --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/events.jsonl @@ -0,0 +1,28 @@ +{"identifier":"55ea0055-eda8-4077-b3b0-2c2d599a8f27","type":"RootNodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"2fbc3faf-663a-4ef0-b586-7b2c1c7fd342","nodeTypeName":"Neos.Neos:Sites","coveredDimensionSpacePoints":[{"country":"deu","language":"en_US"},{"country":"deu","language":"en_UK"},{"country":"deu","language":"de"},{"country":"deu","language":"fr"},{"country":"deu","language":"nl"},{"country":"deu","language":"lv"},{"country":"aut","language":"en_US"},{"country":"aut","language":"en_UK"},{"country":"aut","language":"de"},{"country":"aut","language":"fr"},{"country":"aut","language":"nl"},{"country":"aut","language":"lv"},{"country":"lux","language":"en_UK"},{"country":"lux","language":"de"},{"country":"dnk","language":"en_UK"},{"country":"dnk","language":"da"}],"nodeAggregateClassification":"root"},"metadata":[]} +{"identifier":"a9fdb629-97ce-4321-bbec-a1dbf27530fd","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","nodeTypeName":"Neos.TestNodeTypes:Document.HomePage","originDimensionSpacePoint":{"country":"dnk","language":"da"},"coveredDimensionSpacePoints":[{"country":"dnk","language":"da"}],"parentNodeAggregateId":"2fbc3faf-663a-4ef0-b586-7b2c1c7fd342","nodeName":"neos-test-twodimensions","initialPropertyValues":{"title":{"value":"Home","type":"string"},"uriPathSegment":{"value":"home","type":"string"},"image":{"value":{"__flow_object_type":"Neos\\Media\\Domain\\Model\\ImageVariant","__identifier":"50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1"},"type":"Neos\\Media\\Domain\\Model\\ImageInterface"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"06ce4e00-bede-4d76-a6b9-02c646ce933b","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","sourceOrigin":{"country":"dnk","language":"da"},"peerOrigin":{"country":"deu","language":"lv"},"peerCoverage":[{"country":"deu","language":"lv"},{"country":"aut","language":"lv"}]},"metadata":[]} +{"identifier":"c8b2be8f-fd74-4ea5-b2f8-35a3876d11a9","type":"NodePropertiesWereSet","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","originDimensionSpacePoint":{"country":"deu","language":"lv"},"affectedDimensionSpacePoints":[{"country":"deu","language":"lv"},{"country":"aut","language":"lv"}],"propertyValues":{"title":{"value":"M\u0101jas","type":"string"},"uriPathSegment":{"value":"home","type":"string"}},"propertiesToUnset":[]},"metadata":[]} +{"identifier":"c2c89695-2e12-41ac-bc4a-9b197b67a6f8","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","sourceOrigin":{"country":"deu","language":"lv"},"peerOrigin":{"country":"deu","language":"en_US"},"peerCoverage":[{"country":"deu","language":"en_US"},{"country":"aut","language":"en_US"},{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}]},"metadata":[]} +{"identifier":"9a7f0b96-edf3-43f0-95ab-41dc553e6238","type":"NodePropertiesWereSet","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","originDimensionSpacePoint":{"country":"deu","language":"en_US"},"affectedDimensionSpacePoints":[{"country":"deu","language":"en_US"},{"country":"aut","language":"en_US"},{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}],"propertyValues":{"title":{"value":"Home","type":"string"},"uriPathSegment":{"value":"home","type":"string"},"image":{"value":{"__flow_object_type":"Neos\\Media\\Domain\\Model\\ImageVariant","__identifier":"50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1"},"type":"Neos\\Media\\Domain\\Model\\ImageInterface"}},"propertiesToUnset":[]},"metadata":[]} +{"identifier":"8895d087-a855-485b-a0c5-fc2bd0953468","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","sourceOrigin":{"country":"deu","language":"en_US"},"peerOrigin":{"country":"deu","language":"nl"},"peerCoverage":[{"country":"deu","language":"nl"},{"country":"aut","language":"nl"}]},"metadata":[]} +{"identifier":"b1c595ba-ecf2-48cf-bd58-b14b2823b878","type":"NodePropertiesWereSet","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","originDimensionSpacePoint":{"country":"deu","language":"nl"},"affectedDimensionSpacePoints":[{"country":"deu","language":"nl"},{"country":"aut","language":"nl"}],"propertyValues":{"title":{"value":"Huis","type":"string"},"uriPathSegment":{"value":"home","type":"string"}},"propertiesToUnset":[]},"metadata":[]} +{"identifier":"17735c4f-8a79-4224-87c7-31182ddb04b1","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","sourceOrigin":{"country":"deu","language":"nl"},"peerOrigin":{"country":"deu","language":"de"},"peerCoverage":[{"country":"deu","language":"de"},{"country":"aut","language":"de"},{"country":"lux","language":"de"}]},"metadata":[]} +{"identifier":"aa8cc064-78f1-4bfc-b781-9c1c0782b0a9","type":"NodePropertiesWereSet","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","originDimensionSpacePoint":{"country":"deu","language":"de"},"affectedDimensionSpacePoints":[{"country":"deu","language":"de"},{"country":"aut","language":"de"},{"country":"lux","language":"de"}],"propertyValues":{"title":{"value":"Startseite","type":"string"},"uriPathSegment":{"value":"home","type":"string"}},"propertiesToUnset":[]},"metadata":[]} +{"identifier":"acc19ed2-3409-4801-8579-7cefdd967908","type":"NodeSpecializationVariantWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","sourceOrigin":{"country":"deu","language":"en_US"},"specializationOrigin":{"country":"deu","language":"en_UK"},"specializationCoverage":[{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}]},"metadata":[]} +{"identifier":"406b7e68-d98d-4bd1-bf68-b728ab2b539f","type":"NodePropertiesWereSet","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","originDimensionSpacePoint":{"country":"deu","language":"en_UK"},"affectedDimensionSpacePoints":[{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}],"propertyValues":{"title":{"value":"Home","type":"string"},"uriPathSegment":{"value":"home","type":"string"},"image":{"value":{"__flow_object_type":"Neos\\Media\\Domain\\Model\\ImageVariant","__identifier":"50cd4a3e-1cc3-4bbb-b2ab-919abb4011f1"},"type":"Neos\\Media\\Domain\\Model\\ImageInterface"}},"propertiesToUnset":[]},"metadata":[]} +{"identifier":"7075a98d-ebe8-4035-92b9-8dc3e9e09098","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","sourceOrigin":{"country":"deu","language":"en_UK"},"peerOrigin":{"country":"deu","language":"fr"},"peerCoverage":[{"country":"deu","language":"fr"},{"country":"aut","language":"fr"}]},"metadata":[]} +{"identifier":"5cbcac8a-35e1-47e5-a55c-3392ddeb0ff3","type":"NodePropertiesWereSet","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","originDimensionSpacePoint":{"country":"deu","language":"fr"},"affectedDimensionSpacePoints":[{"country":"deu","language":"fr"},{"country":"aut","language":"fr"}],"propertyValues":{"title":{"value":"Accueil","type":"string"},"uriPathSegment":{"value":"home","type":"string"}},"propertiesToUnset":[]},"metadata":[]} +{"identifier":"a4a517b0-cc81-4840-9fb4-8dc501e57093","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"country":"deu","language":"lv"},"coveredDimensionSpacePoints":[{"country":"deu","language":"lv"},{"country":"aut","language":"lv"}],"parentNodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"bee965e3-c5e9-401d-ae17-cc21699b4f1d","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","sourceOrigin":{"country":"deu","language":"lv"},"peerOrigin":{"country":"deu","language":"fr"},"peerCoverage":[{"country":"deu","language":"fr"},{"country":"aut","language":"fr"}]},"metadata":[]} +{"identifier":"68479aef-43d6-4c9b-9af6-5e582c6fbcdf","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","sourceOrigin":{"country":"deu","language":"fr"},"peerOrigin":{"country":"deu","language":"en_US"},"peerCoverage":[{"country":"deu","language":"en_US"},{"country":"aut","language":"en_US"},{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}]},"metadata":[]} +{"identifier":"2c399044-3a08-4465-ae49-98486029dc8d","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","sourceOrigin":{"country":"deu","language":"en_US"},"peerOrigin":{"country":"dnk","language":"da"},"peerCoverage":[{"country":"dnk","language":"da"}]},"metadata":[]} +{"identifier":"6b9abd14-e993-49c7-a255-d32024fc6e8b","type":"NodeSpecializationVariantWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","sourceOrigin":{"country":"deu","language":"en_US"},"specializationOrigin":{"country":"deu","language":"en_UK"},"specializationCoverage":[{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}]},"metadata":[]} +{"identifier":"07f9b89a-704e-4f66-80d1-b5670ccbd110","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","sourceOrigin":{"country":"deu","language":"en_UK"},"peerOrigin":{"country":"deu","language":"nl"},"peerCoverage":[{"country":"deu","language":"nl"},{"country":"aut","language":"nl"}]},"metadata":[]} +{"identifier":"42a96bec-0f04-43ac-bbc7-be9baedbde2e","type":"NodePeerVariantWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","sourceOrigin":{"country":"deu","language":"nl"},"peerOrigin":{"country":"deu","language":"de"},"peerCoverage":[{"country":"deu","language":"de"},{"country":"aut","language":"de"},{"country":"lux","language":"de"}]},"metadata":[]} +{"identifier":"e1263785-b57d-4269-929f-670da4279cf0","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"af9db7c2-3cf0-4091-978e-1a0f81766242","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"country":"deu","language":"en_US"},"coveredDimensionSpacePoints":[{"country":"deu","language":"en_US"},{"country":"aut","language":"en_US"},{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}],"parentNodeAggregateId":"f676459d-ca77-44bc-aeea-44114814c279","nodeName":"node-w0p249o10vma4","initialPropertyValues":{"uriPathSegment":{"value":"switching-dimensions","type":"string"},"title":{"value":"Switching dimensions","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"58459bb0-6ac4-4f32-9287-e2468bdbdccc","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"53c72be8-befb-409f-b2b6-ad54b68cbe05","nodeTypeName":"Neos.TestNodeTypes:Content.Headline","originDimensionSpacePoint":{"country":"deu","language":"en_US"},"coveredDimensionSpacePoints":[{"country":"deu","language":"en_US"},{"country":"aut","language":"en_US"},{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}],"parentNodeAggregateId":"6c302e0e-1d54-4697-a7ec-88d4e0d010cf","nodeName":"node-chdxek8m9mgp8","initialPropertyValues":{"title":{"value":"Content node to delete","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"e2d2f6a6-67c4-48dd-a4f1-1026ac3d0065","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"e91e184f-eb71-4b54-90dc-e8d8dd997f51","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"country":"deu","language":"en_US"},"coveredDimensionSpacePoints":[{"country":"deu","language":"en_US"},{"country":"aut","language":"en_US"},{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}],"parentNodeAggregateId":"af9db7c2-3cf0-4091-978e-1a0f81766242","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"0ee5bb06-c2a7-473e-8dc4-c3faa515d0d6","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"45270070-0d7c-4a89-90a8-a43ef454edf1","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"country":"deu","language":"en_US"},"coveredDimensionSpacePoints":[{"country":"deu","language":"en_US"},{"country":"aut","language":"en_US"},{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}],"parentNodeAggregateId":"af9db7c2-3cf0-4091-978e-1a0f81766242","nodeName":"node-8ws6hkn4o36q1","initialPropertyValues":{"uriPathSegment":{"value":"translated-page","type":"string"},"title":{"value":"Translated page","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"d82b51b6-ffcf-4119-a279-80b137db438d","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"555c9f80-7682-4916-a4c3-851ab5ea0559","nodeTypeName":"Neos.TestNodeTypes:Document.Page","originDimensionSpacePoint":{"country":"deu","language":"en_US"},"coveredDimensionSpacePoints":[{"country":"deu","language":"en_US"},{"country":"aut","language":"en_US"},{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}],"parentNodeAggregateId":"af9db7c2-3cf0-4091-978e-1a0f81766242","nodeName":"node-f8e3ze7jvbxkl","initialPropertyValues":{"uriPathSegment":{"value":"untranslated-page","type":"string"},"title":{"value":"Untranslated page","type":"string"}},"nodeAggregateClassification":"regular","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"6758939c-0091-43a0-9cee-16228b6ffdf9","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"d1d3bfdc-ce6a-cf2e-4eeb-02c11bbe87f4","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"country":"deu","language":"en_US"},"coveredDimensionSpacePoints":[{"country":"deu","language":"en_US"},{"country":"aut","language":"en_US"},{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}],"parentNodeAggregateId":"45270070-0d7c-4a89-90a8-a43ef454edf1","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} +{"identifier":"3db13d5f-9a7d-4e98-8020-ac37bdc4ad92","type":"NodeAggregateWithNodeWasCreated","payload":{"contentStreamId":"ead48ed5-f915-44bd-8df8-29475c8b145b","workspaceName":"live","nodeAggregateId":"a54789f4-26df-de1d-c5dc-18a15556b40b","nodeTypeName":"Neos.Neos:ContentCollection","originDimensionSpacePoint":{"country":"deu","language":"en_US"},"coveredDimensionSpacePoints":[{"country":"deu","language":"en_US"},{"country":"aut","language":"en_US"},{"country":"deu","language":"en_UK"},{"country":"aut","language":"en_UK"},{"country":"lux","language":"en_UK"}],"parentNodeAggregateId":"555c9f80-7682-4916-a4c3-851ab5ea0559","nodeName":"main","initialPropertyValues":[],"nodeAggregateClassification":"tethered","succeedingNodeAggregateId":null},"metadata":[]} diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/sites.json b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/sites.json new file mode 100644 index 0000000000..e9f8af6712 --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content/sites.json @@ -0,0 +1,17 @@ +[ + { + "name": "Neos.Test.TwoDimensions", + "nodeName": "neos-test-twodimensions", + "siteResourcesPackageKey": "Neos.Test.TwoDimensions", + "online": true, + "domains": [ + { + "hostname": "twodimensions.localhost", + "scheme": null, + "port": 8081, + "active": true, + "primary": true + } + ] + } +] diff --git a/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Resources/Private/Fusion/Root.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Fusion/Root.fusion similarity index 100% rename from Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/Resources/Private/Fusion/Root.fusion rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Fusion/Root.fusion diff --git a/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/composer.json b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/composer.json similarity index 56% rename from Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/composer.json rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/composer.json index fa7065d535..82d376240b 100644 --- a/Tests/IntegrationTests/Fixtures/2Dimension/SitePackage/composer.json +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.Test.TwoDimensions/composer.json @@ -1,6 +1,6 @@ { - "name": "neos/test-site", - "description": "A Neos website", + "name": "neos/test-twodimensions", + "description": "A Neos website with two dimensions", "type": "neos-site", "require": { "neos/neos": "*", @@ -9,7 +9,7 @@ }, "extra": { "neos": { - "package-key": "Neos.TestSite" + "package-key": "Neos.Test.TwoDimensions" } } } diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/RemoveAdditionalSettings/Controller/RemoveAdditionalSettingsController.php b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/RemoveAdditionalSettings/Controller/RemoveAdditionalSettingsController.php new file mode 100644 index 0000000000..db3ddac910 --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/RemoveAdditionalSettings/Controller/RemoveAdditionalSettingsController.php @@ -0,0 +1,60 @@ +getArguments()); + $this->commandHandler->handle($command); + return new Response(status: 200, headers: ['Content-Type' => 'application/json'], body: json_encode( + ['success' => true], + JSON_THROW_ON_ERROR + )); + } catch (\InvalidArgumentException $e) { + return new Response(status: 400, headers: ['Content-Type' => 'application/json'], body: json_encode( + ['error' => [ + 'type' => $e::class, + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + ]], + JSON_THROW_ON_ERROR + )); + } catch (\Exception $e) { + return new Response(status: 500, headers: ['Content-Type' => 'application/json'], body: json_encode( + ['error' => [ + 'type' => $e::class, + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + ]], + JSON_THROW_ON_ERROR + )); + } + } +} diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/RemoveAdditionalSettings/RemoveAdditionalSettingsCommand.php b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/RemoveAdditionalSettings/RemoveAdditionalSettingsCommand.php similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/RemoveAdditionalSettings/RemoveAdditionalSettingsCommand.php rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/RemoveAdditionalSettings/RemoveAdditionalSettingsCommand.php diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/RemoveAdditionalSettings/RemoveAdditionalSettingsCommandHandler.php b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/RemoveAdditionalSettings/RemoveAdditionalSettingsCommandHandler.php similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/RemoveAdditionalSettings/RemoveAdditionalSettingsCommandHandler.php rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/RemoveAdditionalSettings/RemoveAdditionalSettingsCommandHandler.php diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/WriteAdditionalSettings/Controller/WriteAdditionalSettingsController.php b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/WriteAdditionalSettings/Controller/WriteAdditionalSettingsController.php new file mode 100644 index 0000000000..e2944f2ce0 --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/WriteAdditionalSettings/Controller/WriteAdditionalSettingsController.php @@ -0,0 +1,60 @@ +getArguments()); + $this->commandHandler->handle($command); + return new Response(status: 200, headers: ['Content-Type' => 'application/json'], body: json_encode( + ['success' => true], + JSON_THROW_ON_ERROR + )); + } catch (\InvalidArgumentException $e) { + return new Response(status: 400, headers: ['Content-Type' => 'application/json'], body: json_encode( + ['error' => [ + 'type' => $e::class, + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + ]], + JSON_THROW_ON_ERROR + )); + } catch (\Exception $e) { + return new Response(status: 500, headers: ['Content-Type' => 'application/json'], body: json_encode( + ['error' => [ + 'type' => $e::class, + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + ]], + JSON_THROW_ON_ERROR + )); + } + } +} diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/WriteAdditionalSettings/WriteAdditionalSettingsCommand.php b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/WriteAdditionalSettings/WriteAdditionalSettingsCommand.php similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/WriteAdditionalSettings/WriteAdditionalSettingsCommand.php rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/WriteAdditionalSettings/WriteAdditionalSettingsCommand.php diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/WriteAdditionalSettings/WriteAdditionalSettingsCommandHandler.php b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/WriteAdditionalSettings/WriteAdditionalSettingsCommandHandler.php similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Classes/Application/WriteAdditionalSettings/WriteAdditionalSettingsCommandHandler.php rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/Application/WriteAdditionalSettings/WriteAdditionalSettingsCommandHandler.php diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/DataSources/NodeWithDependingPropertiesDataSource.php b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/DataSources/NodeWithDependingPropertiesDataSource.php similarity index 78% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Classes/DataSources/NodeWithDependingPropertiesDataSource.php rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/DataSources/NodeWithDependingPropertiesDataSource.php index 597eb52375..25d8bee8bf 100644 --- a/Tests/IntegrationTests/SharedNodeTypesPackage/Classes/DataSources/NodeWithDependingPropertiesDataSource.php +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/DataSources/NodeWithDependingPropertiesDataSource.php @@ -1,8 +1,8 @@ {value}` - collection = ${q(site).children('[instanceof Neos.Neos:Document]').get()} + items = ${q(site).children('[instanceof Neos.Neos:Document]').get()} itemName = 'node' itemRenderer = Neos.Fusion:Tag { @process.wrap = afx`
  • {value}
  • ` diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Content/Container/Container.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Content/Container/Container.fusion similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Content/Container/Container.fusion rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Content/Container/Container.fusion diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Content/Headline/Headline.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Content/Headline/Headline.fusion similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Content/Headline/Headline.fusion rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Content/Headline/Headline.fusion diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Content/Image/Image.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Content/Image/Image.fusion similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Content/Image/Image.fusion rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Content/Image/Image.fusion diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Content/InlineHeadline/InlineHeadline.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Content/InlineHeadline/InlineHeadline.fusion similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Content/InlineHeadline/InlineHeadline.fusion rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Content/InlineHeadline/InlineHeadline.fusion diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Content/NodeWithDependingProperties/NodeWithDependingProperties.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Content/NodeWithDependingProperties/NodeWithDependingProperties.fusion similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Content/NodeWithDependingProperties/NodeWithDependingProperties.fusion rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Content/NodeWithDependingProperties/NodeWithDependingProperties.fusion diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Content/Text/Text.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Content/Text/Text.fusion similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Content/Text/Text.fusion rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Content/Text/Text.fusion diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Document/Page/HomePage.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Document/Page/HomePage.fusion new file mode 100644 index 0000000000..f55a6eecac --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Document/Page/HomePage.fusion @@ -0,0 +1 @@ +prototype(Neos.TestNodeTypes:Document.HomePage) < prototype(Neos.TestNodeTypes:Document.Page) diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Document/Page/Page.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Document/Page/Page.fusion similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Document/Page/Page.fusion rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Document/Page/Page.fusion diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Document/PageWithImage/PageWithImage.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Document/PageWithImage/PageWithImage.fusion similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Document/PageWithImage/PageWithImage.fusion rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Document/PageWithImage/PageWithImage.fusion diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Document/SelectBoxTestPage/OpensAboveIn.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Document/SelectBoxTestPage/OpensAboveIn.fusion similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Document/SelectBoxTestPage/OpensAboveIn.fusion rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Document/SelectBoxTestPage/OpensAboveIn.fusion diff --git a/Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Document/SelectBoxTestPage/OpensAboveInInspector.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Document/SelectBoxTestPage/OpensAboveInInspector.fusion similarity index 100% rename from Tests/IntegrationTests/SharedNodeTypesPackage/Resources/Private/Fusion/Document/SelectBoxTestPage/OpensAboveInInspector.fusion rename to Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Document/SelectBoxTestPage/OpensAboveInInspector.fusion diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Root.fusion b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Root.fusion new file mode 100644 index 0000000000..fe1d560a40 --- /dev/null +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Resources/Private/Fusion/Root.fusion @@ -0,0 +1 @@ +include: **/*.fusion diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/composer.json b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/composer.json index 47c0164f23..e0af4bd7c2 100644 --- a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/composer.json +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/composer.json @@ -3,8 +3,8 @@ "description": "Some dummy nodetypes", "type": "neos-package", "require": { - "neos/neos-ui": "*", - "neos/fusion-afx": "*" + "neos/neos": "*", + "neos/neos-ui": "*" }, "extra": { "neos": { @@ -12,8 +12,8 @@ } }, "autoload": { - "psr-4": { - "Neos\\TestNodeTypes\\": "Classes" - } + "psr-4": { + "Neos\\TestNodeTypes\\": "Classes" + } } } diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestSite/composer.json b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestSite/composer.json deleted file mode 100644 index 6b391192c8..0000000000 --- a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestSite/composer.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "neos/test-site", - "description": "A dummy site package that will be replaced by fixtures during tests", - "type": "neos-site", - "require": { - "neos/neos": "*", - "neos/neos-ui": "*", - "neos/fusion-afx": "*" - }, - "extra": { - "neos": { - "package-key": "Neos.TestSite" - } - } -} diff --git a/Tests/IntegrationTests/TestDistribution/composer.json b/Tests/IntegrationTests/TestDistribution/composer.json index b9e54f9fa7..24e9fb8c31 100644 --- a/Tests/IntegrationTests/TestDistribution/composer.json +++ b/Tests/IntegrationTests/TestDistribution/composer.json @@ -1,4 +1,3 @@ - { "name": "neos/test-distribution", "description": "Neos Ui test distribution", @@ -11,11 +10,17 @@ } }, "minimum-stability": "dev", + "prefer-stable": true, "require": { - "neos/neos-development-collection": "8.3.x-dev", - "neos/neos-ui": "8.4.x-dev", - "neos/fusion-afx": "*", - "neos/test-site": "@dev", + "neos/flow-development-collection": "9.0.x-dev as 9.0", + + "neos/neos-development-collection": "9.0.x-dev as 9.0", + + "neos/neos-ui": "9.0.x-dev as 9.0", + "neos/neos-ui-compiled": "9.0.x-dev as 9.0", + + "neos/test-onedimension": "@dev", + "neos/test-twodimensions": "@dev", "neos/test-nodetypes": "@dev", "cweagans/composer-patches": "^1.7.3" @@ -26,7 +31,8 @@ }, "require-dev": { "neos/buildessentials": "@dev", - "phpunit/phpunit": "^8.1" + "phpunit/phpunit": "^9.0", + "phpstan/phpstan": "^1.10" }, "repositories": { "distribution": { diff --git a/Tests/IntegrationTests/TestDistribution/setup.sh b/Tests/IntegrationTests/TestDistribution/setup.sh deleted file mode 100755 index 58d012cdbd..0000000000 --- a/Tests/IntegrationTests/TestDistribution/setup.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# This script setups the Neos Test Distribution in order to be able to run E2E tests -# Please make sure to adjust the database settings in Configuration/Settings.yaml - -composer install -cd Packages/Application/Neos.Neos.Ui -make setup -cd ../../.. -./flow doctrine:migrate -./flow flow:cache:flush -./flow user:create --username=admin --password=password --first-name=John --last-name=Doe --roles=Administrator || true - -echo "" -echo "The setup is complete!" -echo "To run E2E test suite execute:" -echo " cd Packages/Application/Neos.Neos.Ui && make test-e2e" -echo "" - -./flow server:run --port 8081 diff --git a/Tests/IntegrationTests/docker-compose.neos-dev-instance.yaml b/Tests/IntegrationTests/docker-compose.neos-dev-instance.yaml index b0dd656f7f..fa822f2292 100644 --- a/Tests/IntegrationTests/docker-compose.neos-dev-instance.yaml +++ b/Tests/IntegrationTests/docker-compose.neos-dev-instance.yaml @@ -14,12 +14,15 @@ services: # Enable GD PHP_EXTENSION_GD: 1 COMPOSER_CACHE_DIR: /home/circleci/.composer/cache + DB_HOST: db db: image: mariadb:10.11 environment: MYSQL_DATABASE: neos MYSQL_ROOT_PASSWORD: not_a_real_password + ports: + - 13309:3306 command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci'] volumes: composer_cache: diff --git a/Tests/IntegrationTests/docker-compose.yaml b/Tests/IntegrationTests/docker-compose.yaml index cd6adb257b..e7b4682c18 100644 --- a/Tests/IntegrationTests/docker-compose.yaml +++ b/Tests/IntegrationTests/docker-compose.yaml @@ -7,11 +7,15 @@ services: ports: - 8081:8081 volumes: + - app:/usr/src/app - composer_cache:/home/circleci/.composer/cache + # add Neos Ui root as cached read-only volume that will be later symlinked into TestDistribution/Packages/ + - ../../.:/usr/src/neos-ui:cached,ro environment: # Enable GD PHP_EXTENSION_GD: 1 COMPOSER_CACHE_DIR: /home/circleci/.composer/cache + DB_HOST: db db: image: mariadb:10.11 @@ -21,4 +25,5 @@ services: command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci'] volumes: + app: composer_cache: diff --git a/Tests/IntegrationTests/e2e-docker.sh b/Tests/IntegrationTests/e2e-docker.sh index adb8c1c76f..bd425b8bd4 100755 --- a/Tests/IntegrationTests/e2e-docker.sh +++ b/Tests/IntegrationTests/e2e-docker.sh @@ -18,9 +18,12 @@ echo "########################################################################## dc down dc up -d dc exec -T php bash <<-'BASH' - rm -rf /usr/src/app/* + # WHY: change owner for composer cache for docker execution + sudo chown -R docker:docker /home/circleci/ BASH -docker cp $(pwd)/Tests/IntegrationTests/. $(dc ps -q php):/usr/src/app +#echo docker cp $(pwd)/Tests/IntegrationTests/TestDistribution/composer.json $(dc ps -q php):/usr/src/app/composer.json +#docker cp $(pwd)/Tests/IntegrationTests/TestDistribution/composer.json $(dc ps -q php):/usr/src/app/composer.json + sleep 2 echo "" @@ -29,10 +32,13 @@ echo "# Install dependencies... echo "#############################################################################" dc exec -T php bash <<-'BASH' cd /usr/src/app + mkdir -p Configuration sudo chown -R docker:docker . - # WHY: change owner for composer cache for docker execution - sudo chown -R docker:docker /home/circleci/ - cd TestDistribution + + ln -sf /usr/src/neos-ui/Tests/IntegrationTests/TestDistribution/composer.json /usr/src/app/composer.json + ln -sf /usr/src/neos-ui/Tests/IntegrationTests/TestDistribution/Configuration/Settings.yaml /usr/src/app/Configuration/Settings.yaml + ln -sfn /usr/src/neos-ui/Tests/IntegrationTests/TestDistribution/DistributionPackages /usr/src/app/DistributionPackages + composer install BASH @@ -40,16 +46,23 @@ echo "" echo "#############################################################################" echo "# Initialize Neos... #" echo "#############################################################################" -docker cp $(pwd)/. $(dc ps -q php):/usr/src/app/TestDistribution/Packages/Application/neos-ui dc exec -T php bash <<-'BASH' - cd TestDistribution rm -rf Packages/Application/Neos.Neos.Ui - mv Packages/Application/neos-ui Packages/Application/Neos.Neos.Ui - sed -i 's/host: 127.0.0.1/host: db/g' Configuration/Settings.yaml + ln -s /usr/src/neos-ui /usr/src/app/Packages/Application/Neos.Neos.Ui + ./flow flow:cache:flush ./flow flow:cache:warmup ./flow doctrine:migrate - ./flow user:create --username=admin --password=password --first-name=John --last-name=Doe --roles=Administrator || true + ./flow user:create --username=admin --password=admin --first-name=Admin --last-name=Admington --roles=Administrator || true + ./flow user:create --username=editor --password=editor --first-name=Editor --last-name=McEditworth --roles=Editor || true + + ./flow cr:setup --content-repository onedimension + ./flow site:importall --content-repository onedimension --path ./DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content + + ./flow cr:setup --content-repository twodimensions + ./flow site:importall --content-repository twodimensions --path ./DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content + + ./flow resource:publish BASH echo "" @@ -57,7 +70,6 @@ echo "########################################################################## echo "# Start Flow Server... #" echo "#############################################################################" dc exec -T php bash <<-'BASH' - cd TestDistribution ./flow server:run --port 8081 --host 0.0.0.0 & BASH @@ -65,35 +77,5 @@ echo "" echo "#############################################################################" echo "# Run E2E tests... #" echo "#############################################################################" -for fixture in $(pwd)/Tests/IntegrationTests/Fixtures/*/; do - echo "" - echo "########################################" - echo "# Fixture '$(basename $fixture)'" - echo "########################################" - dc exec -T php bash <<-BASH - mkdir -p ./TestDistribution/DistributionPackages - - rm -rf ./TestDistribution/DistributionPackages/Neos.TestNodeTypes - ln -s "../../SharedNodeTypesPackage" ./TestDistribution/DistributionPackages/Neos.TestNodeTypes - - rm -rf ./TestDistribution/DistributionPackages/Neos.TestSite - ln -s "../../Fixtures/$(basename $fixture)/SitePackage" ./TestDistribution/DistributionPackages/Neos.TestSite - - # TODO: optimize this - cd TestDistribution - composer reinstall neos/test-nodetypes - composer reinstall neos/test-site - ./flow flow:cache:flush --force - ./flow flow:cache:warmup - ./flow configuration:show --path Neos.ContentRepository.contentDimensions - - if ./flow site:list | grep -q 'Node name'; then - ./flow site:prune '*' - fi - ./flow site:import --package-key=Neos.TestSite - ./flow resource:publish -BASH - - yarn run testcafe "$1" "${fixture}*.e2e.js" \ - --selector-timeout=10000 --assertion-timeout=30000 --debug-on-fail -done +yarn run testcafe "$1" "$(pwd)/Tests/IntegrationTests/Fixtures/*/*.e2e.js" \ + --selector-timeout=10000 --assertion-timeout=30000 --debug-on-fail diff --git a/Tests/IntegrationTests/e2e.sh b/Tests/IntegrationTests/e2e.sh index 4b5c3c9bec..eb2b918e90 100755 --- a/Tests/IntegrationTests/e2e.sh +++ b/Tests/IntegrationTests/e2e.sh @@ -75,68 +75,26 @@ function check_saucectl_installed { fi } -# parse dimension from fixture file name -function get_dimension() { - dimension=$(basename "$1") - echo "$dimension" -} - -# Function that gets a fixture as parameter. With the fixture we -# load the related site package and import the site. -function initialize_neos_site() { - local fixture=$1 - - ln -s "../${fixture}SitePackage" DistributionPackages/Neos.TestSite - - composer reinstall neos/test-nodetypes - composer reinstall neos/test-site - # make sure neos is installed even if patching led to the removal (bug) - composer update neos/neos-development-collection - ./flow flow:cache:flush --force - ./flow flow:cache:warmup - ./flow configuration:show --path Neos.ContentRepository.contentDimensions - - if ./flow site:list | grep -q 'Node name'; then - ./flow site:prune '*' - fi - ./flow site:import --package-key=Neos.TestSite - ./flow resource:publish -} - function run_tests() { cd ../../.. - rm -rf DummyDistributionPackages || true - mv DistributionPackages DummyDistributionPackages - mkdir DistributionPackages - - ln -s "../Packages/Application/Neos.Neos.Ui/Tests/IntegrationTests/SharedNodeTypesPackage" DistributionPackages/Neos.TestNodeTypes + ./flow cr:setup --content-repository onedimension + ./flow site:importall --content-repository onedimension --path ./DistributionPackages/Neos.Test.OneDimension/Resources/Private/Content - for fixture in Packages/Application/Neos.Neos.Ui/Tests/IntegrationTests/Fixtures/*/; do - dimension=$(get_dimension "$fixture") - initialize_neos_site "$fixture" + ./flow cr:setup --content-repository twodimensions + ./flow site:importall --content-repository twodimensions --path ./DistributionPackages/Neos.Test.TwoDimensions/Resources/Private/Content - # go tp the Neos.Neos.Ui package and run the tests - cd Packages/Application/Neos.Neos.Ui + ./flow resource:publish - if [[ $BROWSER ]]; then - yarn run testcafe "$BROWSER" "../../../${fixture}*.e2e.js" --selector-timeout=10000 --assertion-timeout=30000 || hasFailure=1 - fi + cd Packages/Application/Neos.Neos.Ui - if [[ $USE_SAUCELABS ]]; then - saucectl run --config .sauce/config${dimension}.yml || hasFailure=1 - fi - - # cd back to the root directory and clean up - cd ../../.. - rm -f DistributionPackages/Neos.TestSite - done - - rm -rf DistributionPackages - mv DummyDistributionPackages DistributionPackages + if [[ $BROWSER ]]; then + yarn run testcafe "$BROWSER" "Tests/IntegrationTests/Fixtures/*/*.e2e.js" \ + --selector-timeout=10000 --assertion-timeout=30000 + fi - if [[ $hasFailure -eq 1 ]] ; then - exit 1 + if [[ $USE_SAUCELABS ]]; then + saucectl run --config .sauce/config.yml fi } diff --git a/Tests/IntegrationTests/pageModel.js b/Tests/IntegrationTests/pageModel.js index 2c4551e904..c75de226d5 100644 --- a/Tests/IntegrationTests/pageModel.js +++ b/Tests/IntegrationTests/pageModel.js @@ -59,19 +59,55 @@ export class PublishDropDown { static publishDropdownDiscardAll = ReactSelector('PublishDropDown ContextDropDownContents').find('button').withText('Discard all'); + static publishDropdownPublishAll = ReactSelector('PublishDropDown ContextDropDownContents').find('button').withText('Publish all'); + static async discardAll() { + const $discardAllBtn = Selector(this.publishDropdownDiscardAll); + const $confirmBtn = Selector('#neos-DiscardDialog-Confirm'); + const $acknowledgeBtn = Selector('#neos-DiscardDialog-Acknowledge'); + await t.click(this.publishDropdown) + await t.expect($discardAllBtn.exists) + .ok('"Discard all" button is not available.'); - const publishDropdownDiscardAllExists = await Selector(this.publishDropdownDiscardAll).exists; - if (publishDropdownDiscardAllExists) { - await t.click(this.publishDropdownDiscardAll); + if (await $discardAllBtn.hasAttribute('disabled')) { + return; } - const confirmButtonExists = await Selector('#neos-DiscardDialog-Confirm').exists; - if (confirmButtonExists) { - await t.click(Selector('#neos-DiscardDialog-Confirm')); + await t.click($discardAllBtn); + await t.expect($confirmBtn.exists) + .ok('Confirmation button for "Discard all" is not available.'); + await t.click($confirmBtn); + await t.expect($acknowledgeBtn.exists) + .ok('Acknowledge button for "Discard all" is not available.', { + timeout: 30000 + }); + await t.click($acknowledgeBtn); + } + + static async publishAll() { + const $publishAllBtn = Selector(this.publishDropdownPublishAll); + const $confirmBtn = Selector('#neos-PublishDialog-Confirm'); + const $acknowledgeBtn = Selector('#neos-PublishDialog-Acknowledge'); + + await t.click(this.publishDropdown) + await t.expect($publishAllBtn.exists) + .ok('"Publish all" button is not available.'); + + if (await $publishAllBtn.hasAttribute('disabled')) { + return; } - await Page.waitForIframeLoading(); + + await t.click($publishAllBtn); + await t.expect($confirmBtn.exists) + .ok('Confirmation button for "Publish all" is not available.'); + await t.click($confirmBtn); + await t.expect($acknowledgeBtn.exists) + .ok('Acknowledge button for "Publish all" is not available.', { + timeout: 30000 + }); + await t.click($acknowledgeBtn); + await t.wait(2000); } } diff --git a/Tests/IntegrationTests/start-neos-dev-instance.sh b/Tests/IntegrationTests/start-neos-dev-instance.sh index 600f27eaa9..8cc2af7761 100644 --- a/Tests/IntegrationTests/start-neos-dev-instance.sh +++ b/Tests/IntegrationTests/start-neos-dev-instance.sh @@ -40,7 +40,8 @@ dc exec -T php bash <<-'BASH' ./flow flow:cache:flush ./flow flow:cache:warmup ./flow doctrine:migrate - ./flow user:create --username=admin --password=password --first-name=John --last-name=Doe --roles=Administrator || true + ./flow user:create --username=admin --password=admin --first-name=John --last-name=Doe --roles=Administrator || true + ./flow user:create --username=editor --password=editor --first-name=Some --last-name=FooBarEditor --roles=Editor || true BASH echo "" @@ -58,17 +59,15 @@ dc exec -T php bash <<-BASH rm -rf ./TestDistribution/DistributionPackages/Neos.TestSite ln -s "../../Fixtures/1Dimension/SitePackage" ./TestDistribution/DistributionPackages/Neos.TestSite - # TODO: optimize this cd TestDistribution composer reinstall neos/test-site ./flow flow:cache:flush --force ./flow flow:cache:warmup ./flow configuration:show --path Neos.ContentRepository.contentDimensions - if ./flow site:list | grep -q 'Node name'; then - ./flow site:prune '*' - fi - ./flow site:import --package-key=Neos.TestSite + ./flow cr:setup --content-repository onedimension + ./flow site:pruneAll --content-repository onedimension --force --verbose + ./flow site:importAll --content-repository onedimension --package-key Neos.Test.OneDimension --verbose ./flow resource:publish BASH @@ -85,5 +84,5 @@ dc exec -T php bash <<-'BASH' # enable changes of the Neos.TestNodeTypes outside of the container to appear in the container via sym link to mounted volume rm -rf /usr/src/app/TestDistribution/Packages/Application/Neos.TestNodeTypes - ln -s /usr/src/neos-ui/Tests/IntegrationTests/SharedNodeTypesPackage/ /usr/src/app/TestDistribution/Packages/Application/Neos.TestNodeTypes + ln -s /usr/src/neos-ui/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes /usr/src/app/TestDistribution/Packages/Application/Neos.TestNodeTypes BASH diff --git a/Tests/IntegrationTests/utils.js b/Tests/IntegrationTests/utils.js index 98e3c5b070..cc663f04f2 100644 --- a/Tests/IntegrationTests/utils.js +++ b/Tests/IntegrationTests/utils.js @@ -1,16 +1,42 @@ -import {t, Role, ClientFunction} from 'testcafe'; +import {t, Role, ClientFunction, Selector} from 'testcafe'; import {waitForReact} from 'testcafe-react-selectors'; import {PublishDropDown, Page} from './pageModel'; +import {forEach} from "../../.yarn/releases/yarn-3.2.0"; export const subSection = name => console.log('\x1b[33m%s\x1b[0m', ' - ' + name); -const adminUrl = 'http://127.0.0.1:8081/neos'; const adminUserName = 'admin'; -const adminPassword = 'password'; +const adminPassword = 'admin'; +const editorUserName = 'editor'; +const editorPassword = 'editor'; export const getUrl = ClientFunction(() => window.location.href); -export const adminUser = Role(adminUrl, async t => { +export const adminUserOnOneDimensionTestSite = Role('http://onedimension.localhost:8081/neos', async t => { + await t + .typeText('#username', adminUserName) + .typeText('#password', adminPassword) + .click('button.neos-login-btn'); + + await t.expect(getUrl()).contains('/content'); + + await waitForReact(30000); + await Page.waitForIframeLoading(); +}, {preserveUrl: true}); + +export const editorUserOnOneDimensionTestSite = Role('http://onedimension.localhost:8081/neos', async t => { + await t + .typeText('#username', editorUserName) + .typeText('#password', editorPassword) + .click('button.neos-login-btn'); + + await t.expect(getUrl()).contains('/content'); + + await waitForReact(30000); + await Page.waitForIframeLoading(); +}, {preserveUrl: true}); + +export const adminUserOnTwoDimensionsTestSite = Role('http://twodimensions.localhost:8081/neos', async t => { await t .typeText('#username', adminUserName) .typeText('#password', adminPassword) @@ -35,8 +61,69 @@ export async function checkPropTypes() { } export async function beforeEach(t) { - await t.useRole(adminUser); + await t.useRole(adminUserOnOneDimensionTestSite); await waitForReact(30000); await PublishDropDown.discardAll(); + if (await Selector('#neos-DiscardDialog-Acknowledge').exists) { + await t.click(Selector('#neos-DiscardDialog-Acknowledge')); + } await Page.goToPage('Home'); } + +// This is a workaround for the fact that the contenteditable element is not directly selectable +// for more information see https://testcafe.io/documentation/402688/reference/test-api/testcontroller/selecteditablecontent +export async function typeTextInline(t, selector, text, textType, switchToIframe = true) { + await waitForReact(30000); + await Page.waitForIframeLoading(); + + const textTypeToTagMap = { + paragraph: 'p', + heading1: 'h1', + heading2: 'h2', + heading3: 'h3', + heading4: 'h4' + }; + + if (!Object.keys(textTypeToTagMap).includes(textType)) { + textType = 'paragraph'; + } + + const tagName = textTypeToTagMap[textType] || ''; + try { + const contentIframeSelector = Selector('[name="neos-content-main"]', {timeout: 2000}); + + if (switchToIframe) { + await t.switchToIframe(contentIframeSelector); + } + + await t.eval(() => { + const element = window.document.querySelector(selector); + const editor = element.closest('.ck-editor__editable'); + const content = tagName !== '' ? `<${tagName}>${text}` : text; + editor.ckeditorInstance.data.set(content); + }, + {dependencies: {selector, text, tagName}} + ); + } catch (e) { + // console.log(e); + } +} + +export async function clearInlineText(t, selector, switchToIframe = true) { + await waitForReact(30000); + await Page.waitForIframeLoading(); + + try { + const contentIframeSelector = Selector('[name="neos-content-main"]', {timeout: 2000}); + const lastEditableElement = selector; + if (switchToIframe) { + await t.switchToIframe(contentIframeSelector); + } + + await t + .selectEditableContent(lastEditableElement, lastEditableElement) + .pressKey('ctrl+a delete'); + } catch (e) { + // console.log(e); + } +} diff --git a/Tests/Unit/CreationDialogNodeTypePostprocessorTest.php b/Tests/Unit/CreationDialogNodeTypePostprocessorTest.php new file mode 100644 index 0000000000..76563cb57d --- /dev/null +++ b/Tests/Unit/CreationDialogNodeTypePostprocessorTest.php @@ -0,0 +1,503 @@ + [ + 'nodeTypeDefinition' => <<<'YAML' + references: + someReferences: + ui: + showInCreationDialog: true + YAML, + 'expectedCreationDialog' => <<<'YAML' + elements: + someReferences: + type: references + ui: + editor: ReferencesEditor + YAML + ]; + + yield 'singular reference' => [ + 'nodeTypeDefinition' => <<<'YAML' + references: + someReference: + constraints: + maxItems: 1 + ui: + showInCreationDialog: true + YAML, + 'expectedCreationDialog' => <<<'YAML' + elements: + someReference: + type: reference + ui: + editor: SingularReferenceEditor + YAML + ]; + } + + /** + * @test + * @dataProvider examples + */ + public function processExamples(string $nodeTypeDefinition, string $expectedCreationDialog) + { + $configuration = array_merge([ + 'references' => [], + 'properties' => [] + ], Yaml::parse($nodeTypeDefinition)); + + $dataTypesDefaultConfiguration = [ + 'reference' => [ + 'editor' => 'SingularReferenceEditor', + ], + 'references' => [ + 'editor' => 'ReferencesEditor', + ], + ]; + + $result = $this->processConfigurationFully($configuration, $dataTypesDefaultConfiguration, []); + + self::assertEquals(Yaml::parse($expectedCreationDialog), $result['ui']['creationDialog'] ?? null); + } + + /** + * promoted elements (showInCreationDialog: true) + * + * @test + */ + public function processCopiesInspectorConfigurationToCreationDialogElements(): void + { + $configuration = [ + 'references' => [], + 'properties' => [ + 'somePropertyName' => [ + 'ui' => [ + 'showInCreationDialog' => true, + 'inspector' => [ + 'position' => 123, + 'editor' => 'Some\Editor', + 'editorOptions' => ['some' => 'option'], + 'hidden' => 'ClientEval:false' + ], + ], + 'validation' => [ + 'Neos.Neos/Validation/NotEmptyValidator' => [], + 'Neos.Neos/Validation/StringLengthValidator' => [ + 'minimum' => 1, + 'maximum' => 255, + ] + ], + ], + ], + ]; + + $expectedElements = [ + 'somePropertyName' => [ + 'type' => 'string', + 'ui' => [ + 'label' => 'somePropertyName', + 'hidden' => 'ClientEval:false', + 'editor' => 'Some\Editor', + 'editorOptions' => ['some' => 'option'], + ], + 'validation' => [ + 'Neos.Neos/Validation/NotEmptyValidator' => [], + 'Neos.Neos/Validation/StringLengthValidator' => [ + 'minimum' => 1, + 'maximum' => 255, + ] + ], + 'position' => 123, + ], + ]; + + $result = $this->processConfigurationLegacyOnlyOnce($configuration, [], []); + + self::assertSame($expectedElements, $result['ui']['creationDialog']['elements']); + } + + /** + * @test + */ + public function processDoesNotCreateEmptyCreationDialogs(): void + { + $configuration = [ + 'properties' => [ + 'somePropertyName' => [ + 'ui' => [ + 'inspector' => [ + 'editor' => 'Some\Editor', + 'editorOptions' => ['some' => 'option'], + ], + ], + ], + ], + ]; + + $result = $this->processConfigurationLegacyOnlyOnce($configuration, [], []); + + self::assertSame($configuration, $result); + + } + + /** + * promoted elements (showInCreationDialog: true) + * + * @test + */ + public function processRespectsDataTypeDefaultConfiguration(): void + { + $configuration = [ + 'properties' => [ + 'somePropertyName' => [ + 'type' => 'SomeType', + 'ui' => [ + 'label' => 'Some Label', + 'showInCreationDialog' => true, + 'inspector' => [ + 'editorOptions' => ['some' => 'option'], + ], + ], + ], + ], + ]; + $dataTypesDefaultConfiguration = [ + 'SomeType' => [ + 'editor' => 'Some\Default\Editor', + 'editorOptions' => [ + 'some' => 'defaultOption', + 'someDefault' => 'option', + ] + ] + ]; + + $expectedElements = [ + 'somePropertyName' => [ + 'type' => 'SomeType', + 'ui' => [ + 'label' => 'Some Label', + 'editor' => 'Some\Default\Editor', + 'editorOptions' => ['some' => 'option', 'someDefault' => 'option'], + ], + ], + ]; + + $result = $this->processConfigurationLegacyOnlyOnce($configuration, $dataTypesDefaultConfiguration, []); + + self::assertEquals($expectedElements, $result['ui']['creationDialog']['elements']); + } + + /** + * promoted elements (showInCreationDialog: true) + * + * @test + */ + public function processRespectsEditorDefaultConfiguration(): void + { + $configuration = [ + 'properties' => [ + 'somePropertyName' => [ + 'type' => 'SomeType', + 'ui' => [ + 'showInCreationDialog' => true, + 'inspector' => [ + 'editorOptions' => ['some' => 'option'], + ], + ], + ], + ], + ]; + $editorDefaultConfiguration = [ + 'Some\Editor' => [ + 'editorOptions' => [ + 'some' => 'editorDefault', + 'someDefault' => 'fromEditor', + 'someEditorDefault' => 'fromEditor', + ] + ] + ]; + $dataTypesDefaultConfiguration = [ + 'SomeType' => [ + 'editor' => 'Some\Editor', + 'editorOptions' => [ + 'some' => 'defaultOption', + 'someDefault' => 'fromDataType', + ] + ] + ]; + + $expectedElements = [ + 'somePropertyName' => [ + 'type' => 'SomeType', + 'ui' => [ + 'label' => 'somePropertyName', + 'editor' => 'Some\Editor', + 'editorOptions' => ['some' => 'option', 'someDefault' => 'fromDataType', 'someEditorDefault' => 'fromEditor'], + ], + ], + ]; + + $result = $this->processConfigurationLegacyOnlyOnce($configuration, $dataTypesDefaultConfiguration, $editorDefaultConfiguration); + + self::assertEquals($expectedElements, $result['ui']['creationDialog']['elements']); + } + + /** + * default editor + * + * @test + */ + public function processConvertsCreationDialogConfiguration(): void + { + $configuration = [ + 'references' => [], + 'properties' => [], + 'ui' => [ + 'creationDialog' => [ + 'elements' => [ + 'elementWithoutType' => [ + 'ui' => [ + 'label' => 'Some Label' + ] + ], + 'elementWithUnknownType' => [ + 'type' => 'TypeWithoutDataTypeConfig', + 'ui' => [ + 'label' => 'Some Label', + 'editor' => 'EditorFromPropertyConfig', + ] + ], + 'elementWithEditorFromDataTypeConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'value' => 'fromPropertyConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithEditorFromDataTypeConfigWithoutUiConfig' => [ + 'type' => 'TypeWithDataTypeConfig' + ], + 'elementWithOverriddenEditorConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'editor' => 'EditorFromPropertyConfig', + 'value' => 'fromPropertyConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithOverriddenEditorConfigAndEditorDefaultConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'editor' => 'EditorWithDefaultConfig', + 'value' => 'fromPropertyConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithEditorDefaultConfig' => [ + 'type' => 'TypeWithDefaultEditorConfig', + 'ui' => [ + 'value' => 'fromPropertyConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithOverriddenEditorConfigAndEditorDefaultConfig2' => [ + 'type' => 'TypeWithDefaultEditorConfig', + 'ui' => [ + 'editor' => 'EditorWithoutDefaultConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithOverriddenEditorConfigAndEditorDefaultConfig3' => [ + 'type' => 'TypeWithDefaultEditorConfig2', + 'ui' => [ + 'editor' => 'EditorWithDefaultConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + ], + ], + ], + ]; + + $editorDefaultConfiguration = [ + 'EditorWithDefaultConfig' => [ + 'value' => 'fromEditorDefaultConfig', + 'editorDefaultValue' => 'fromEditorDefaultConfig', + ], + ]; + + $dataTypesDefaultConfiguration = [ + 'TypeWithDataTypeConfig' => [ + 'editor' => 'EditorFromDataTypeConfig', + 'value' => 'fromDataTypeConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + ], + 'TypeWithDefaultEditorConfig' => [ + 'editor' => 'EditorWithDefaultConfig', + 'value' => 'fromDataTypeConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + ], + 'TypeWithDefaultEditorConfig2' => [ + 'editor' => 'EditorWithDefaultConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + ], + ]; + + $expectedResult = [ + 'references' => [], + 'properties' => [], + 'ui' => [ + 'creationDialog' => [ + 'elements' => [ + 'elementWithoutType' => [ + 'ui' => [ + 'label' => 'Some Label' + ] + ], + 'elementWithUnknownType' => [ + 'type' => 'TypeWithoutDataTypeConfig', + 'ui' => [ + 'label' => 'Some Label', + 'editor' => 'EditorFromPropertyConfig', + ] + ], + 'elementWithEditorFromDataTypeConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'editor' => 'EditorFromDataTypeConfig', + 'value' => 'fromPropertyConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithEditorFromDataTypeConfigWithoutUiConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'editor' => 'EditorFromDataTypeConfig', + 'value' => 'fromDataTypeConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + ] + ], + 'elementWithOverriddenEditorConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'editor' => 'EditorFromPropertyConfig', + 'value' => 'fromPropertyConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithOverriddenEditorConfigAndEditorDefaultConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'value' => 'fromPropertyConfig', + 'editorDefaultValue' => 'fromEditorDefaultConfig', + 'editor' => 'EditorWithDefaultConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithEditorDefaultConfig' => [ + 'type' => 'TypeWithDefaultEditorConfig', + 'ui' => [ + 'value' => 'fromPropertyConfig', + 'editorDefaultValue' => 'fromEditorDefaultConfig', + 'editor' => 'EditorWithDefaultConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithOverriddenEditorConfigAndEditorDefaultConfig2' => [ + 'type' => 'TypeWithDefaultEditorConfig', + 'ui' => [ + 'editor' => 'EditorWithoutDefaultConfig', + 'value' => 'fromDataTypeConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithOverriddenEditorConfigAndEditorDefaultConfig3' => [ + 'type' => 'TypeWithDefaultEditorConfig2', + 'ui' => [ + 'value' => 'fromEditorDefaultConfig', + 'editorDefaultValue' => 'fromEditorDefaultConfig', + 'editor' => 'EditorWithDefaultConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + ], + ], + ], + ]; + + self::assertSame($expectedResult, $this->processConfigurationLegacyOnlyOnce($configuration, $dataTypesDefaultConfiguration, $editorDefaultConfiguration)); + } + + /** + * @test + */ + public function processDoesNotThrowExceptionIfNoCreationDialogEditorCanBeResolved(): void + { + $configuration = [ + 'references' => [], + 'properties' => [], + 'ui' => [ + 'creationDialog' => [ + 'elements' => [ + 'someElement' => [ + 'type' => 'string', + 'ui' => ['label' => 'Foo'] + ], + ], + ], + ], + ]; + + self::assertSame($configuration, $this->processConfigurationFully($configuration, [], [])); + } + + private function processConfigurationFully(array $configuration, array $dataTypesDefaultConfiguration, array $editorDefaultConfiguration): array + { + $mockNodeType = new NodeType(NodeTypeName::fromString('Some.NodeType:Name'), [], []); + + $firstProcessor = new DefaultPropertyEditorPostprocessor(); + $this->inject($firstProcessor, 'dataTypesDefaultConfiguration', $dataTypesDefaultConfiguration); + $this->inject($firstProcessor, 'editorDefaultConfiguration', $editorDefaultConfiguration); + + $firstProcessor->process($mockNodeType, $configuration, []); + + + $secondProcessor = new CreationDialogNodeTypePostprocessor(); + $this->inject($secondProcessor, 'dataTypesDefaultConfiguration', $dataTypesDefaultConfiguration); + $this->inject($secondProcessor, 'editorDefaultConfiguration', $editorDefaultConfiguration); + + $secondProcessor->process($mockNodeType, $configuration, []); + + return $configuration; + } + + private function processConfigurationLegacyOnlyOnce(array $configuration, array $dataTypesDefaultConfiguration, array $editorDefaultConfiguration): array + { + $mockNodeType = new NodeType(NodeTypeName::fromString('Some.NodeType:Name'), [], []); + + $postprocessor = new CreationDialogNodeTypePostprocessor(); + $this->inject($postprocessor, 'dataTypesDefaultConfiguration', $dataTypesDefaultConfiguration); + $this->inject($postprocessor, 'editorDefaultConfiguration', $editorDefaultConfiguration); + + $postprocessor->process($mockNodeType, $configuration, []); + return $configuration; + } +} diff --git a/composer.json b/composer.json index 43e50f09e2..61df71487a 100644 --- a/composer.json +++ b/composer.json @@ -6,9 +6,12 @@ "GPL-3.0-or-later" ], "require": { - "neos/neos": "^8.3.0", + "neos/neos": "^9.0.0 || 9.0.x-dev", "neos/neos-ui-compiled": "self.version" }, + "scripts": { + "lint:phpstan": "../../../bin/phpstan analyse" + }, "autoload": { "psr-4": { "Neos\\Neos\\Ui\\": "Classes" diff --git a/package.json b/package.json index 9e88f177c6..639c971b78 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,7 @@ "moment": "^2.20.1", "vfile-message": "^2.0.2", "isemail@3.2.0": "patch:isemail@npm:3.2.0#./patches/isemail-npm-3.2.0-browserified.patch", - "react-codemirror2@7.2.1": "patch:react-codemirror2@npm:7.2.1#./patches/react-codemirror2-npm-7.2.1-browserified.patch", - "@ckeditor/ckeditor5-engine@^16.0.0": "patch:@ckeditor/ckeditor5-engine@npm:16.0.0#./patches/@ckeditor-ckeditor5-engine-npm-16.0.0-placeholder.patch" + "react-codemirror2@7.2.1": "patch:react-codemirror2@npm:7.2.1#./patches/react-codemirror2-npm-7.2.1-browserified.patch" }, "scripts": { "lint": "tsc --noemit && stylelint 'packages/*/src/**/*.css' && yarn eslint 'packages/*/src/**/*.{js,jsx,ts,tsx}'", @@ -25,6 +24,7 @@ "@neos-project/eslint-config-neos": "^2.6.1", "@typescript-eslint/eslint-plugin": "^5.44.0", "@typescript-eslint/parser": "^5.44.0", + "cross-fetch": "^4.0.0", "editorconfig-checker": "^4.0.2", "esbuild": "~0.17.0", "eslint": "^8.27.0", diff --git a/packages/framework-observable-react/README.md b/packages/framework-observable-react/README.md new file mode 100644 index 0000000000..82b3625e2c --- /dev/null +++ b/packages/framework-observable-react/README.md @@ -0,0 +1,120 @@ +# @neos-project/framework-observable-react + +> React bindings for @neos-project/framework-observable + +This package provides a set of React [hooks](https://react.dev/reference/react/hooks) to let components interact with `Observable`s. + +## API + +### `useLatestValueFrom` + +```typescript +// Without default value: +function useLatestValueFrom(observable$: Observable): null | V; + +// With default value: +function useLatestValueFrom( + observable$: Observable, + defaultValue: D +): D | V; +``` + +`useLatestValueFrom` is a way to bind a react component the latest value emitted from an `Observable`. + +#### Parameters + +| Name | Description | +| ------------------------- | ---------------------------------------------------------------------------------------------- | +| `observable$` | The `Observable` to subscribe to | +| `defaultValue` (optional) | The value to default for when `observable$` hasn't emitted any values yet (defaults to `null`) | + +#### Return Value + +This hook returns the latest value from the provided `observable$`. If no value has been emitted from the observable yet, it returns `defaultValue` which itself defaults to `null`. + +#### Example + +This component will display the amount of seconds that have passed since it was first mounted: + +```typescript +const clock$ = createObservable((next) => { + let i = 1; + const interval = setInterval(() => { + next(i++); + }, 1000); + + return () => clearInterval(interval); +}); + +const MyComponent = () => { + const seconds = useLatestValueFrom(clock$, 0); + + return
    {seconds} seconds passed
    ; +}; +``` + +You can combine this with `React.useMemo`, if you wish to create an ad-hoc observable: + +```typescript +const MyComponent = (props) => { + const beats = useLatestValueFrom( + React.useMemo( + () => + createObservable((next) => { + let i = 1; + const interval = setInterval(() => { + next(i++); + }, props.millisecondsPerBeat); + + return () => clearInterval(interval); + }), + [props.millisecondsPerBeat] + ), + 0 + ); + + return
    {beats} beats passed
    ; +}; +``` + +### `useLatestState` + +```typescript +function useLatestState(state$: State): V; +``` + +`useLatestState` subscribes to a given state observable and keeps track of its latest value. + +#### Parameters + +| Name | Description | +| -------- | --------------------------------------- | +| `state$` | The `State` observable to keep track of | + +#### Return Value + +This hook returns the latest value from the given `State` observable. Initially it contains the current value of the `State` at the moment the component was first mounted. + +#### Example + +```typescript +const count$ = createState(0); + +const MyComponent = () => { + const count = useLatestState(count$); + const handleInc = React.useCallback(() => { + count$.update((count) => count + 1); + }, []); + const handleDec = React.useCallback(() => { + count$.update((count) => count - 1); + }, []); + + return ( +
    +
    Count {count}
    + + +
    + ); +}; +``` diff --git a/packages/framework-observable-react/package.json b/packages/framework-observable-react/package.json new file mode 100644 index 0000000000..ae114ecebb --- /dev/null +++ b/packages/framework-observable-react/package.json @@ -0,0 +1,12 @@ +{ + "name": "@neos-project/framework-observable-react", + "version": "", + "description": "React bindings for @neos-project/framework-observable", + "private": true, + "main": "./src/index.ts", + "dependencies": { + "@neos-project/framework-observable": "workspace:*", + "react": "^16.12.0" + }, + "license": "GNU GPLv3" +} diff --git a/packages/framework-observable-react/src/index.ts b/packages/framework-observable-react/src/index.ts new file mode 100644 index 0000000000..1ddd85b034 --- /dev/null +++ b/packages/framework-observable-react/src/index.ts @@ -0,0 +1,11 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {useLatestState} from './useLatestState'; +export {useLatestValueFrom} from './useLatestValueFrom'; diff --git a/packages/framework-observable-react/src/useLatestState.ts b/packages/framework-observable-react/src/useLatestState.ts new file mode 100644 index 0000000000..d7b23bad5c --- /dev/null +++ b/packages/framework-observable-react/src/useLatestState.ts @@ -0,0 +1,16 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import type {State} from '@neos-project/framework-observable'; + +import {useLatestValueFrom} from './useLatestValueFrom'; + +export function useLatestState(state$: State) { + return useLatestValueFrom(state$, state$.current); +} diff --git a/packages/framework-observable-react/src/useLatestValueFrom.ts b/packages/framework-observable-react/src/useLatestValueFrom.ts new file mode 100644 index 0000000000..9158face7d --- /dev/null +++ b/packages/framework-observable-react/src/useLatestValueFrom.ts @@ -0,0 +1,41 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import React from 'react'; + +import type {Observable} from '@neos-project/framework-observable'; + +export function useLatestValueFrom(observable$: Observable): null | V; +export function useLatestValueFrom( + observable$: Observable, + defaultValue: D +): D | V; + +export function useLatestValueFrom( + observable$: Observable, + defaultValue?: D +) { + const [value, setValue] = React.useState( + defaultValue ?? null + ); + + React.useEffect(() => { + const subscription = observable$.subscribe({ + next: (incomingValue) => { + if (incomingValue !== value) { + setValue(incomingValue); + } + } + }); + + return () => subscription.unsubscribe(); + }, [observable$]); + + return value; +} diff --git a/packages/framework-observable/README.md b/packages/framework-observable/README.md new file mode 100644 index 0000000000..c350482578 --- /dev/null +++ b/packages/framework-observable/README.md @@ -0,0 +1,152 @@ +# @neos-project/framework-observable + +> Observable pattern implementation for the Neos UI + +> [!NOTE] +> This package implements a pattern for which there is a WICG proposal: +> https://github.com/WICG/observable +> +> It is therefore likely that future versions of this package will use the web-native `Observable` primitive under the hood. + +## API + +### Observables + +An `Observable` represents a sequence of values that can be *observed* from the outside. This is a powerful abstraction that allows to encapsule all kinds of value streams like: + +- (DOM) Events +- Timeouts & Intervals +- Async operations & Promises +- Websockets +- etc. + +An `Observable` can be created using the `createObservable` function like this: + +```typescript +const numbers$ = createObservable((next) => { + next(1); + next(2); + next(3); +}); +``` + +> [!NOTE] +> Suffixing variable names with `$` is a common naming convention to signify that a variable represents an observable. + +Here, the `numbers$` observable represents the sequence of the numbers 1, 2 and 3. This observable can be subscribed to thusly: + +```typescript +numbers$.subscribe((value) => { + console.log(value); +}); +``` + +Because the `numbers$` observable emits its values immediately, the above subscription will immediately log: +``` +1 +2 +3 +``` + +An additional subscription would also immediately receive all 3 values. By default, oberservables are *lazy* and *single-cast*. This means, values are generated exclusively for each subscription, and the generation starts exactly when a subscriber is registered. + +The usefulness of observables becomes more apparent when we introduce some asynchrony: +```typescript +const timedNumbers$ = createObservable((next) => { + let i = 1; + const interval = setInterval(() => { + next(i++); + }, 2000); + + return () => clearInterval(interval); +}); +``` + +This `timedNumbers$` observable will emit a new value every two seconds. This time, the callback used to facilitate the observable returns a function: +```typescript +// .. +return () => clearInterval(interval); +// .. +``` + +This function will be called when a subscription is cancelled. This is a way for observables to clean up after themselves. + +If we now subscribe to `timedNumbers$` like this: +```typescript +const subscription = timedNumbers$.subscribe((value) => { + console.log(value); +}); +``` + +The following values will be logged to the console: +``` +1 (After 2 seconds) +2 (After 4 seconds) +3 (After 6 seconds) +4 (After 8 seconds) +... +``` + +This will go on forever, unless we call the `unsubscribe` on our `subscription` which has been the return value we've saved from `timedNumber$.subscribe(...)`. When we call `unsubscribe`, the cleanup function of the `timedNumbers$` observable will be called and so the interval will be cleared: +```typescript +subscription.unsubscribe(); +``` + +That's all there is to it. With this small set of tools, `Observable`s can be used to encapsule all kinds of synchronous or asynchronous value streams. + +They can be created from a Promise: +```typescript +async function someLongRunningOperation() { + // ... +} + +const fromPromise$ = createObservable((next) => { + someLongRunningOperation().then(next); +}); +``` + +Or DOM events: +```typescript +const clicks$ = createObservable((next) => { + const button = document.querySelector('button'); + button.addEventListener('click', next); + return () => button.removeEventListener('click', next); +}); +``` + +And there are many, many more possibilities. + +### State + +A `State` is a special `Observable` that can track a value over time. `State`s can be created using the `createState` function like this: + +```typescript +const count$ = createState(0); +``` + +The `count$` state is now set to `0`. Unlike regular observables, a `State` instance can be queried for its current value: +```typescript +console.log(count$.current); // output: 0 +``` + +Each `State` instance has an `update` method that can be used to push new values to the state observable. It takes a callback that receives the current value as its first paramater and returns the new value: + +```typescript +count$.update((value) => value + 1); + +console.log(count$.current); // output: 1 +``` + +When a new subscriber is registered to a `State` instance, that subscriber immediately receives the current value: +```typescript +const count$ = createState(0); +count$.update((value) => value + 1); // nothing is logged, nobody has subscribed yet +count$.update((value) => value + 1); // nothing is logged, nobody has subscribed yet +count$.update((value) => value + 1); // nothing is logged, nobody has subscribed yet + +count$.subscribe((value) => console.log(value)); // immediately logs: 3 + +count$.update((value) => value + 1); // logs: 4 +``` + +Unlike regular `Observable`s, `State`s are multi-cast. This means that all subscribers receive updates at the same time, and every subscriber only receives updates that are published after the subscription has been registered. diff --git a/packages/framework-observable/package.json b/packages/framework-observable/package.json new file mode 100644 index 0000000000..d79d8c5fd9 --- /dev/null +++ b/packages/framework-observable/package.json @@ -0,0 +1,8 @@ +{ + "name": "@neos-project/framework-observable", + "version": "", + "description": "Observable pattern implementation for the Neos UI", + "private": true, + "main": "./src/index.ts", + "license": "GNU GPLv3" +} diff --git a/packages/framework-observable/src/Observable.spec.ts b/packages/framework-observable/src/Observable.spec.ts new file mode 100644 index 0000000000..04dfff09cd --- /dev/null +++ b/packages/framework-observable/src/Observable.spec.ts @@ -0,0 +1,94 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import {createObservable} from './Observable'; + +describe('Observable', () => { + test('emit some values and subscribe', () => { + const observable$ = createObservable((next) => { + next(1); + next(2); + next(3); + }); + const subscriber = { + next: jest.fn() + }; + + observable$.subscribe(subscriber); + + expect(subscriber.next).toHaveBeenCalledTimes(3); + expect(subscriber.next).toHaveBeenNthCalledWith(1, 1); + expect(subscriber.next).toHaveBeenNthCalledWith(2, 2); + expect(subscriber.next).toHaveBeenNthCalledWith(3, 3); + }); + + test('emit some values and subscribe a couple of times', () => { + const observable$ = createObservable((next) => { + next(1); + next(2); + next(3); + }); + const subscriber1 = { + next: jest.fn() + }; + const subscriber2 = { + next: jest.fn() + }; + const subscriber3 = { + next: jest.fn() + }; + + observable$.subscribe(subscriber1); + observable$.subscribe(subscriber2); + observable$.subscribe(subscriber3); + + expect(subscriber1.next).toHaveBeenCalledTimes(3); + expect(subscriber1.next).toHaveBeenNthCalledWith(1, 1); + expect(subscriber1.next).toHaveBeenNthCalledWith(2, 2); + expect(subscriber1.next).toHaveBeenNthCalledWith(3, 3); + + expect(subscriber2.next).toHaveBeenCalledTimes(3); + expect(subscriber2.next).toHaveBeenNthCalledWith(1, 1); + expect(subscriber2.next).toHaveBeenNthCalledWith(2, 2); + expect(subscriber2.next).toHaveBeenNthCalledWith(3, 3); + + expect(subscriber3.next).toHaveBeenCalledTimes(3); + expect(subscriber3.next).toHaveBeenNthCalledWith(1, 1); + expect(subscriber3.next).toHaveBeenNthCalledWith(2, 2); + expect(subscriber3.next).toHaveBeenNthCalledWith(3, 3); + }); + + test('emit no values, subscribe and unsubscribe', () => { + const unsubscribe = jest.fn(); + const observable$ = createObservable(() => { + return unsubscribe; + }); + const subscriber = { + next: jest.fn() + }; + + const subscription = observable$.subscribe(subscriber); + subscription.unsubscribe(); + + expect(subscriber.next).toHaveBeenCalledTimes(0); + expect(unsubscribe).toHaveBeenCalledTimes(1); + }); + + test('emit no values, subscribe and unsubscribe with void observer', () => { + const observable$ = createObservable(() => {}); + const subscriber = { + next: jest.fn() + }; + + const subscription = observable$.subscribe(subscriber); + + expect(() => subscription.unsubscribe()).not.toThrow(); + expect(subscriber.next).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/framework-observable/src/Observable.ts b/packages/framework-observable/src/Observable.ts new file mode 100644 index 0000000000..482fb87a9d --- /dev/null +++ b/packages/framework-observable/src/Observable.ts @@ -0,0 +1,51 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import type {Subscriber} from './Subscriber'; +import type {Subscription} from './Subscription'; +import type {Observer} from './Observer'; + +/** + * An Observable emits values over time. You can attach a subscriber to it + * using the Observable's `subscribe` method, or you can perform operations + * producing new Observables via its `pipe` method. + */ +export interface Observable { + subscribe: (subscriber: Subscriber) => Subscription; +} + +/** + * An ObservablePipeOperation is a function that takes an observable and + * returns a new observable. It can be passed to any Observable's `pipe` + * method. + */ +export interface ObservablePipeOperation { + (observable: Observable): Observable; +} + +/** + * Creates an Observable from the given Observer. + */ +export function createObservable(observer: Observer): Observable { + const observable: Observable = { + subscribe(subscriber) { + return Object.freeze({ + unsubscribe: observer( + subscriber.next, + subscriber.error ?? noop + ) ?? noop + }); + } + }; + + return Object.freeze(observable); +} + +function noop() { +} diff --git a/packages/framework-observable/src/Observer.ts b/packages/framework-observable/src/Observer.ts new file mode 100644 index 0000000000..b9c145b0ac --- /dev/null +++ b/packages/framework-observable/src/Observer.ts @@ -0,0 +1,19 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +/** + * An Observer is a function that emits values via its `next` callback. It can + * return a function that handles all logic that must be performed when a + * Subscription is cancelled (e.g. clearTimeout or similar cancellation + * effects). + */ +export interface Observer { + (next: (value: V) => void, fail: (error: any) => void): void | (() => void); +} diff --git a/packages/framework-observable/src/State.spec.ts b/packages/framework-observable/src/State.spec.ts new file mode 100644 index 0000000000..b573a6c880 --- /dev/null +++ b/packages/framework-observable/src/State.spec.ts @@ -0,0 +1,67 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import {createState} from './State'; + +describe('State', () => { + test('get current value', () => { + const state$ = createState(0); + + expect(state$.current).toBe(0); + + state$.update((value) => value + 1); + expect(state$.current).toBe(1); + + state$.update((value) => value + 1); + expect(state$.current).toBe(2); + + state$.update((value) => value + 1); + expect(state$.current).toBe(3); + }); + + test('subscribe to state updates: subscriber receives current value immediately', () => { + const state$ = createState(0); + const subscriber1 = { + next: jest.fn() + }; + const subscriber2 = { + next: jest.fn() + }; + + state$.subscribe(subscriber1); + expect(subscriber1.next).toHaveBeenCalledTimes(1); + expect(subscriber1.next).toHaveBeenNthCalledWith(1, 0); + + state$.update((value) => value + 1); + state$.update((value) => value + 1); + state$.update((value) => value + 1); + + state$.subscribe(subscriber2); + expect(subscriber2.next).toHaveBeenCalledTimes(1); + expect(subscriber2.next).toHaveBeenNthCalledWith(1, 3); + }); + + test('subscribe to state updates: subscriber receives all updates', () => { + const state$ = createState(0); + const subscriber = { + next: jest.fn() + }; + + state$.subscribe(subscriber); + state$.update((value) => value + 1); + state$.update((value) => value + 1); + state$.update((value) => value + 1); + + expect(subscriber.next).toHaveBeenCalledTimes(4); + expect(subscriber.next).toHaveBeenNthCalledWith(1, 0); + expect(subscriber.next).toHaveBeenNthCalledWith(2, 1); + expect(subscriber.next).toHaveBeenNthCalledWith(3, 2); + expect(subscriber.next).toHaveBeenNthCalledWith(4, 3); + }); +}); diff --git a/packages/framework-observable/src/State.ts b/packages/framework-observable/src/State.ts new file mode 100644 index 0000000000..c0d63a9f2f --- /dev/null +++ b/packages/framework-observable/src/State.ts @@ -0,0 +1,61 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import {createObservable, Observable} from './Observable'; + +/** + * A State is a special kind of Observable that keeps track of a value over + * time. + * + * It has a public readonly `current` property that allows you to ask for + * its current value at any point in time. A new subscriber to the State + * Observable will also immediately receive the current value at the time of + * subscription. + * + * Via the `update` method, a State's value can be modified. When called, + * Subscribers to the state are immediately informed about the new value. + */ +export interface State extends Observable { + readonly current: V; + update: (updateFn: (current: V) => V) => void; +} + +/** + * Creates a new State with the given initial value. + */ +export function createState(initialValue: V): State { + let currentState = initialValue; + const listeners = new Set<(value: V) => void>(); + const state: State = { + ...createObservable((next) => { + listeners.add(next); + next(currentState); + + return () => listeners.delete(next); + }), + + get current() { + return currentState; + }, + + update(updateFn) { + const nextState = updateFn(currentState); + + if (currentState !== nextState) { + currentState = nextState; + + for (const next of listeners) { + next(currentState); + } + } + } + }; + + return Object.freeze(state); +} diff --git a/packages/framework-observable/src/Subscriber.ts b/packages/framework-observable/src/Subscriber.ts new file mode 100644 index 0000000000..ac8f5c963e --- /dev/null +++ b/packages/framework-observable/src/Subscriber.ts @@ -0,0 +1,19 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +/** + * A Subscriber can be attached to an Observable. It receives values from the + * Observable in its `next` callback function. It may also provide an optional + * `error` callback, that will only be called if the Observable emits an Error. + */ +export interface Subscriber { + next: (value: V) => void; + error?: (error: Error) => void; +} diff --git a/packages/framework-observable/src/Subscription.ts b/packages/framework-observable/src/Subscription.ts new file mode 100644 index 0000000000..3821d0d04a --- /dev/null +++ b/packages/framework-observable/src/Subscription.ts @@ -0,0 +1,19 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +/** + * When attaching a Subscriber to an Observable, a Subscription is returned. + * The `unsubscribe` method of the Subscription allows you to detach the + * Subscriber from the Observable again, after which the Subscriber no longer + * receives any values emitted from the Observable. + */ +export interface Subscription { + unsubscribe: () => void; +} diff --git a/packages/framework-observable/src/index.ts b/packages/framework-observable/src/index.ts new file mode 100644 index 0000000000..b84ad0927e --- /dev/null +++ b/packages/framework-observable/src/index.ts @@ -0,0 +1,14 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export type {Observable} from './Observable'; +export {createObservable} from './Observable'; + +export type {State} from './State'; +export {createState} from './State'; diff --git a/packages/jest-preset-neos-ui/src/setupBrowserEnv.js b/packages/jest-preset-neos-ui/src/setupBrowserEnv.js index 792b4ae476..c1d08efc5c 100644 --- a/packages/jest-preset-neos-ui/src/setupBrowserEnv.js +++ b/packages/jest-preset-neos-ui/src/setupBrowserEnv.js @@ -1,6 +1,5 @@ import 'regenerator-runtime/runtime'; import browserEnv from 'browser-env'; +import 'cross-fetch/polyfill'; browserEnv(); - -window.fetch = () => Promise.resolve(null); diff --git a/packages/neos-ts-interfaces/package.json b/packages/neos-ts-interfaces/package.json index d6e993fee1..4d24fcb673 100644 --- a/packages/neos-ts-interfaces/package.json +++ b/packages/neos-ts-interfaces/package.json @@ -4,6 +4,9 @@ "description": "Neos domain-related TypeScript interfaces", "private": true, "main": "src/index.ts", + "dependencies": { + "@neos-project/neos-ui-i18n": "workspace:*" + }, "devDependencies": { "@neos-project/jest-preset-neos-ui": "workspace:*", "typescript": "^4.6.4" diff --git a/packages/neos-ts-interfaces/src/index.ts b/packages/neos-ts-interfaces/src/index.ts index 9bd1fb005e..efcaae26db 100644 --- a/packages/neos-ts-interfaces/src/index.ts +++ b/packages/neos-ts-interfaces/src/index.ts @@ -1,3 +1,5 @@ +import type {I18nRegistry} from '@neos-project/neos-ui-i18n'; + export type NodeContextPath = string; export type FusionPath = string; export type NodeTypeName = string; @@ -101,6 +103,11 @@ export enum SelectionModeTypes { RANGE_SELECT = 'RANGE_SELECT' } +export enum WorkspaceStatus { + UP_TO_DATE = 'UP_TO_DATE', + OUTDATED = 'OUTDATED' +} + export interface ValidatorConfiguration { [propName: string]: any; } @@ -137,7 +144,26 @@ export interface PropertyConfiguration { [propName: string]: ValidatorConfiguration | undefined; }; } - +export interface ReferencesConfiguration { + type?: string; + ui?: { + label?: string; + reloadIfChanged?: boolean; + inspector?: { + hidden?: boolean; + editor?: string; + editorOptions?: { + [propName: string]: any; + } + group?: string; + position?: number | string; + }; + help?: { + message?: string; + thumbnail?: string; + }; + }; +} export interface NodeType { name?: string; superTypes: { @@ -210,6 +236,9 @@ export interface NodeType { properties?: { [propName: string]: PropertyConfiguration | undefined; }; + references?: { + [referenceName: string]: ReferencesConfiguration | undefined; + }; } // @@ -241,10 +270,9 @@ export interface ValidatorRegistry { get: (validatorName: string) => Validator | null; set: (validatorName: string, validator: Validator) => void; } -export interface I18nRegistry { - translate: (id?: string, fallback?: string, params?: {}, packageKey?: string, sourceName?: string) => string; -} export interface GlobalRegistry { get: (key: K) => K extends 'i18n' ? I18nRegistry : K extends 'validators' ? ValidatorRegistry : null; } + +export type {I18nRegistry} from '@neos-project/neos-ui-i18n'; diff --git a/packages/neos-ui-backend-connector/package.json b/packages/neos-ui-backend-connector/package.json index 36918ffe3f..76dbd9d223 100644 --- a/packages/neos-ui-backend-connector/package.json +++ b/packages/neos-ui-backend-connector/package.json @@ -10,8 +10,7 @@ }, "dependencies": { "@neos-project/neos-ts-interfaces": "workspace:*", - "@neos-project/utils-helpers": "workspace:*", - "plow-js": "3.0.0" + "@neos-project/utils-helpers": "workspace:*" }, "license": "GNU GPLv3" } diff --git a/packages/neos-ui-backend-connector/src/Endpoints/Helpers.ts b/packages/neos-ui-backend-connector/src/Endpoints/Helpers.ts index b37702d16e..b0bf9e0946 100644 --- a/packages/neos-ui-backend-connector/src/Endpoints/Helpers.ts +++ b/packages/neos-ui-backend-connector/src/Endpoints/Helpers.ts @@ -1,8 +1,3 @@ -export const getContextString = (uri: string) => { - const decodedUri = unescape(uri); - const uriParts = decodedUri.split('@'); - return uriParts ? uriParts[1].split('.')[0] : ''; -}; /** * diff --git a/packages/neos-ui-backend-connector/src/Endpoints/index.ts b/packages/neos-ui-backend-connector/src/Endpoints/index.ts index 643faee25d..aa2130b77d 100644 --- a/packages/neos-ui-backend-connector/src/Endpoints/index.ts +++ b/packages/neos-ui-backend-connector/src/Endpoints/index.ts @@ -1,4 +1,4 @@ -import {getElementInnerText, getElementAttributeValue, getContextString} from './Helpers'; +import {getElementInnerText, getElementAttributeValue} from './Helpers'; import {isNil} from '@neos-project/utils-helpers'; import {urlWithParams, encodeAsQueryString} from '@neos-project/utils-helpers/src/urlWithParams'; @@ -9,25 +9,27 @@ export interface Routes { ui: { service: { change: string; - publish: string; - discard: string; + publishChangesInSite: string; + publishChangesInDocument: string; + discardAllChanges: string; + discardChangesInSite: string; + discardChangesInDocument: string; changeBaseWorkspace: string; + syncWorkspace: string; copyNodes: string; cutNodes: string; clearClipboard: string; - loadTree: string; flowQuery: string; generateUriPathSegment: string; getWorkspaceInfo: string; getAdditionalNodeMetadata: string; + reloadNodes: string; }; }; core: { content: { imageWithMetadata: string; createImageVariant: string; - loadMasterPlugins: string; - loadPluginViews: string; uploadAsset: string; }; service: { @@ -67,8 +69,8 @@ export default (routes: Routes) => { })).then(response => fetchWithErrorHandling.parseJson(response)) .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); - const publish = (nodeContextPaths: NodeContextPath[], targetWorkspaceName: WorkspaceName) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ - url: routes.ui.service.publish, + const publishChangesInSite = (siteId: NodeContextPath, workspaceName: WorkspaceName, preferredDimensionSpacePoint: null|DimensionCombination) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ + url: routes.ui.service.publishChangesInSite, method: 'POST', credentials: 'include', headers: { @@ -76,15 +78,55 @@ export default (routes: Routes) => { 'Content-Type': 'application/json' }, body: JSON.stringify({ - nodeContextPaths, - targetWorkspaceName + command: {siteId, workspaceName, preferredDimensionSpacePoint} }) })).then(response => fetchWithErrorHandling.parseJson(response)) .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); - const discard = (nodeContextPaths: NodeContextPath[]) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ - url: routes.ui.service.discard, + const publishChangesInDocument = (documentId: NodeContextPath, workspaceName: WorkspaceName, preferredDimensionSpacePoint: null|DimensionCombination) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ + url: routes.ui.service.publishChangesInDocument, + method: 'POST', + credentials: 'include', + headers: { + 'X-Flow-Csrftoken': csrfToken, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + command: {documentId, workspaceName, preferredDimensionSpacePoint} + }) + })).then(response => fetchWithErrorHandling.parseJson(response)) + .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); + + const discardAllChanges = (workspaceName: WorkspaceName) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ + url: routes.ui.service.discardAllChanges, + method: 'POST', + credentials: 'include', + headers: { + 'X-Flow-Csrftoken': csrfToken, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + command: {workspaceName} + }) + })).then(response => fetchWithErrorHandling.parseJson(response)) + .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); + + const discardChangesInSite = (siteId: NodeContextPath, workspaceName: WorkspaceName) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ + url: routes.ui.service.discardChangesInSite, + method: 'POST', + credentials: 'include', + headers: { + 'X-Flow-Csrftoken': csrfToken, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + command: {siteId, workspaceName} + }) + })).then(response => fetchWithErrorHandling.parseJson(response)) + .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); + const discardChangesInDocument = (documentId: NodeContextPath, workspaceName: WorkspaceName) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ + url: routes.ui.service.discardChangesInDocument, method: 'POST', credentials: 'include', headers: { @@ -92,7 +134,7 @@ export default (routes: Routes) => { 'Content-Type': 'application/json' }, body: JSON.stringify({ - nodeContextPaths + command: {documentId, workspaceName} }) })).then(response => fetchWithErrorHandling.parseJson(response)) .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); @@ -113,6 +155,23 @@ export default (routes: Routes) => { })).then(response => fetchWithErrorHandling.parseJson(response)) .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); + const syncWorkspace = (targetWorkspaceName: WorkspaceName, force: boolean, dimensionSpacePoint: null|DimensionCombination) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ + url: routes.ui.service.syncWorkspace, + + method: 'POST', + credentials: 'include', + headers: { + 'X-Flow-Csrftoken': csrfToken, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + targetWorkspaceName, + force, + dimensionSpacePoint + }) + })).then(response => fetchWithErrorHandling.parseJson(response)) + .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); + const copyNodes = (nodes: NodeContextPath[]) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ url: routes.ui.service.copyNodes, @@ -192,26 +251,6 @@ export default (routes: Routes) => { })).then(response => fetchWithErrorHandling.parseJson(response)) .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); - const loadMasterPlugins = (workspaceName: WorkspaceName, dimensions: DimensionCombination) => fetchWithErrorHandling.withCsrfToken(() => ({ - url: urlWithParams(routes.core.content.loadMasterPlugins, {workspaceName, dimensions}), - method: 'GET', - credentials: 'include', - headers: { - 'Content-Type': 'application/json' - } - })).then(response => fetchWithErrorHandling.parseJson(response)) - .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); - - const loadPluginViews = (identifier: string, workspaceName: WorkspaceName, dimensions: DimensionCombination) => fetchWithErrorHandling.withCsrfToken(() => ({ - url: urlWithParams(routes.core.content.loadPluginViews, {identifier, workspaceName, dimensions}), - method: 'GET', - credentials: 'include', - headers: { - 'Content-Type': 'application/json' - } - })).then(response => fetchWithErrorHandling.parseJson(response)) - .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); - const contentDimensions = (dimensionName: DimensionName, chosenDimensionPresets: DimensionPresetCombination) => fetchWithErrorHandling.withCsrfToken(() => ({ url: urlWithParams(`${routes.core.service.contentDimensions}/${dimensionName}.json`, {chosenDimensionPresets}), method: 'GET', @@ -477,15 +516,15 @@ export default (routes: Routes) => { throw new Error('.node-frontend-uri does not contain a valid href attribut'); } - const nodePath = d.querySelector('.node-path'); - if (!nodePath) { - throw new Error('.node-path is not found in the result'); + const nodeContextPathElement = d.querySelector('.node-context-path'); + if (!nodeContextPathElement) { + throw new Error('.node-context-path is not found in the result'); } - // Hackish way to get context string from uri - const contextString = getContextString(nodeFrontendUri); - // TODO: Temporary hack due to missing contextPath in the API response - const nodeContextPath = `${nodePath.innerHTML}@${contextString}`; + const nodeContextPath = nodeContextPathElement.innerHTML.trim(); + if (!nodeContextPath) { + throw new Error('.node-context-path is empty'); + } return { nodeFound: true, @@ -656,18 +695,41 @@ export default (routes: Routes) => { .then(response => fetchWithErrorHandling.parseJson(response)) .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); + const reloadNodes = (query: { + workspaceName: WorkspaceName; + dimensionSpacePoint: DimensionCombination; + siteId: NodeContextPath; + documentId: NodeContextPath; + ancestorsOfDocumentIds: NodeContextPath[]; + toggledNodesIds: NodeContextPath[]; + clipboardNodesIds: NodeContextPath[]; + }) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ + url: routes.ui.service.reloadNodes, + method: 'POST', + credentials: 'include', + headers: { + 'X-Flow-Csrftoken': csrfToken, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({query}) + })) + .then(response => fetchWithErrorHandling.parseJson(response)) + .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); + return { loadImageMetadata, change, - publish, - discard, + publishChangesInSite, + publishChangesInDocument, + discardAllChanges, + discardChangesInSite, + discardChangesInDocument, changeBaseWorkspace, + syncWorkspace, copyNodes, cutNodes, clearClipboard, createImageVariant, - loadMasterPlugins, - loadPluginViews, uploadAsset, assetProxyImport, assetProxySearch, @@ -686,6 +748,7 @@ export default (routes: Routes) => { tryLogin, contentDimensions, impersonateStatus, - impersonateRestore + impersonateRestore, + reloadNodes }; }; diff --git a/packages/neos-ui-backend-connector/src/FlowQuery/index.ts b/packages/neos-ui-backend-connector/src/FlowQuery/index.ts index e9bf7a2a7b..cdd23eb7f7 100644 --- a/packages/neos-ui-backend-connector/src/FlowQuery/index.ts +++ b/packages/neos-ui-backend-connector/src/FlowQuery/index.ts @@ -1,5 +1,5 @@ import * as operations from './Operations/index'; -import {$get} from 'plow-js'; + import fetchWithErrorHandling from '../FetchWithErrorHandling/index'; import {Node, NodeContextPath, ContextProperties} from '@neos-project/neos-ts-interfaces'; import {Routes} from '../Endpoints'; @@ -33,7 +33,7 @@ export const createNodeEnvelope = (node: NodeEnvelope | NodeContextPath) => { let contextPath = ''; if (typeof node === 'object') { - contextPath = $get(['contextPath'], node) || $get(['$node'], node); + contextPath = node?.contextPath || node?.$node; } else if (typeof node === 'string') { contextPath = node; } @@ -103,8 +103,8 @@ export default (routes: Routes) => { // const q = (context: ContextProperties | string, ignoreMiddleware: boolean = false) => { let finalContext: NodeContextPath[]; - if (typeof context === 'object' && typeof $get(['contextPath'], context) === 'string') { - finalContext = [$get(['contextPath'], context) as string]; + if (typeof context === 'object' && typeof context?.contextPath === 'string') { + finalContext = [context?.contextPath as string]; } else if (typeof context === 'string') { finalContext = [context]; } else if (Array.isArray(context)) { diff --git a/packages/neos-ui-backend-connector/src/index.ts b/packages/neos-ui-backend-connector/src/index.ts index 995cfd2711..66fe06618a 100644 --- a/packages/neos-ui-backend-connector/src/index.ts +++ b/packages/neos-ui-backend-connector/src/index.ts @@ -41,11 +41,18 @@ export const initializeJsAPI = (parent: {[propName: string]: any}, {alias = 'neo return parent[alias]; }; +type Api = { + use: ReturnType; + q: ReturnType; + endpoints: ReturnType; + [libraryName: string]: any +} + // // Expose methods to access the initialized api // export default { - get(alias: string = 'neos', ctx: {[propName: string]: any} = window): any { + get(alias: string = 'neos', ctx: {[propName: string]: any} = window): Api { return ctx[alias]; } }; @@ -62,3 +69,8 @@ export const createPlugin = (identifier: string, factory: any) => { // Expose fetchWithErrorHandling // export {fetchWithErrorHandling}; + +// +// Expose types +// +export type {Routes}; diff --git a/packages/neos-ui-ckeditor5-bindings/package.json b/packages/neos-ui-ckeditor5-bindings/package.json index 5b8ff358e0..c0a7079198 100644 --- a/packages/neos-ui-ckeditor5-bindings/package.json +++ b/packages/neos-ui-ckeditor5-bindings/package.json @@ -5,20 +5,20 @@ "private": true, "main": "./src/manifest.js", "dependencies": { - "@ckeditor/ckeditor5-alignment": "^16.0.0", - "@ckeditor/ckeditor5-basic-styles": "^16.0.0", - "@ckeditor/ckeditor5-core": "^16.0.0", - "@ckeditor/ckeditor5-editor-decoupled": "^16.0.0", - "@ckeditor/ckeditor5-engine": "^16.0.0", - "@ckeditor/ckeditor5-essentials": "^16.0.0", - "@ckeditor/ckeditor5-heading": "^16.0.0", - "@ckeditor/ckeditor5-link": "^16.0.0", - "@ckeditor/ckeditor5-list": "^16.0.0", - "@ckeditor/ckeditor5-paragraph": "^16.0.0", - "@ckeditor/ckeditor5-remove-format": "^16.0.0", - "@ckeditor/ckeditor5-table": "^16.0.0", - "@ckeditor/ckeditor5-utils": "^16.0.0", - "@ckeditor/ckeditor5-widget": "^16.0.0", + "@ckeditor/ckeditor5-alignment": "^44.0.0", + "@ckeditor/ckeditor5-basic-styles": "^44.0.0", + "@ckeditor/ckeditor5-core": "^44.0.0", + "@ckeditor/ckeditor5-editor-decoupled": "^44.0.0", + "@ckeditor/ckeditor5-engine": "^44.0.0", + "@ckeditor/ckeditor5-essentials": "^44.0.0", + "@ckeditor/ckeditor5-heading": "^44.0.0", + "@ckeditor/ckeditor5-link": "^44.0.0", + "@ckeditor/ckeditor5-list": "^44.0.0", + "@ckeditor/ckeditor5-paragraph": "^44.0.0", + "@ckeditor/ckeditor5-remove-format": "^44.0.0", + "@ckeditor/ckeditor5-table": "^44.0.0", + "@ckeditor/ckeditor5-utils": "^44.0.0", + "@ckeditor/ckeditor5-widget": "^44.0.0", "@neos-project/neos-ui-decorators": "workspace:*", "@neos-project/neos-ui-editors": "workspace:*", "@neos-project/neos-ui-extensibility": "workspace:*", @@ -27,8 +27,7 @@ "@neos-project/utils-helpers": "workspace:*", "classnames": "^2.2.3", "lodash.debounce": "^4.0.8", - "lodash.omit": "^4.5.0", - "plow-js": "3.0.0" + "lodash.omit": "^4.5.0" }, "peerDependencies": { "prop-types": "^15.5.10", diff --git a/packages/neos-ui-ckeditor5-bindings/src/EditorToolbar/LinkButton.js b/packages/neos-ui-ckeditor5-bindings/src/EditorToolbar/LinkButton.js index 050a98379a..142515a1d4 100644 --- a/packages/neos-ui-ckeditor5-bindings/src/EditorToolbar/LinkButton.js +++ b/packages/neos-ui-ckeditor5-bindings/src/EditorToolbar/LinkButton.js @@ -1,7 +1,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; -import {$get, $transform} from 'plow-js'; + import LinkInput from '@neos-project/neos-ui-editors/src/Library/LinkInput'; import {IconButton} from '@neos-project/react-ui-components'; @@ -10,8 +10,8 @@ import {selectors, actions} from '@neos-project/neos-ui-redux-store'; import style from './LinkButton.module.css'; -@connect($transform({ - isOpen: selectors.UI.ContentCanvas.isLinkEditorOpen +@connect(state => ({ + isOpen: selectors.UI.ContentCanvas.isLinkEditorOpen(state) }), { toggle: actions.UI.ContentCanvas.toggleLinkEditor }) @@ -35,7 +35,7 @@ export default class LinkButton extends PureComponent { handleLinkButtonClick = () => { if (this.props.isOpen) { - if ($get('link', this.props.formattingUnderCursor) !== undefined) { + if (this.props.formattingUnderCursor?.link !== undefined) { // We need to remove all attributes before unsetting the link this.props.executeCommand('linkTitle', false, false); this.props.executeCommand('linkRelNofollow', false, false); @@ -87,7 +87,7 @@ export default class LinkButton extends PureComponent { {isOpen ? (
    - {this.props.i18nRegistry.translate(this.props.tooltip)} + {this.props.i18nRegistry.translate(this.props.tooltip)} {this.props.options.map(item => item.type === 'checkBox' ? ( @@ -54,7 +56,7 @@ export default class TableDropDownButton extends PureComponent { className={style.checkBox} onClick={() => this.handleClick(item.commandName)} > - + {this.props.i18nRegistry.translate(item.label)} ) :
    ); } diff --git a/packages/neos-ui-editors/src/Editors/MasterPlugin/index.js b/packages/neos-ui-editors/src/Editors/MasterPlugin/index.js deleted file mode 100644 index d6478c1a50..0000000000 --- a/packages/neos-ui-editors/src/Editors/MasterPlugin/index.js +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import SelectBox from '@neos-project/react-ui-components/src/SelectBox/'; -import backend from '@neos-project/neos-ui-backend-connector'; -import {neos} from '@neos-project/neos-ui-decorators'; -import {connect} from 'react-redux'; -import {selectors} from '@neos-project/neos-ui-redux-store'; -import {$transform, $get} from 'plow-js'; - -@neos(globalRegistry => { - return { - i18nRegistry: globalRegistry.get('i18n') - }; -}) -@connect($transform({ - activeContentDimensions: selectors.CR.ContentDimensions.active, - personalWorkspace: selectors.CR.Workspaces.personalWorkspaceNameSelector -})) -class MasterPluginEditor extends React.PureComponent { - static propTypes = { - className: PropTypes.string, - id: PropTypes.string, - value: PropTypes.string, - commit: PropTypes.func.isRequired, - i18nRegistry: PropTypes.object.isRequired, - activeContentDimensions: PropTypes.object.isRequired, - personalWorkspace: PropTypes.string - }; - - constructor(...args) { - super(...args); - - this.state = { - isLoading: false, - options: [] - }; - } - - renderPlaceholder() { - const placeholderPrefix = 'Neos.Neos:Main:content.inspector.editors.masterPluginEditor.'; - const placeholderLabel = placeholderPrefix + (this.state.options.length > 0 ? 'selectPlugin' : 'noPluginConfigured'); - return this.props.i18nRegistry.translate(placeholderLabel); - } - - transformMasterPluginStructure(plugins) { - const pluginsList = []; - for (const property in plugins) { - if (Object.prototype.hasOwnProperty.call(plugins, property)) { - pluginsList.push({ - value: property, - label: plugins[property] - }); - } - } - - return pluginsList; - } - - componentDidMount() { - const {loadMasterPlugins} = backend.get().endpoints; - const {personalWorkspace, activeContentDimensions} = this.props; - - if (!this.state.options.length) { - this.setState({isLoading: true}); - - loadMasterPlugins(personalWorkspace, activeContentDimensions) - .then(options => { - this.setState({ - isLoading: false, - options: this.transformMasterPluginStructure(options) - }); - }); - } - } - - handleValueChange = value => { - this.props.commit(value); - } - - render() { - const {options, isLoading} = this.state; - const disabled = $get('options.disabled', this.props); - - return ( - - ); - } -} - -export default MasterPluginEditor; diff --git a/packages/neos-ui-editors/src/Editors/NodeType/index.js b/packages/neos-ui-editors/src/Editors/NodeType/index.js index 30eb035df5..38a15e950a 100644 --- a/packages/neos-ui-editors/src/Editors/NodeType/index.js +++ b/packages/neos-ui-editors/src/Editors/NodeType/index.js @@ -1,7 +1,6 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; -import {$get} from 'plow-js'; import mergeClassNames from 'classnames'; import SelectBox from '@neos-project/react-ui-components/src/SelectBox/'; @@ -18,12 +17,12 @@ import style from './style.module.css'; return state => { const focusedNode = selectors.CR.Nodes.focusedSelector(state); - const focusedNodeContextPath = $get('contextPath', focusedNode); + const focusedNodeContextPath = focusedNode?.contextPath; const allowedSiblingNodeTypesForFocusedNode = getAllowedSiblingNodeTypes(state, { subject: focusedNodeContextPath, reference: focusedNodeContextPath }); - const isSiteNode = $get('cr.nodes.siteNode', state) === focusedNodeContextPath; + const isSiteNode = state?.cr?.nodes?.siteNode === focusedNodeContextPath; return {focusedNode, allowedSiblingNodeTypesForFocusedNode, isSiteNode}; }; @@ -52,17 +51,17 @@ export default class NodeType extends PureComponent { return (
    - {i18nRegistry.translate($get('ui.label', nodeTypesRegistry.get(value)))} + {i18nRegistry.translate(nodeTypesRegistry.get(value)?.ui?.label)}
    ); } render() { const {value, className, commit, nodeTypesRegistry, allowedSiblingNodeTypesForFocusedNode, i18nRegistry, focusedNode, isSiteNode} = this.props; - const disabled = $get('options.disabled', this.props); + const disabled = this.props?.options?.disabled; // Auto-created child nodes cannot change type - if ($get('isAutoCreated', focusedNode) === true) { + if (focusedNode?.isAutoCreated === true) { return this.renderNoOptionsAvailable(); } @@ -71,7 +70,7 @@ export default class NodeType extends PureComponent { const nodeTypes = nodeTypesRegistry.getGroupedNodeTypeList(nodeTypeFilter).reduce((result, group) => { group.nodeTypes.forEach(nodeType => { result.push({ - icon: $get('ui.icon', nodeType), + icon: nodeType?.ui?.icon, label: i18nRegistry.translate(nodeType.label) || nodeType.name, value: nodeType.name, group: i18nRegistry.translate(group.label) diff --git a/packages/neos-ui-editors/src/Editors/PluginView/index.js b/packages/neos-ui-editors/src/Editors/PluginView/index.js deleted file mode 100644 index 6bba799dae..0000000000 --- a/packages/neos-ui-editors/src/Editors/PluginView/index.js +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import SelectBox from '@neos-project/react-ui-components/src/SelectBox/'; -import backend from '@neos-project/neos-ui-backend-connector'; -import {neos} from '@neos-project/neos-ui-decorators'; -import {connect} from 'react-redux'; -import {selectors} from '@neos-project/neos-ui-redux-store'; -import {$transform, $get} from 'plow-js'; - -@neos(globalRegistry => { - return { - i18nRegistry: globalRegistry.get('i18n') - }; -}) - -@connect($transform({ - activeContentDimensions: selectors.CR.ContentDimensions.active, - personalWorkspace: selectors.CR.Workspaces.personalWorkspaceNameSelector, - focusedNode: selectors.CR.Nodes.focusedSelector, - transientValues: selectors.UI.Inspector.transientValues -})) - -class PluginViewEditor extends React.PureComponent { - static propTypes = { - id: PropTypes.string, - value: PropTypes.string, - className: PropTypes.string, - commit: PropTypes.func.isRequired, - i18nRegistry: PropTypes.object.isRequired, - activeContentDimensions: PropTypes.object.isRequired, - personalWorkspace: PropTypes.string, - focusedNode: PropTypes.object.isRequired, - transientValues: PropTypes.object - // focusedNode: PropTypes.instanceOf(PluginViewEditor).isRequired TODO: This is currently broken and gives an error in console, needs to be fixed - }; - - state = { - isLoading: false, - options: [] - }; - - renderPlaceholder() { - const placeholderPrefix = 'Neos.Neos:Main:content.inspector.editors.masterPluginEditor.'; - const placeholderLabel = placeholderPrefix + (this.state.options.length > 0 ? 'selectPlugin' : 'noPluginConfigured'); - return this.props.i18nRegistry.translate(placeholderLabel); - } - - transformPluginStructure(plugins) { - const pluginsList = []; - for (const key in plugins) { - if (plugins[key] === undefined || plugins[key].label === undefined) { - continue; - } - pluginsList.push({value: key, label: plugins[key].label}); - } - - return pluginsList; - } - - componentDidMount() { - this.loadOptions(this.props); - } - - UNSAFE_componentWillReceiveProps(nextProps) { - if ($get('plugin.value', nextProps.transientValues) !== $get('plugin.value', this.props.transientValues)) { - this.loadOptions(nextProps); - } - } - - loadOptions(props) { - const {personalWorkspace, activeContentDimensions, focusedNode, transientValues} = props; - if (!focusedNode) { - return; - } - - const {loadPluginViews} = backend.get().endpoints; - - const pluginNodeProperties = $get('properties', focusedNode); - - if (pluginNodeProperties.plugin) { - const pluginNodeIdentifier = $get('plugin.value', transientValues) === undefined ? $get('plugin', pluginNodeProperties) : $get('plugin.value', transientValues); - this.setState({isLoading: true}); - loadPluginViews(pluginNodeIdentifier, personalWorkspace, activeContentDimensions) - .then(views => { - this.setState({ - isLoading: false, - options: this.transformPluginStructure(views) - }); - }); - } - } - - handleValueChange = value => { - this.props.commit(value); - } - - render() { - const {options, isLoading} = this.state; - const disabled = $get('options.disabled', this.props); - - return ( - - ); - } -} - -export default PluginViewEditor; diff --git a/packages/neos-ui-editors/src/Editors/PluginViews/index.js b/packages/neos-ui-editors/src/Editors/PluginViews/index.js deleted file mode 100644 index e403a654d1..0000000000 --- a/packages/neos-ui-editors/src/Editors/PluginViews/index.js +++ /dev/null @@ -1,125 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import backend from '@neos-project/neos-ui-backend-connector'; -import {neos} from '@neos-project/neos-ui-decorators'; -import {connect} from 'react-redux'; -import {selectors, actions} from '@neos-project/neos-ui-redux-store'; -import mergeClassNames from 'classnames'; -import {$transform} from 'plow-js'; -import style from './style.module.css'; - -@neos(globalRegistry => { - return { - i18nRegistry: globalRegistry.get('i18n') - }; -}) - -@connect($transform({ - activeContentDimensions: selectors.CR.ContentDimensions.active, - personalWorkspace: selectors.CR.Workspaces.personalWorkspaceNameSelector, - focusedNodeIdentifier: selectors.CR.Nodes.focusedNodeIdentifierSelector -}), { - setActiveContentCanvasSrc: actions.UI.ContentCanvas.setSrc -}) - -class PluginViewsEditor extends React.PureComponent { - static propTypes = { - i18nRegistry: PropTypes.object.isRequired, - className: PropTypes.string, - activeContentDimensions: PropTypes.object.isRequired, - personalWorkspace: PropTypes.string, - focusedNodeIdentifier: PropTypes.string.isRequired, - setActiveContentCanvasSrc: PropTypes.func - }; - - state = { - isLoading: false, - views: [] - }; - - componentDidMount() { - const {personalWorkspace, activeContentDimensions, focusedNodeIdentifier} = this.props; - - if (!focusedNodeIdentifier) { - return; - } - - const {loadPluginViews} = backend.get().endpoints; - - if (!this.state.views.length) { - this.setState({isLoading: true}); - - loadPluginViews(focusedNodeIdentifier, personalWorkspace, activeContentDimensions) - .then(views => { - const viewsArray = []; - for (const viewName in views) { - if (views[viewName]) { - viewsArray.push(views[viewName]); - } - } - - this.setState({ - isLoading: false, - views: viewsArray - }); - }); - } - } - - renderViewListItems() { - const {isLoading, views} = this.state; - - if (isLoading) { - return ( -
  • - {this.props.i18nRegistry.translate('Neos.Neos:Main:loading', 'Loading')} -
  • - ); - } - - if (views.length > 0) { - return views.map(view => -
  • - {view.label} - {this.renderLocationLabel(Object.prototype.hasOwnProperty.call(view, 'pageNode'))} - {this.renderLink(view.pageNode)} -
  • - ); - } - } - - renderLocationLabel(onPage) { - let label = 'content.inspector.editors.pluginViewsEditor.'; - label += onPage ? 'displayedOnPage' : 'displayedOnCurrentPage'; - return this.props.i18nRegistry.translate(label); - } - - renderLink(pageNode) { - return ( - pageNode ? {pageNode.title} : null - ); - } - - handleClick = source => () => { - const {setActiveContentCanvasSrc} = this.props; - if (setActiveContentCanvasSrc) { - setActiveContentCanvasSrc(source); - } - } - - render() { - const {className} = this.props; - const classNames = mergeClassNames({ - [className]: true, - [style.pluginViewContainer]: true - }); - - return ( -
      - {this.renderViewListItems()} -
    - ); - } -} - -export default PluginViewsEditor; diff --git a/packages/neos-ui-editors/src/Editors/PluginViews/style.module.css b/packages/neos-ui-editors/src/Editors/PluginViews/style.module.css deleted file mode 100644 index 5bcaab86a1..0000000000 --- a/packages/neos-ui-editors/src/Editors/PluginViews/style.module.css +++ /dev/null @@ -1,25 +0,0 @@ -.pluginViewContainer { - width: 100%; - list-style: none; - background: var(--colors-ContrastNeutral); - margin: 0; - padding: 0; -} - -.pluginViewContainer__listItem { - font-size: var(--fontSize-Small); - line-height: var(--spacing-Full); - margin-bottom: var(--spacing-Half); - padding: var(--spacing-Full); -} - -.pluginViewContainer__listItem b { - margin-right: calc(var(--spacing-Half) / 2); -} - -.pluginViewContainer__listItem a { - font-size: var(--fontSize-Small); - color: var(--colors-PrimaryBlue); - text-decoration: none; - margin-left: calc(var(--spacing-Half) / 2); -} diff --git a/packages/neos-ui-editors/src/Editors/Reference/createNew.js b/packages/neos-ui-editors/src/Editors/Reference/createNew.js index db7bd27745..d34da6570b 100644 --- a/packages/neos-ui-editors/src/Editors/Reference/createNew.js +++ b/packages/neos-ui-editors/src/Editors/Reference/createNew.js @@ -1,13 +1,13 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; -import {$get, $transform} from 'plow-js'; + import backend from '@neos-project/neos-ui-backend-connector'; import {actions} from '@neos-project/neos-ui-redux-store'; export default () => WrappedComponent => { - @connect($transform({ - siteNodeContextPath: $get('cr.nodes.siteNode') + @connect(state => ({ + siteNodeContextPath: state?.cr?.nodes?.siteNode }), { handleServerFeedback: actions.ServerFeedback.handleServerFeedback }) @@ -54,7 +54,7 @@ export default () => WrappedComponent => { const feedbacks = response.feedbacks.filter(feedback => feedback.type !== 'Neos.Neos.Ui:NodeCreated'); this.props.handleServerFeedback({feedbacks}); const nodeCreatedFeedback = response.feedbacks.find(item => item.type === 'Neos.Neos.Ui:NodeCreated'); - const identifier = $get('payload.identifier', nodeCreatedFeedback); + const identifier = nodeCreatedFeedback?.payload?.identifier; const value = Array.isArray(this.props.value) ? this.props.value.concat(identifier) : identifier; this.props.commit(value); @@ -63,8 +63,8 @@ export default () => WrappedComponent => { } render() { - const onCreateNew = $get('options.createNew.type', this.props) && $get('options.createNew.path', this.props) ? this.handleCreateNew : null; - const disabled = $get('options.disabled', this.props); + const onCreateNew = this.props?.options?.createNew?.type && this.props?.options?.createNew?.path ? this.handleCreateNew : null; + const disabled = this.props?.options?.disabled; return ( ({ + creationDialogIsOpen: state?.ui?.nodeCreationDialog?.isOpen, + changesInInspector: state?.ui?.inspector?.valuesByNodePath }), { setActiveContentCanvasSrc: actions.UI.ContentCanvas.setSrc }) diff --git a/packages/neos-ui-editors/src/Editors/Reference/referenceDataLoader.js b/packages/neos-ui-editors/src/Editors/Reference/referenceDataLoader.js index a365ab3ecf..345602dffb 100644 --- a/packages/neos-ui-editors/src/Editors/Reference/referenceDataLoader.js +++ b/packages/neos-ui-editors/src/Editors/Reference/referenceDataLoader.js @@ -1,7 +1,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; -import {$get, $transform} from 'plow-js'; + import {selectors} from '@neos-project/neos-ui-redux-store'; import {neos} from '@neos-project/neos-ui-decorators'; @@ -10,8 +10,8 @@ export default ({isMulti}) => WrappedComponent => { nodeLookupDataLoader: globalRegistry.get('dataLoaders').get('NodeLookup'), nodeTypeRegistry: globalRegistry.get('@neos-project/neos-ui-contentrepository') })) - @connect($transform({ - contextForNodeLinking: selectors.UI.NodeLinking.contextForNodeLinking + @connect(state => ({ + contextForNodeLinking: selectors.UI.NodeLinking.contextForNodeLinking(state) })) class ReferenceDataLoader extends PureComponent { @@ -62,7 +62,7 @@ export default ({isMulti}) => WrappedComponent => { .then(options => { options.forEach(option => { const nodeType = this.props.nodeTypeRegistry.getNodeType(option.nodeType); - const icon = $get('ui.icon', nodeType); + const icon = nodeType?.ui?.icon; if (icon) { option.icon = icon; } @@ -83,7 +83,7 @@ export default ({isMulti}) => WrappedComponent => { .then(searchOptions => { searchOptions.forEach(option => { const nodeType = this.props.nodeTypeRegistry.getNodeType(option.nodeType); - const icon = $get('ui.icon', nodeType); + const icon = nodeType?.ui?.icon; if (icon) { option.icon = icon; } @@ -103,7 +103,7 @@ export default ({isMulti}) => WrappedComponent => { } getDataLoaderOptions() { - const startingPoint = $get('options.startingPoint', this.props); + const startingPoint = this.props?.options?.startingPoint; const contextForNodeLinking = startingPoint ? Object.assign({}, this.props.contextForNodeLinking, { contextNode: startingPoint.indexOf('ClientEval:') === 0 ? @@ -114,7 +114,7 @@ export default ({isMulti}) => WrappedComponent => { this.props.contextForNodeLinking; return { - nodeTypes: $get('options.nodeTypes', this.props) || ['Neos.Neos:Document'], + nodeTypes: this.props?.options?.nodeTypes || ['Neos.Neos:Document'], contextForNodeLinking }; } diff --git a/packages/neos-ui-editors/src/Editors/References/index.js b/packages/neos-ui-editors/src/Editors/References/index.js index 89204ce577..02166acbd3 100644 --- a/packages/neos-ui-editors/src/Editors/References/index.js +++ b/packages/neos-ui-editors/src/Editors/References/index.js @@ -7,14 +7,13 @@ import NodeOption from '../../Library/NodeOption'; import {dndTypes} from '@neos-project/neos-ui-constants'; import {neos} from '@neos-project/neos-ui-decorators'; import {connect} from 'react-redux'; -import {$transform, $get} from 'plow-js'; import {actions} from '@neos-project/neos-ui-redux-store'; import {sanitizeOptions} from '../../Library'; -@connect($transform({ - creationDialogIsOpen: $get('ui.nodeCreationDialog.isOpen'), - changesInInspector: $get('ui.inspector.valuesByNodePath') +@connect((state) => ({ + creationDialogIsOpen: state?.ui?.nodeCreationDialog?.isOpen, + changesInInspector: state?.ui?.inspector?.valuesByNodePath }), { setActiveContentCanvasSrc: actions.UI.ContentCanvas.setSrc }) diff --git a/packages/neos-ui-editors/src/Editors/SelectBox/DataSourceBasedSelectBoxEditor.js b/packages/neos-ui-editors/src/Editors/SelectBox/DataSourceBasedSelectBoxEditor.js index 548cedcb5d..58325198c6 100644 --- a/packages/neos-ui-editors/src/Editors/SelectBox/DataSourceBasedSelectBoxEditor.js +++ b/packages/neos-ui-editors/src/Editors/SelectBox/DataSourceBasedSelectBoxEditor.js @@ -1,6 +1,5 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {$transform} from 'plow-js'; import {connect} from 'react-redux'; import {SelectBox, MultiSelectBox} from '@neos-project/react-ui-components'; import {selectors} from '@neos-project/neos-ui-redux-store'; @@ -21,8 +20,8 @@ const getDataLoaderOptionsForProps = props => ({ i18nRegistry: globalRegistry.get('i18n'), dataSourcesDataLoader: globalRegistry.get('dataLoaders').get('DataSources') })) -@connect($transform({ - focusedNodePath: selectors.CR.Nodes.focusedNodePathSelector +@connect(state => ({ + focusedNodePath: selectors.CR.Nodes.focusedNodePathSelector(state) })) export default class DataSourceBasedSelectBoxEditor extends PureComponent { static propTypes = { diff --git a/packages/neos-ui-editors/src/Editors/SelectBox/selectBoxHelpers.spec.ts b/packages/neos-ui-editors/src/Editors/SelectBox/selectBoxHelpers.spec.ts index 02f5a618f5..52fa4b2334 100644 --- a/packages/neos-ui-editors/src/Editors/SelectBox/selectBoxHelpers.spec.ts +++ b/packages/neos-ui-editors/src/Editors/SelectBox/selectBoxHelpers.spec.ts @@ -1,9 +1,9 @@ import {processSelectBoxOptions} from './selectBoxHelpers'; import {I18nRegistry} from '@neos-project/neos-ts-interfaces'; -const fakeI18NRegistry: I18nRegistry = { +const fakeI18NRegistry = { translate: (id) => id ?? '' -}; +} as I18nRegistry; describe('processSelectBoxOptions', () => { it('transforms an associative array with labels to list of objects', () => { diff --git a/packages/neos-ui-editors/src/Editors/index.js b/packages/neos-ui-editors/src/Editors/index.js index 9450b1e8b5..1e21cd2730 100644 --- a/packages/neos-ui-editors/src/Editors/index.js +++ b/packages/neos-ui-editors/src/Editors/index.js @@ -12,9 +12,6 @@ import NodeType from './NodeType/index'; import CodeMirror from './CodeMirror/index'; import CKEditor from './CKEditor/index'; import AssetEditor from './AssetEditor/index'; -import MasterPlugin from './MasterPlugin/index'; -import PluginViews from './PluginViews/index'; -import PluginView from './PluginView/index'; import UriPathSegment from './UriPathSegment/index'; export { @@ -32,8 +29,5 @@ export { CodeMirror, CKEditor, AssetEditor, - MasterPlugin, - PluginViews, - PluginView, UriPathSegment }; diff --git a/packages/neos-ui-editors/src/Library/AssetUpload.js b/packages/neos-ui-editors/src/Library/AssetUpload.js index e69d167ad4..7e1293eb44 100644 --- a/packages/neos-ui-editors/src/Library/AssetUpload.js +++ b/packages/neos-ui-editors/src/Library/AssetUpload.js @@ -1,6 +1,5 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; -import {$get, $transform} from 'plow-js'; import {connect} from 'react-redux'; import mergeClassNames from 'classnames'; import Dropzone from 'react-dropzone'; @@ -9,9 +8,9 @@ import backend from '@neos-project/neos-ui-backend-connector'; import style from './style.module.css'; import {selectors} from '@neos-project/neos-ui-redux-store'; -@connect($transform({ - siteNodePath: $get('cr.nodes.siteNode'), - focusedNodePath: selectors.CR.Nodes.focusedNodePathSelector +@connect(state => ({ + siteNodePath: state?.cr?.nodes?.siteNode, + focusedNodePath: selectors.CR.Nodes.focusedNodePathSelector(state) }), null, null, {forwardRef: true}) export default class AssetUpload extends PureComponent { static defaultProps = { diff --git a/packages/neos-ui-editors/src/Library/LinkInput.js b/packages/neos-ui-editors/src/Library/LinkInput.js index 18d5c2a58a..00c47b5367 100644 --- a/packages/neos-ui-editors/src/Library/LinkInput.js +++ b/packages/neos-ui-editors/src/Library/LinkInput.js @@ -1,7 +1,6 @@ import React, {PureComponent, Fragment} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; -import {$get, $transform} from 'plow-js'; import {IconButton, SelectBox, Icon} from '@neos-project/react-ui-components'; import LinkOption from './LinkOption'; @@ -37,8 +36,8 @@ const looksLikeExternalLink = link => { i18nRegistry: globalRegistry.get('i18n'), containerRegistry: globalRegistry.get('containers') })) -@connect($transform({ - contextForNodeLinking: selectors.UI.NodeLinking.contextForNodeLinking +@connect(state => ({ + contextForNodeLinking: selectors.UI.NodeLinking.contextForNodeLinking(state) }), { lockPublishing: actions.UI.Remote.lockPublishing, unlockPublishing: actions.UI.Remote.unlockPublishing @@ -96,16 +95,16 @@ export default class LinkInput extends PureComponent { getDataLoaderOptions() { const options = {...this.props.options, ...this.props.linkingOptions}; - const contextForNodeLinking = $get('startingPoint', options) ? + const contextForNodeLinking = options?.startingPoint ? {...this.props.contextForNodeLinking, contextNode: options.startingPoint} : this.props.contextForNodeLinking; return { - nodeTypes: $get('nodeTypes', options) || ['Neos.Neos:Document'], - asset: $get('assets', options), - node: $get('nodes', options), - startingPoint: $get('startingPoint', options), - constraints: $get('constraints', options), + nodeTypes: options?.nodeTypes || ['Neos.Neos:Document'], + asset: options?.assets, + node: options?.nodes, + startingPoint: options?.startingPoint, + constraints: options?.constraints, contextForNodeLinking }; } @@ -310,7 +309,7 @@ export default class LinkInput extends PureComponent { value={''} plainInputMode={isUri(this.state.searchTerm)} onValueChange={this.handleValueChange} - placeholder={this.props.i18nRegistry.translate($get('options.placeholder', this.props) || 'Neos.Neos:Main:content.inspector.editors.linkEditor.search', $get('options.placeholder', this.props) || 'Paste a link, or type to search')} + placeholder={this.props.i18nRegistry.translate(this.props?.options?.placeholder || 'Neos.Neos:Main:content.inspector.editors.linkEditor.search', 'Paste a link, or type to search')} displayLoadingIndicator={this.state.isLoading} displaySearchBox={true} setFocus={this.props.setFocus} @@ -395,7 +394,7 @@ export default class LinkInput extends PureComponent { return (
    - {this.state.isEditMode && !$get('options.disabled', this.props) ? this.renderEditMode() : this.renderViewMode()} + {this.state.isEditMode && !this.props?.options?.disabled ? this.renderEditMode() : this.renderViewMode()} {optionsPanelEnabled && ( - {$get('anchor', linkingOptions) && ( + {linkingOptions?.anchor && (
    )} - {$get('title', linkingOptions) && ( + {linkingOptions?.title && (