├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .sauce └── config.yml ├── .sauceignore ├── Classes ├── Application │ ├── ChangeTargetWorkspace.php │ ├── DiscardAllChanges.php │ ├── DiscardChangesInDocument.php │ ├── DiscardChangesInSite.php │ ├── PublishChangesInDocument │ │ ├── PublishChangesInDocumentCommand.php │ │ └── PublishChangesInDocumentCommandHandler.php │ ├── PublishChangesInSite │ │ ├── PublishChangesInSiteCommand.php │ │ └── PublishChangesInSiteCommandHandler.php │ ├── ReloadNodes │ │ ├── MinimalNodeForTree.php │ │ ├── NoDocumentNodeWasFound.php │ │ ├── NoSiteNodeWasFound.php │ │ ├── NodeMap.php │ │ ├── NodeMapBuilder.php │ │ ├── ReloadNodesQuery.php │ │ ├── ReloadNodesQueryHandler.php │ │ └── ReloadNodesQueryResult.php │ ├── Shared │ │ ├── Conflict.php │ │ ├── Conflicts.php │ │ ├── ConflictsOccurred.php │ │ ├── IconLabel.php │ │ ├── PartialConflictsOccurred.php │ │ ├── PublishSucceeded.php │ │ ├── ReasonForConflict.php │ │ └── TypeOfChange.php │ └── SyncWorkspace │ │ ├── SyncWorkspaceCommand.php │ │ ├── SyncWorkspaceCommandHandler.php │ │ └── SyncingSucceeded.php ├── ContentRepository │ └── Service │ │ ├── NeosUiNodeService.php │ │ └── WorkspaceService.php ├── Controller │ ├── BackendController.php │ ├── BackendServiceController.php │ └── TranslationTrait.php ├── Domain │ ├── InitialData │ │ ├── CacheConfigurationVersionProviderInterface.php │ │ ├── ConfigurationProviderInterface.php │ │ ├── FrontendConfigurationProviderInterface.php │ │ ├── InitialStateProviderInterface.php │ │ ├── MenuProviderInterface.php │ │ ├── NodeTypeGroupsAndRolesProviderInterface.php │ │ └── RoutesProviderInterface.php │ ├── Model │ │ ├── AbstractChange.php │ │ ├── AbstractFeedback.php │ │ ├── ChangeCollection.php │ │ ├── ChangeInterface.php │ │ ├── Changes │ │ │ ├── AbstractCreate.php │ │ │ ├── AbstractStructuralChange.php │ │ │ ├── CopyAfter.php │ │ │ ├── CopyBefore.php │ │ │ ├── CopyInto.php │ │ │ ├── Create.php │ │ │ ├── CreateAfter.php │ │ │ ├── CreateBefore.php │ │ │ ├── MoveAfter.php │ │ │ ├── MoveBefore.php │ │ │ ├── MoveInto.php │ │ │ ├── Property.php │ │ │ └── Remove.php │ │ ├── Feedback │ │ │ ├── AbstractMessageFeedback.php │ │ │ ├── Messages │ │ │ │ ├── Error.php │ │ │ │ ├── Info.php │ │ │ │ ├── Success.php │ │ │ │ └── Warning.php │ │ │ └── Operations │ │ │ │ ├── NodeCreated.php │ │ │ │ ├── Redirect.php │ │ │ │ ├── ReloadContentOutOfBand.php │ │ │ │ ├── ReloadDocument.php │ │ │ │ ├── RemoveNode.php │ │ │ │ ├── RenderContentOutOfBand.php │ │ │ │ ├── UpdateNodeInfo.php │ │ │ │ ├── UpdateNodePath.php │ │ │ │ ├── UpdateNodePreviewUrl.php │ │ │ │ └── UpdateWorkspaceInfo.php │ │ ├── FeedbackCollection.php │ │ ├── FeedbackInterface.php │ │ ├── ReferencingChangeInterface.php │ │ └── RenderedNodeDomAddress.php │ ├── NodeCreation │ │ ├── NodeCreationCommands.php │ │ ├── NodeCreationElements.php │ │ ├── NodeCreationHandlerFactoryInterface.php │ │ └── NodeCreationHandlerInterface.php │ └── Service │ │ ├── ConfigurationRenderingService.php │ │ ├── NodePropertyConversionService.php │ │ ├── NodePropertyConverterService.php │ │ ├── StyleAndJavascriptInclusionService.php │ │ └── UserLocaleService.php ├── Exception │ └── InvalidNodeCreationHandlerException.php ├── FlowQueryOperations │ ├── NeosUiDefaultNodesOperation.php │ ├── NeosUiFilteredChildrenOperation.php │ └── SearchOperation.php ├── Fusion │ ├── ExceptionHandler │ │ └── PageExceptionHandler.php │ ├── Helper │ │ ├── ContentDimensionsHelper.php │ │ ├── NodeInfoHelper.php │ │ ├── RenderingModeHelper.php │ │ ├── StaticResourcesHelper.php │ │ └── WorkspaceHelper.php │ └── RenderConfigurationImplementation.php ├── Infrastructure │ ├── Cache │ │ └── CacheConfigurationVersionProvider.php │ ├── Configuration │ │ ├── ConfigurationProvider.php │ │ ├── FrontendConfigurationProvider.php │ │ └── InitialStateProvider.php │ ├── ContentRepository │ │ ├── ConflictsFactory.php │ │ ├── CreationDialog │ │ │ ├── CreationDialogNodeTypePostprocessor.php │ │ │ └── PromotedElementsCreationHandlerFactory.php │ │ └── NodeTypeGroupsAndRolesProvider.php │ ├── MVC │ │ ├── RoutesProvider.php │ │ └── RoutesProviderHelper.php │ └── Neos │ │ ├── MenuProvider.php │ │ └── UriPathSegmentNodeCreationHandlerFactory.php ├── Presentation │ └── ApplicationView.php ├── Service │ ├── NodeClipboard.php │ └── NodePropertyValidationService.php ├── TypeConverter │ └── ChangeCollectionConverter.php └── View │ ├── OutOfBandRenderingCapable.php │ ├── OutOfBandRenderingFusionView.php │ └── OutOfBandRenderingViewFactory.php ├── Configuration ├── NodeTypes.yaml ├── Objects.yaml ├── Policy.yaml ├── Routes.Backend.yaml ├── Routes.Service.yaml ├── Routes.yaml └── Settings.yaml ├── LICENSE ├── Migrations └── Code │ ├── Version20180907103800.php │ └── Version20190319094900.php ├── README.md ├── Resources ├── Private │ ├── Fusion │ │ ├── Prototypes │ │ │ ├── Page.fusion │ │ │ └── RenderConfiguration.fusion │ │ └── Root.fusion │ ├── Templates │ │ ├── Backend │ │ │ ├── ConfigurationVersion.html │ │ │ ├── Guest.html │ │ │ └── GuestNotificationScript.html │ │ └── Error │ │ │ └── ErrorMessage.html │ └── Translations │ │ ├── ar │ │ └── Main.xlf │ │ ├── cs │ │ └── Main.xlf │ │ ├── da │ │ └── Main.xlf │ │ ├── de │ │ └── Main.xlf │ │ ├── el │ │ └── Main.xlf │ │ ├── en │ │ ├── Error.xlf │ │ ├── Main.xlf │ │ ├── PublishingDialog.xlf │ │ └── SyncWorkspaceDialog.xlf │ │ ├── es │ │ └── Main.xlf │ │ ├── fi │ │ └── Main.xlf │ │ ├── fr │ │ └── Main.xlf │ │ ├── hu │ │ └── Main.xlf │ │ ├── id │ │ └── Main.xlf │ │ ├── it │ │ └── Main.xlf │ │ ├── ja │ │ └── Main.xlf │ │ ├── km │ │ └── Main.xlf │ │ ├── lv │ │ └── Main.xlf │ │ ├── nl │ │ └── Main.xlf │ │ ├── no │ │ └── Main.xlf │ │ ├── pl │ │ └── Main.xlf │ │ ├── pt │ │ └── Main.xlf │ │ ├── pt_PT │ │ └── Main.xlf │ │ ├── ru │ │ └── Main.xlf │ │ ├── sr │ │ └── Main.xlf │ │ ├── sv │ │ └── Main.xlf │ │ ├── tl │ │ └── Main.xlf │ │ ├── tr │ │ └── Main.xlf │ │ ├── uk │ │ └── Main.xlf │ │ ├── vi │ │ └── Main.xlf │ │ ├── zh │ │ └── Main.xlf │ │ └── zh_TW │ │ └── Main.xlf └── Public │ ├── Build │ └── .gitkeep │ └── Images │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── safari-pinned-tab.svg ├── composer.json ├── cssModules.js ├── cssVariables.css ├── cssVariables.js ├── esbuild.js ├── legacyDependencies.js ├── patches ├── isemail-npm-3.2.0-browserified.patch └── react-codemirror2-npm-7.2.1-browserified.patch ├── phpstan.neon.dist └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | indent_style = space 7 | indent_size = 4 8 | 9 | [*.{yml,yaml,json,xlf}] 10 | indent_size = 2 11 | 12 | [*.md] 13 | indent_size = 2 14 | trim_trailing_whitespace = false 15 | 16 | [Makefile] 17 | indent_style = tab 18 | 19 | [*.css.d.ts] 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | '@neos-project/eslint-config-neos', 5 | 'plugin:@typescript-eslint/eslint-recommended' 6 | ], 7 | parser: '@typescript-eslint/parser', 8 | globals: { 9 | expect: true, 10 | sinon: false 11 | }, 12 | env: { 13 | node: true 14 | }, 15 | plugins: [ 16 | '@typescript-eslint' 17 | ], 18 | rules: { 19 | // Fix for incorrect unused var detection 20 | 'no-unused-vars': 'off', 21 | '@typescript-eslint/no-unused-vars': ['error'], 22 | 23 | // remove bs 24 | 'operator-linebreak': 'off', 25 | 'arrow-parens': 'off', 26 | 'camelcase': 'off', 27 | 28 | // The following rules should be fixed and enabled again #nobody-likes-you-linter! 29 | 'no-use-before-define': 'off', 30 | 'default-case': 'off', 31 | 'no-mixed-operators': 'off', 32 | 'no-negated-condition': 'off', 33 | 'complexity': 'off', 34 | 35 | // This rule would prevent us from implementing meaningful value objects 36 | 'no-useless-constructor': 'off' 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/releases/** binary 2 | /.yarn/plugins/** binary 3 | 4 | # Ignore dev related folders in archives 5 | /.circleci export-ignore 6 | /.github export-ignore 7 | /.yarn export-ignore 8 | /Build export-ignore 9 | /Tests export-ignore 10 | /packages export-ignore 11 | 12 | # Ignore dev related files in archives 13 | /.babelrc export-ignore 14 | /.ecrc.json export-ignore 15 | /.eslintrc export-ignore 16 | /.nvmrc export-ignore 17 | /.styleci.yml export-ignore 18 | /.stylelintrc export-ignore 19 | /.yarnrc.yml export-ignore 20 | /Makefile export-ignore 21 | /docker-compose.yaml export-ignore 22 | /package.json export-ignore 23 | /yarn.lock export-ignore 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # Test suite & generated coverage. 3 | # 4 | Coverage/ 5 | packages/*/dist/ 6 | packages/*/.nyc_output/ 7 | packages/*/coverage/ 8 | Build/Selenium/Screenshots/*.png 9 | lcov.info 10 | 11 | # 12 | # Customized credentials for the Neos backend which the WebDriverIO runner uses. 13 | # 14 | Build/Selenium/Settings.yaml 15 | 16 | # 17 | # Application & development dependencies. 18 | # 19 | node_modules/ 20 | packages/*/yarn.lock 21 | 22 | # 23 | # Compiled assets. 24 | # 25 | Resources/Public/Build/* 26 | !Resources/Public/Build/.gitkeep 27 | *.tsbuildinfo 28 | 29 | # 30 | # System Files 31 | # 32 | .DS_Store 33 | 34 | # 35 | # editors / IDEs 36 | # 37 | .vscode/ 38 | .idea/ 39 | *.iml 40 | 41 | # 42 | # Package Manager 43 | # 44 | .yarn/* 45 | !.yarn/cache 46 | !.yarn/patches 47 | !.yarn/plugins 48 | !.yarn/releases 49 | !.yarn/sdks 50 | !.yarn/versions 51 | -------------------------------------------------------------------------------- /.sauce/config.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1alpha 2 | kind: testcafe 3 | showConsoleLog: true 4 | sauce: 5 | region: us-west-1 6 | concurrency: 1 # Controls how many suites are executed at the same time. 7 | # todo fix and enable retries 8 | retries: 0 9 | metadata: 10 | tags: 11 | - e2e 12 | - $TARGET_BRANCH 13 | build: $TARGET_BRANCH 14 | tunnel: 15 | name: "circleci-tunnel" 16 | testcafe: 17 | version: 3.6.2 18 | # Controls what files are available in the context of a test run (unless explicitly excluded by .sauceignore). 19 | rootDir: ./ 20 | suites: 21 | - name: "Tests in Firefox on Windows" 22 | browserName: "firefox" 23 | src: 24 | - "Tests/IntegrationTests/Fixtures/*/*.e2e.js" 25 | platformName: "Windows 10" 26 | screenResolution: "1280x1024" 27 | # todo use chrome here and fix ci https://github.com/neos/neos-ui/issues/3591 (but even firefox fails in ci) 28 | # - name: "Tests in Firefox on MacOS" 29 | # browserName: "firefox" 30 | # src: 31 | # - "Tests/IntegrationTests/Fixtures/*/*.e2e.js" 32 | # platformName: "macOS 13" 33 | # screenResolution: "1440x900" 34 | npm: 35 | dependencies: 36 | - testcafe-react-selectors 37 | 38 | # Controls what artifacts to fetch when the suites have finished. 39 | artifacts: 40 | download: 41 | match: 42 | - neosui-test-report.json 43 | - console.log 44 | - sauce-test-report.json 45 | when: always 46 | allAttempts: true 47 | directory: ../../Data/Logs/saucelabs-artifacts/ 48 | 49 | reporters: 50 | json: 51 | enabled: true 52 | filename: neosui-test-report.json 53 | -------------------------------------------------------------------------------- /.sauceignore: -------------------------------------------------------------------------------- 1 | # This file instructs saucectl to not package any files mentioned here. 2 | .git/ 3 | .github/ 4 | .DS_Store 5 | .hg/ 6 | .vscode/ 7 | .idea/ 8 | .gitignore 9 | .hgignore 10 | .gitlab-ci.yml 11 | .npmrc 12 | *.gif 13 | -------------------------------------------------------------------------------- /Classes/Application/ChangeTargetWorkspace.php: -------------------------------------------------------------------------------- 1 | $values 37 | */ 38 | public static function fromArray(array $values): self 39 | { 40 | return new self( 41 | ContentRepositoryId::fromString($values['contentRepositoryId']), 42 | WorkspaceName::fromString($values['workspaceName']), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Classes/Application/DiscardChangesInDocument.php: -------------------------------------------------------------------------------- 1 | $values 39 | */ 40 | public static function fromArray(array $values): self 41 | { 42 | return new self( 43 | ContentRepositoryId::fromString($values['contentRepositoryId']), 44 | WorkspaceName::fromString($values['workspaceName']), 45 | NodeAggregateId::fromString($values['documentId']), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Classes/Application/DiscardChangesInSite.php: -------------------------------------------------------------------------------- 1 | $values 39 | */ 40 | public static function fromArray(array $values): self 41 | { 42 | return new self( 43 | ContentRepositoryId::fromString($values['contentRepositoryId']), 44 | WorkspaceName::fromString($values['workspaceName']), 45 | NodeAggregateId::fromString($values['siteId']), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Classes/Application/PublishChangesInDocument/PublishChangesInDocumentCommand.php: -------------------------------------------------------------------------------- 1 | } $values 42 | */ 43 | public static function fromArray(array $values): self 44 | { 45 | return new self( 46 | ContentRepositoryId::fromString($values['contentRepositoryId']), 47 | WorkspaceName::fromString($values['workspaceName']), 48 | NodeAggregateId::fromString($values['documentId']), 49 | isset($values['preferredDimensionSpacePoint']) && !empty($values['preferredDimensionSpacePoint']) 50 | ? DimensionSpacePoint::fromLegacyDimensionArray($values['preferredDimensionSpacePoint']) 51 | : null, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Classes/Application/PublishChangesInSite/PublishChangesInSiteCommand.php: -------------------------------------------------------------------------------- 1 | } $values 42 | */ 43 | public static function fromArray(array $values): self 44 | { 45 | return new self( 46 | ContentRepositoryId::fromString($values['contentRepositoryId']), 47 | WorkspaceName::fromString($values['workspaceName']), 48 | NodeAggregateId::fromString($values['siteId']), 49 | isset($values['preferredDimensionSpacePoint']) && !empty($values['preferredDimensionSpacePoint']) 50 | ? DimensionSpacePoint::fromLegacyDimensionArray($values['preferredDimensionSpacePoint']) 51 | : null, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Classes/Application/PublishChangesInSite/PublishChangesInSiteCommandHandler.php: -------------------------------------------------------------------------------- 1 | workspacePublishingService->publishChangesInSite( 53 | $command->contentRepositoryId, 54 | $command->workspaceName, 55 | $command->siteId 56 | ); 57 | 58 | $workspace = $this->contentRepositoryRegistry->get($command->contentRepositoryId)->findWorkspaceByName( 59 | $command->workspaceName 60 | ); 61 | 62 | return new PublishSucceeded( 63 | numberOfAffectedChanges: $publishingResult->numberOfPublishedChanges, 64 | baseWorkspaceName: $workspace?->baseWorkspaceName?->value 65 | ); 66 | } catch (WorkspaceRebaseFailed $e) { 67 | $this->throwableStorage->logThrowable($e); 68 | 69 | $conflictsFactory = new ConflictsFactory( 70 | contentRepository: $this->contentRepositoryRegistry 71 | ->get($command->contentRepositoryId), 72 | nodeLabelGenerator: $this->nodeLabelGenerator, 73 | workspaceName: $command->workspaceName, 74 | preferredDimensionSpacePoint: $command->preferredDimensionSpacePoint 75 | ); 76 | 77 | return new ConflictsOccurred( 78 | conflicts: $conflictsFactory->fromWorkspaceRebaseFailed($e) 79 | ); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Classes/Application/ReloadNodes/MinimalNodeForTree.php: -------------------------------------------------------------------------------- 1 | $data 33 | */ 34 | private function __construct(private array $data) 35 | { 36 | } 37 | 38 | public static function tryFromNode( 39 | Node $node, 40 | NodeInfoHelper $nodeInfoHelper, 41 | ActionRequest $actionRequest 42 | ): ?self { 43 | /** @var null|(array{contextPath:string}&array) $data */ 44 | $data = $nodeInfoHelper 45 | ->renderNodeWithMinimalPropertiesAndChildrenInformation( 46 | node: $node, 47 | actionRequest: $actionRequest 48 | ); 49 | 50 | return $data ? new self($data) : null; 51 | } 52 | 53 | public function getNodeAddressAsString(): string 54 | { 55 | return $this->data['contextPath']; 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | public function jsonSerialize(): array 62 | { 63 | return $this->data; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Classes/Application/ReloadNodes/NoDocumentNodeWasFound.php: -------------------------------------------------------------------------------- 1 | items = $items; 35 | } 36 | 37 | /** 38 | * @param class-string $nodeRepresentationClass 39 | */ 40 | public static function builder( 41 | string $nodeRepresentationClass, 42 | NodeInfoHelper $nodeInfoHelper, 43 | ActionRequest $actionRequest 44 | ): NodeMapBuilder { 45 | return new NodeMapBuilder( 46 | nodeRepresentationClass: $nodeRepresentationClass, 47 | nodeInfoHelper: $nodeInfoHelper, 48 | actionRequest: $actionRequest, 49 | ); 50 | } 51 | 52 | /** 53 | * @return \stdClass|(array) 54 | */ 55 | public function jsonSerialize(): mixed 56 | { 57 | $result = []; 58 | foreach ($this->items as $item) { 59 | $result[$item->getNodeAddressAsString()] = $item; 60 | } 61 | 62 | return $result ? $result : new \stdClass; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Classes/Application/ReloadNodes/NodeMapBuilder.php: -------------------------------------------------------------------------------- 1 | $nodeRepresentationClass 35 | */ 36 | public function __construct( 37 | private readonly string $nodeRepresentationClass, 38 | private readonly NodeInfoHelper $nodeInfoHelper, 39 | private readonly ActionRequest $actionRequest 40 | ) { 41 | } 42 | 43 | public function addNode(Node $node): void 44 | { 45 | $item = $this->nodeRepresentationClass::tryFromNode( 46 | node: $node, 47 | nodeInfoHelper: $this->nodeInfoHelper, 48 | actionRequest: $this->actionRequest 49 | ); 50 | 51 | if ($item !== null) { 52 | $this->items[] = $item; 53 | } 54 | } 55 | 56 | public function build(): NodeMap 57 | { 58 | return new NodeMap(...$this->items); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Classes/Application/ReloadNodes/ReloadNodesQuery.php: -------------------------------------------------------------------------------- 1 | $values 47 | */ 48 | public static function fromArray(array $values): self 49 | { 50 | return new self( 51 | contentRepositoryId: ContentRepositoryId::fromString($values['contentRepositoryId']), 52 | workspaceName: WorkspaceName::fromString($values['workspaceName']), 53 | dimensionSpacePoint: DimensionSpacePoint::fromLegacyDimensionArray($values['dimensionSpacePoint']), 54 | siteId: NodeAggregateId::fromString($values['siteId']), 55 | documentId: NodeAggregateId::fromString($values['documentId']), 56 | ancestorsOfDocumentIds: NodeAggregateIds::fromArray($values['ancestorsOfDocumentIds']), 57 | toggledNodesIds: NodeAggregateIds::fromArray($values['toggledNodesIds']), 58 | clipboardNodesIds: NodeAggregateIds::fromArray($values['clipboardNodesIds']) 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Classes/Application/ReloadNodes/ReloadNodesQueryResult.php: -------------------------------------------------------------------------------- 1 | $this->documentId->toJson(), 42 | 'nodes' => $this->nodes 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Classes/Application/Shared/Conflict.php: -------------------------------------------------------------------------------- 1 | items = array_values($items); 31 | } 32 | 33 | public function jsonSerialize(): mixed 34 | { 35 | return $this->items; 36 | } 37 | 38 | public function count(): int 39 | { 40 | return count($this->items); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Classes/Application/Shared/ConflictsOccurred.php: -------------------------------------------------------------------------------- 1 | true 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Classes/Application/Shared/PublishSucceeded.php: -------------------------------------------------------------------------------- 1 | get_object_vars($this) 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Classes/Application/Shared/ReasonForConflict.php: -------------------------------------------------------------------------------- 1 | value; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Classes/Application/Shared/TypeOfChange.php: -------------------------------------------------------------------------------- 1 | value; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Classes/Application/SyncWorkspace/SyncWorkspaceCommand.php: -------------------------------------------------------------------------------- 1 | workspacePublishingService->rebaseWorkspace( 51 | $command->contentRepositoryId, 52 | $command->workspaceName, 53 | $command->rebaseErrorHandlingStrategy 54 | ); 55 | return new SyncingSucceeded(); 56 | } catch (WorkspaceRebaseFailed $e) { 57 | $this->throwableStorage->logThrowable($e); 58 | 59 | $conflictsFactory = new ConflictsFactory( 60 | contentRepository: $this->contentRepositoryRegistry 61 | ->get($command->contentRepositoryId), 62 | nodeLabelGenerator: $this->nodeLabelGenerator, 63 | workspaceName: $command->workspaceName, 64 | preferredDimensionSpacePoint: $command->preferredDimensionSpacePoint 65 | ); 66 | 67 | return new ConflictsOccurred( 68 | conflicts: $conflictsFactory->fromWorkspaceRebaseFailed($e) 69 | ); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Classes/Application/SyncWorkspace/SyncingSucceeded.php: -------------------------------------------------------------------------------- 1 | true]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Classes/ContentRepository/Service/NeosUiNodeService.php: -------------------------------------------------------------------------------- 1 | contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); 33 | 34 | $subgraph = $contentRepository->getContentSubgraph($nodeAddress->workspaceName, $nodeAddress->dimensionSpacePoint); 35 | return $subgraph->findNodeById($nodeAddress->aggregateId); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Classes/Controller/TranslationTrait.php: -------------------------------------------------------------------------------- 1 | $arguments 31 | */ 32 | public function getLabel(string $id, array $arguments = [], ?int $quantity = null): string 33 | { 34 | return $this->translator->translateById( 35 | $id, 36 | $arguments, 37 | $quantity, 38 | null, 39 | 'Main', 40 | 'Neos.Neos.Ui' 41 | ) ?: $id; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/Domain/InitialData/CacheConfigurationVersionProviderInterface.php: -------------------------------------------------------------------------------- 1 | */ 29 | public function getFrontendConfiguration( 30 | ActionRequest $actionRequest 31 | ): array; 32 | } 33 | -------------------------------------------------------------------------------- /Classes/Domain/InitialData/InitialStateProviderInterface.php: -------------------------------------------------------------------------------- 1 | */ 31 | public function getInitialState( 32 | ActionRequest $actionRequest, 33 | ?Node $documentNode, 34 | ?Node $site, 35 | User $user, 36 | ): array; 37 | } 38 | -------------------------------------------------------------------------------- /Classes/Domain/InitialData/MenuProviderInterface.php: -------------------------------------------------------------------------------- 1 | }> 29 | */ 30 | public function getMenu(ActionRequest $actionRequest): array; 31 | } 32 | -------------------------------------------------------------------------------- /Classes/Domain/InitialData/NodeTypeGroupsAndRolesProviderInterface.php: -------------------------------------------------------------------------------- 1 | */ 31 | public function getRoutes(UriBuilder $uriBuilder): array; 32 | } 33 | -------------------------------------------------------------------------------- /Classes/Domain/Model/AbstractChange.php: -------------------------------------------------------------------------------- 1 | subject = $subject; 57 | } 58 | 59 | final public function getSubject(): Node 60 | { 61 | return $this->subject; 62 | } 63 | 64 | /** 65 | * Helper method to inform the client, that new workspace information is available 66 | */ 67 | final protected function updateWorkspaceInfo(): void 68 | { 69 | $subgraph = $this->contentRepositoryRegistry->subgraphForNode($this->subject); 70 | $documentNode = $subgraph->findClosestNode($this->subject->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_DOCUMENT)); 71 | if (!is_null($documentNode)) { 72 | $updateWorkspaceInfo = new UpdateWorkspaceInfo($documentNode->contentRepositoryId, $documentNode->workspaceName); 73 | $this->feedbackCollection->add($updateWorkspaceInfo); 74 | } 75 | } 76 | 77 | final protected function findParentNode(Node $node): ?Node 78 | { 79 | return $this->contentRepositoryRegistry->subgraphForNode($node) 80 | ->findParentNode($node->aggregateId); 81 | } 82 | 83 | final protected function getNodeType(Node $node): ?NodeType 84 | { 85 | $contentRepository = $this->contentRepositoryRegistry->get($node->contentRepositoryId); 86 | return $contentRepository->getNodeTypeManager()->getNodeType($node->nodeTypeName); 87 | } 88 | 89 | /** 90 | * Inform the client to reload the currently-displayed document, because the rendering has changed. 91 | * 92 | * This method will be triggered if [nodeType].properties.[propertyName].ui.reloadIfChanged is TRUE. 93 | */ 94 | protected function reloadDocument(?Node $node = null): void 95 | { 96 | $reloadDocument = new ReloadDocument(); 97 | if ($node) { 98 | $reloadDocument->setNode($node); 99 | } 100 | 101 | $this->feedbackCollection->add($reloadDocument); 102 | } 103 | 104 | /** 105 | * Inform the client that a node has been created, the client decides if and which tree should react to this change. 106 | */ 107 | final protected function addNodeCreatedFeedback(?Node $subject = null): void 108 | { 109 | $node = $subject ?? $this->getSubject(); 110 | $nodeCreated = new NodeCreated(); 111 | $nodeCreated->setNode($node); 112 | $this->feedbackCollection->add($nodeCreated); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Classes/Domain/Model/AbstractFeedback.php: -------------------------------------------------------------------------------- 1 | */ 23 | public function serialize(ControllerContext $controllerContext): array 24 | { 25 | return [ 26 | 'type' => $this->getType(), 27 | 'description' => $this->getDescription(), 28 | 'payload' => $this->serializePayload($controllerContext) 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Classes/Domain/Model/ChangeCollection.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | protected array $changes = []; 26 | 27 | /** 28 | * Add a change to this collection 29 | */ 30 | public function add(ChangeInterface $change): void 31 | { 32 | $this->changes[] = $change; 33 | } 34 | 35 | /** 36 | * Apply all changes 37 | */ 38 | public function apply(): void 39 | { 40 | while ($change = array_shift($this->changes)) { 41 | if ($change->canApply()) { 42 | $change->apply(); 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * Get the number of changes in this collection 49 | */ 50 | public function count(): int 51 | { 52 | return count($this->changes); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Classes/Domain/Model/ChangeInterface.php: -------------------------------------------------------------------------------- 1 | getSiblingNode(); 39 | if (is_null($siblingNode)) { 40 | return false; 41 | } 42 | $parentNode = $this->findParentNode($siblingNode); 43 | return $parentNode && $this->isNodeTypeAllowedAsChildNode($parentNode, $this->subject->nodeTypeName); 44 | } 45 | 46 | public function getMode(): string 47 | { 48 | return 'after'; 49 | } 50 | 51 | /** 52 | * Applies this change 53 | */ 54 | public function apply(): void 55 | { 56 | $previousSibling = $this->getSiblingNode(); 57 | $parentNodeOfPreviousSibling = !is_null($previousSibling) 58 | ? $this->findParentNode($previousSibling) 59 | : null; 60 | $subject = $this->subject; 61 | 62 | if ($this->canApply() && !is_null($previousSibling) && !is_null($parentNodeOfPreviousSibling)) { 63 | $succeedingSibling = null; 64 | try { 65 | $succeedingSibling = $this->findChildNodes($parentNodeOfPreviousSibling)->next($previousSibling); 66 | } catch (\InvalidArgumentException $e) { 67 | // do nothing; $succeedingSibling is null. Todo add Nodes::contain() 68 | } 69 | if (!$subject->dimensionSpacePoint->equals($parentNodeOfPreviousSibling->dimensionSpacePoint)) { 70 | throw new \RuntimeException('Copying across dimensions is not supported yet (https://github.com/neos/neos-development-collection/issues/5054)', 1733586265); 71 | } 72 | $this->nodeDuplicationService->copyNodesRecursively( 73 | $subject->contentRepositoryId, 74 | $subject->workspaceName, 75 | $subject->dimensionSpacePoint, 76 | $subject->aggregateId, 77 | OriginDimensionSpacePoint::fromDimensionSpacePoint($parentNodeOfPreviousSibling->dimensionSpacePoint), 78 | $parentNodeOfPreviousSibling->aggregateId, 79 | $succeedingSibling?->aggregateId, 80 | NodeAggregateIdMapping::createEmpty() 81 | ->withNewNodeAggregateId($subject->aggregateId, $newlyCreatedNodeId = NodeAggregateId::create()) 82 | ); 83 | 84 | $newlyCreatedNode = $this->contentRepositoryRegistry->subgraphForNode($parentNodeOfPreviousSibling) 85 | ->findNodeById($newlyCreatedNodeId); 86 | if (!$newlyCreatedNode) { 87 | throw new \RuntimeException(sprintf('Node %s was not found after copy.', $newlyCreatedNodeId->value), 1716023308); 88 | } 89 | $this->finish($newlyCreatedNode); 90 | // NOTE: we need to run "finish" before "addNodeCreatedFeedback" 91 | // to ensure the new node already exists when the last feedback is processed 92 | $this->addNodeCreatedFeedback($newlyCreatedNode); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Changes/CopyBefore.php: -------------------------------------------------------------------------------- 1 | getSiblingNode(); 37 | if (is_null($siblingNode)) { 38 | return false; 39 | } 40 | $parentNode = $this->findParentNode($siblingNode); 41 | 42 | return $parentNode && $this->isNodeTypeAllowedAsChildNode($parentNode, $this->subject->nodeTypeName); 43 | } 44 | 45 | public function getMode(): string 46 | { 47 | return 'before'; 48 | } 49 | 50 | /** 51 | * Applies this change 52 | * 53 | * @return void 54 | */ 55 | public function apply(): void 56 | { 57 | $succeedingSibling = $this->getSiblingNode(); 58 | $parentNodeOfSucceedingSibling = !is_null($succeedingSibling) 59 | ? $this->findParentNode($succeedingSibling) 60 | : null; 61 | $subject = $this->subject; 62 | if ($this->canApply() && !is_null($succeedingSibling) 63 | && !is_null($parentNodeOfSucceedingSibling) 64 | ) { 65 | if (!$subject->dimensionSpacePoint->equals($succeedingSibling->dimensionSpacePoint)) { 66 | throw new \RuntimeException('Copying across dimensions is not supported yet (https://github.com/neos/neos-development-collection/issues/5054)', 1733586265); 67 | } 68 | $this->nodeDuplicationService->copyNodesRecursively( 69 | $subject->contentRepositoryId, 70 | $subject->workspaceName, 71 | $subject->dimensionSpacePoint, 72 | $subject->aggregateId, 73 | OriginDimensionSpacePoint::fromDimensionSpacePoint($succeedingSibling->dimensionSpacePoint), 74 | $parentNodeOfSucceedingSibling->aggregateId, 75 | $succeedingSibling->aggregateId, 76 | NodeAggregateIdMapping::createEmpty() 77 | ->withNewNodeAggregateId($subject->aggregateId, $newlyCreatedNodeId = NodeAggregateId::create()) 78 | ); 79 | 80 | $newlyCreatedNode = $this->contentRepositoryRegistry->subgraphForNode($parentNodeOfSucceedingSibling) 81 | ->findNodeById($newlyCreatedNodeId); 82 | if (!$newlyCreatedNode) { 83 | throw new \RuntimeException(sprintf('Node %s was not found after copy.', $newlyCreatedNodeId->value), 1716023308); 84 | } 85 | $this->finish($newlyCreatedNode); 86 | // NOTE: we need to run "finish" before "addNodeCreatedFeedback" 87 | // to ensure the new node already exists when the last feedback is processed 88 | $this->addNodeCreatedFeedback($newlyCreatedNode); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Changes/CopyInto.php: -------------------------------------------------------------------------------- 1 | parentContextPath = $parentContextPath; 39 | } 40 | 41 | public function getParentNode(): ?Node 42 | { 43 | if (!isset($this->cachedParentNode)) { 44 | $this->cachedParentNode = $this->parentContextPath 45 | ? $this->nodeService->findNodeBySerializedNodeAddress($this->parentContextPath) 46 | : null; 47 | } 48 | 49 | return $this->cachedParentNode; 50 | } 51 | 52 | /** 53 | * "Subject" is the to-be-copied node; the "parent" node is the new parent 54 | */ 55 | public function canApply(): bool 56 | { 57 | $parentNode = $this->getParentNode(); 58 | 59 | return $parentNode && $this->isNodeTypeAllowedAsChildNode($parentNode, $this->subject->nodeTypeName); 60 | } 61 | 62 | public function getMode(): string 63 | { 64 | return 'into'; 65 | } 66 | 67 | /** 68 | * Applies this change 69 | */ 70 | public function apply(): void 71 | { 72 | $subject = $this->getSubject(); 73 | $parentNode = $this->getParentNode(); 74 | if ($parentNode && $this->canApply()) { 75 | if (!$subject->dimensionSpacePoint->equals($parentNode->dimensionSpacePoint)) { 76 | throw new \RuntimeException('Copying across dimensions is not supported yet (https://github.com/neos/neos-development-collection/issues/5054)', 1733586265); 77 | } 78 | $this->nodeDuplicationService->copyNodesRecursively( 79 | $subject->contentRepositoryId, 80 | $subject->workspaceName, 81 | $subject->dimensionSpacePoint, 82 | $subject->aggregateId, 83 | OriginDimensionSpacePoint::fromDimensionSpacePoint($parentNode->dimensionSpacePoint), 84 | $parentNode->aggregateId, 85 | null, 86 | NodeAggregateIdMapping::createEmpty() 87 | ->withNewNodeAggregateId($subject->aggregateId, $newlyCreatedNodeId = NodeAggregateId::create()) 88 | ); 89 | 90 | $newlyCreatedNode = $this->contentRepositoryRegistry->subgraphForNode($parentNode) 91 | ->findNodeById($newlyCreatedNodeId); 92 | if (!$newlyCreatedNode) { 93 | throw new \RuntimeException(sprintf('Node %s was not found after copy.', $newlyCreatedNodeId->value), 1716023308); 94 | } 95 | $this->finish($newlyCreatedNode); 96 | // NOTE: we need to run "finish" before "addNodeCreatedFeedback" 97 | // to ensure the new node already exists when the last feedback is processed 98 | $this->addNodeCreatedFeedback($newlyCreatedNode); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Changes/Create.php: -------------------------------------------------------------------------------- 1 | getSubject(); 41 | $nodeTypeName = $this->getNodeTypeName(); 42 | 43 | return $nodeTypeName && $this->isNodeTypeAllowedAsChildNode($subject, $nodeTypeName); 44 | } 45 | 46 | /** 47 | * Create a new node beneath the subject 48 | */ 49 | public function apply(): void 50 | { 51 | $parentNode = $this->getSubject(); 52 | if ($this->canApply()) { 53 | $this->createNode($parentNode, null); 54 | $this->updateWorkspaceInfo(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Changes/CreateAfter.php: -------------------------------------------------------------------------------- 1 | findParentNode($this->subject); 36 | $nodeTypeName = $this->getNodeTypeName(); 37 | 38 | return $parent && $nodeTypeName && $this->isNodeTypeAllowedAsChildNode($parent, $nodeTypeName); 39 | } 40 | 41 | /** 42 | * Create a new node after the subject 43 | */ 44 | public function apply(): void 45 | { 46 | $parentNode = $this->findParentNode($this->subject); 47 | $subject = $this->subject; 48 | if ($this->canApply() && !is_null($parentNode)) { 49 | $succeedingSibling = null; 50 | try { 51 | $succeedingSibling = $this->findChildNodes($parentNode)->next($subject); 52 | } catch (\InvalidArgumentException $e) { 53 | // do nothing; $succeedingSibling is null. 54 | } 55 | 56 | $this->createNode($parentNode, $succeedingSibling?->aggregateId); 57 | 58 | $this->updateWorkspaceInfo(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Changes/CreateBefore.php: -------------------------------------------------------------------------------- 1 | findParentNode($this->subject); 36 | $nodeTypeName = $this->getNodeTypeName(); 37 | 38 | return $parent && $nodeTypeName && $this->isNodeTypeAllowedAsChildNode($parent, $nodeTypeName); 39 | } 40 | 41 | /** 42 | * Create a new node after the subject 43 | */ 44 | public function apply(): void 45 | { 46 | $parent = $this->findParentNode($this->subject); 47 | $subject = $this->subject; 48 | if ($this->canApply() && !is_null($parent)) { 49 | $this->createNode($parent, $subject->aggregateId); 50 | $this->updateWorkspaceInfo(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Changes/Remove.php: -------------------------------------------------------------------------------- 1 | subject; 51 | if ($this->canApply()) { 52 | $parentNode = $this->findParentNode($subject); 53 | if (is_null($parentNode)) { 54 | throw new \InvalidArgumentException( 55 | 'Cannot apply Remove without a parent on node ' . $subject->aggregateId->value, 56 | 1645560717 57 | ); 58 | } 59 | 60 | // we have to schedule and the update workspace info before we actually delete the node; 61 | // otherwise we cannot find the parent nodes anymore. 62 | $this->updateWorkspaceInfo(); 63 | 64 | // Issuing 'hard' removals via 'RemoveNodeAggregate' on a non-live workspace is not desired in Neos, see SoftRemovedTag 65 | $command = TagSubtree::create( 66 | $subject->workspaceName, 67 | $subject->aggregateId, 68 | $subject->dimensionSpacePoint, 69 | NodeVariantSelectionStrategy::STRATEGY_ALL_SPECIALIZATIONS, 70 | NeosSubtreeTag::removed() 71 | ); 72 | 73 | $contentRepository = $this->contentRepositoryRegistry->get($subject->contentRepositoryId); 74 | $contentRepository->handle($command); 75 | 76 | $removeNode = new RemoveNode($subject, $parentNode); 77 | $this->feedbackCollection->add($removeNode); 78 | 79 | $updateParentNodeInfo = new UpdateNodeInfo(); 80 | $updateParentNodeInfo->setNode($parentNode); 81 | 82 | $this->feedbackCollection->add($updateParentNodeInfo); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Feedback/AbstractMessageFeedback.php: -------------------------------------------------------------------------------- 1 | message = $message; 42 | } 43 | 44 | /** 45 | * Get the message 46 | * 47 | * @return string 48 | */ 49 | public function getMessage() 50 | { 51 | return $this->message; 52 | } 53 | 54 | /** 55 | * Get the severity 56 | * 57 | * @return string 58 | */ 59 | public function getSeverity() 60 | { 61 | return $this->severity; 62 | } 63 | 64 | /** 65 | * Checks whether this feedback is similar to another 66 | * 67 | * @param FeedbackInterface $feedback 68 | * @return boolean 69 | */ 70 | public function isSimilarTo(FeedbackInterface $feedback) 71 | { 72 | if (!$feedback instanceof AbstractMessageFeedback) { 73 | return false; 74 | } 75 | 76 | return ( 77 | $this->getSeverity() === $feedback->getSeverity() && 78 | $this->getMessage() === $feedback->getMessage() 79 | ); 80 | } 81 | 82 | /** 83 | * Serialize the payload for this feedback 84 | * 85 | * @param ControllerContext $controllerContext 86 | * @return mixed 87 | */ 88 | public function serializePayload(ControllerContext $controllerContext) 89 | { 90 | return [ 91 | 'message' => $this->getMessage(), 92 | 'severity' => $this->getSeverity() 93 | ]; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Feedback/Messages/Error.php: -------------------------------------------------------------------------------- 1 | node = $node; 42 | } 43 | 44 | /** 45 | * Get the node 46 | */ 47 | public function getNode(): Node 48 | { 49 | return $this->node; 50 | } 51 | 52 | /** 53 | * Get the type identifier 54 | */ 55 | public function getType(): string 56 | { 57 | return 'Neos.Neos.Ui:NodeCreated'; 58 | } 59 | 60 | /** 61 | * Get the description 62 | */ 63 | public function getDescription(): string 64 | { 65 | return sprintf('Document Node "%s" created.', $this->getNode()->aggregateId->value); 66 | } 67 | 68 | /** 69 | * Checks whether this feedback is similar to another 70 | * 71 | * @param FeedbackInterface $feedback 72 | * @return boolean 73 | */ 74 | public function isSimilarTo(FeedbackInterface $feedback) 75 | { 76 | if (!$feedback instanceof NodeCreated) { 77 | return false; 78 | } 79 | 80 | return $this->getNode()->equals($feedback->getNode()); 81 | } 82 | 83 | /** 84 | * Serialize the payload for this feedback 85 | * 86 | * @param ControllerContext $controllerContext 87 | * @return mixed 88 | */ 89 | public function serializePayload(ControllerContext $controllerContext) 90 | { 91 | $node = $this->getNode(); 92 | $contentRepository = $this->contentRepositoryRegistry->get($node->contentRepositoryId); 93 | $nodeType = $contentRepository->getNodeTypeManager()->getNodeType($node->nodeTypeName); 94 | 95 | return [ 96 | 'contextPath' => NodeAddress::fromNode($node)->toJson(), 97 | 'identifier' => $node->aggregateId->value, 98 | 'isDocument' => $nodeType?->isOfType(NodeTypeNameFactory::NAME_DOCUMENT) 99 | ]; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Feedback/Operations/Redirect.php: -------------------------------------------------------------------------------- 1 | node = $node; 52 | } 53 | 54 | /** 55 | * Get the node 56 | * 57 | * @return Node 58 | */ 59 | public function getNode() 60 | { 61 | return $this->node; 62 | } 63 | 64 | /** 65 | * Get the type identifier 66 | * 67 | * @return string 68 | */ 69 | public function getType() 70 | { 71 | return 'Neos.Neos.Ui:Redirect'; 72 | } 73 | 74 | /** 75 | * Get the description 76 | * 77 | * @return string 78 | */ 79 | public function getDescription() 80 | { 81 | return sprintf('Redirect to node "%s".', $this->nodeLabelGenerator->getLabel($this->getNode())); 82 | } 83 | 84 | /** 85 | * Checks whether this feedback is similar to another 86 | * 87 | * @param FeedbackInterface $feedback 88 | * @return boolean 89 | */ 90 | public function isSimilarTo(FeedbackInterface $feedback) 91 | { 92 | if (!$feedback instanceof UpdateNodeInfo) { 93 | return false; 94 | } 95 | 96 | return $this->getNode()->equals($feedback->getNode()); 97 | } 98 | 99 | /** 100 | * Serialize the payload for this feedback 101 | * 102 | * @param ControllerContext $controllerContext 103 | * @return array 104 | */ 105 | public function serializePayload(ControllerContext $controllerContext): array 106 | { 107 | $node = $this->getNode(); 108 | 109 | $redirectUri = $this->nodeUriBuilderFactory->forActionRequest($controllerContext->getRequest()) 110 | ->uriFor( 111 | NodeAddress::fromNode($node), 112 | Options::createForceAbsolute() 113 | ); 114 | 115 | $contentRepository = $this->contentRepositoryRegistry->get($node->contentRepositoryId); 116 | 117 | return [ 118 | 'redirectUri' => (string)$redirectUri, 119 | 'redirectContextPath' => NodeAddress::fromNode($node)->toJson(), 120 | ]; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Feedback/Operations/ReloadDocument.php: -------------------------------------------------------------------------------- 1 | node = $node; 42 | } 43 | 44 | public function getNode(): ?Node 45 | { 46 | return $this->node; 47 | } 48 | 49 | public function getDescription(): string 50 | { 51 | return sprintf('Reload of current document required.'); 52 | } 53 | 54 | /** 55 | * Checks whether this feedback is similar to another 56 | */ 57 | public function isSimilarTo(FeedbackInterface $feedback) 58 | { 59 | if (!$feedback instanceof ReloadDocument) { 60 | return false; 61 | } 62 | 63 | return true; 64 | } 65 | 66 | /** 67 | * Serialize the payload for this feedback 68 | * 69 | * @return array 70 | */ 71 | public function serializePayload(ControllerContext $controllerContext): array 72 | { 73 | if (!$this->node) { 74 | return []; 75 | } 76 | $nodeInfoHelper = new NodeInfoHelper(); 77 | 78 | $documentNode = $this->contentRepositoryRegistry->subgraphForNode($this->node) 79 | ->findClosestNode($this->node->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_DOCUMENT)); 80 | 81 | if ($documentNode) { 82 | return [ 83 | 'uri' => $nodeInfoHelper->previewUri($documentNode, $controllerContext->getRequest()) 84 | ]; 85 | } 86 | 87 | return []; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Feedback/Operations/RemoveNode.php: -------------------------------------------------------------------------------- 1 | node = $node; 51 | $this->parentNode = $parentNode; 52 | } 53 | 54 | protected function initializeObject(): void 55 | { 56 | $this->nodeAddress = NodeAddress::fromNode($this->node); 57 | $this->parentNodeAddress = NodeAddress::fromNode($this->parentNode); 58 | } 59 | 60 | public function getNode(): Node 61 | { 62 | return $this->node; 63 | } 64 | 65 | /** 66 | * Get the type identifier 67 | * 68 | * @return string 69 | */ 70 | public function getType() 71 | { 72 | return 'Neos.Neos.Ui:RemoveNode'; 73 | } 74 | 75 | /** 76 | * Get the description 77 | * 78 | * @return string 79 | */ 80 | public function getDescription(): string 81 | { 82 | return sprintf('Node "%s" has been removed.', $this->nodeLabelGenerator->getLabel($this->getNode())); 83 | } 84 | 85 | /** 86 | * Checks whether this feedback is similar to another 87 | * 88 | * @param FeedbackInterface $feedback 89 | * @return boolean 90 | */ 91 | public function isSimilarTo(FeedbackInterface $feedback) 92 | { 93 | if (!$feedback instanceof RemoveNode) { 94 | return false; 95 | } 96 | 97 | return $this->getNode()->equals($feedback->getNode()); 98 | } 99 | 100 | /** 101 | * Serialize the payload for this feedback 102 | * 103 | * @param ControllerContext $controllerContext 104 | * @return mixed 105 | */ 106 | public function serializePayload(ControllerContext $controllerContext) 107 | { 108 | return [ 109 | 'contextPath' => $this->nodeAddress->toJson(), 110 | 'parentContextPath' => $this->parentNodeAddress->toJson() 111 | ]; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Feedback/Operations/UpdateNodeInfo.php: -------------------------------------------------------------------------------- 1 | baseNodeType = $baseNodeType; 51 | } 52 | 53 | public function getBaseNodeType(): ?string 54 | { 55 | return $this->baseNodeType; 56 | } 57 | 58 | public function setNode(Node $node): void 59 | { 60 | $this->node = $node; 61 | } 62 | 63 | /** 64 | * Update node infos recursively 65 | */ 66 | public function recursive(): void 67 | { 68 | $this->isRecursive = true; 69 | } 70 | 71 | public function getNode(): Node 72 | { 73 | return $this->node; 74 | } 75 | 76 | public function getType(): string 77 | { 78 | return 'Neos.Neos.Ui:UpdateNodeInfo'; 79 | } 80 | 81 | public function getDescription(): string 82 | { 83 | return sprintf('Updated info for node "%s" is available.', $this->node->aggregateId->value); 84 | } 85 | 86 | /** 87 | * Checks whether this feedback is similar to another 88 | */ 89 | public function isSimilarTo(FeedbackInterface $feedback): bool 90 | { 91 | if (!$feedback instanceof UpdateNodeInfo) { 92 | return false; 93 | } 94 | 95 | return $this->getNode()->equals($feedback->getNode()); 96 | } 97 | 98 | /** 99 | * Serialize the payload for this feedback 100 | * 101 | * @return array 102 | */ 103 | public function serializePayload(ControllerContext $controllerContext): array 104 | { 105 | return [ 106 | 'byContextPath' => $this->serializeNodeRecursively($this->node, $controllerContext->getRequest()) 107 | ]; 108 | } 109 | 110 | /** 111 | * Serialize node and all child nodes 112 | * 113 | * @return array> 114 | */ 115 | private function serializeNodeRecursively(Node $node, ActionRequest $actionRequest): array 116 | { 117 | $contentRepository = $this->contentRepositoryRegistry->get($node->contentRepositoryId); 118 | 119 | $result = [ 120 | NodeAddress::fromNode($node)->toJson() 121 | => $this->nodeInfoHelper->renderNodeWithPropertiesAndChildrenInformation( 122 | $node, 123 | $actionRequest 124 | ) 125 | ]; 126 | 127 | if ($this->isRecursive === true) { 128 | $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); 129 | foreach ($subgraph->findChildNodes($node->aggregateId, FindChildNodesFilter::create()) as $childNode) { 130 | $result = array_merge($result, $this->serializeNodeRecursively($childNode, $actionRequest)); 131 | } 132 | } 133 | 134 | return $result; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Feedback/Operations/UpdateNodePath.php: -------------------------------------------------------------------------------- 1 | oldContextPath = $contextPath; 42 | } 43 | 44 | /** 45 | * Set the new context path after a node was moved 46 | * 47 | * @param string $contextPath 48 | * @return void 49 | */ 50 | public function setNewContextPath(string $contextPath): void 51 | { 52 | $this->newContextPath = $contextPath; 53 | } 54 | 55 | /** 56 | * Get the original context path of the moved node 57 | * 58 | * @return string 59 | */ 60 | public function getOldContextPath(): string 61 | { 62 | return $this->oldContextPath; 63 | } 64 | 65 | /** 66 | * Get the new context path of the moved node 67 | * 68 | * @return string 69 | */ 70 | public function getNewContextPath(): string 71 | { 72 | return $this->newContextPath; 73 | } 74 | 75 | /** 76 | * Get the type identifier 77 | * 78 | * @return string 79 | */ 80 | public function getType() 81 | { 82 | return 'Neos.Neos.Ui:UpdateNodePath'; 83 | } 84 | 85 | /** 86 | * Get the description 87 | * 88 | * @return string 89 | */ 90 | public function getDescription() 91 | { 92 | return sprintf('Updated path for node context path "%s" is available.', $this->getOldContextPath()); 93 | } 94 | 95 | /** 96 | * Checks whether this feedback is similar to another 97 | * 98 | * @param FeedbackInterface $feedback 99 | * @return boolean 100 | */ 101 | public function isSimilarTo(FeedbackInterface $feedback) 102 | { 103 | if (!$feedback instanceof self) { 104 | return false; 105 | } 106 | 107 | return $this->getOldContextPath() === $feedback->getOldContextPath(); 108 | } 109 | 110 | /** 111 | * Serialize the payload for this feedback 112 | * 113 | * @param ControllerContext $controllerContext 114 | * @return mixed 115 | */ 116 | public function serializePayload(ControllerContext $controllerContext) 117 | { 118 | return [ 119 | 'oldContextPath' => $this->getOldContextPath(), 120 | 'newContextPath' => $this->getNewContextPath(), 121 | ]; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Feedback/Operations/UpdateNodePreviewUrl.php: -------------------------------------------------------------------------------- 1 | node = $node; 55 | } 56 | 57 | /** 58 | * Get the node 59 | * 60 | * @return Node 61 | */ 62 | public function getNode() 63 | { 64 | return $this->node; 65 | } 66 | 67 | /** 68 | * Get the type identifier 69 | * 70 | * @return string 71 | */ 72 | public function getType() 73 | { 74 | return 'Neos.Neos.Ui:UpdateNodePreviewUrl'; 75 | } 76 | 77 | /** 78 | * Get the description 79 | * 80 | * @return string 81 | */ 82 | public function getDescription() 83 | { 84 | return sprintf('The "preview URL" of node "%s" has been changed potentially.', $this->nodeLabelGenerator->getLabel($this->getNode())); 85 | } 86 | 87 | /** 88 | * Checks whether this feedback is similar to another 89 | * 90 | * @param FeedbackInterface $feedback 91 | * @return boolean 92 | */ 93 | public function isSimilarTo(FeedbackInterface $feedback) 94 | { 95 | if (!$feedback instanceof UpdateNodePreviewUrl) { 96 | return false; 97 | } 98 | return $this->getNode()->equals($feedback->getNode()); 99 | } 100 | 101 | /** 102 | * Serialize the payload for this feedback 103 | * 104 | * @param ControllerContext $controllerContext 105 | * @return array 106 | */ 107 | public function serializePayload(ControllerContext $controllerContext): array 108 | { 109 | if ($this->node === null) { 110 | $newPreviewUrl = ''; 111 | $contextPath = ''; 112 | } else { 113 | $nodeInfoHelper = new NodeInfoHelper(); 114 | $newPreviewUrl = $nodeInfoHelper->createRedirectToNode($this->node, $controllerContext->getRequest()); 115 | $contextPath = NodeAddress::fromNode($this->node)->toJson(); 116 | } 117 | return [ 118 | 'newPreviewUrl' => $newPreviewUrl, 119 | 'contextPath' => $contextPath, 120 | ]; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Feedback/Operations/UpdateWorkspaceInfo.php: -------------------------------------------------------------------------------- 1 | workspaceName; 56 | } 57 | 58 | /** 59 | * Get the type identifier 60 | * 61 | * @return string 62 | */ 63 | public function getType() 64 | { 65 | return 'Neos.Neos.Ui:UpdateWorkspaceInfo'; 66 | } 67 | 68 | /** 69 | * Get the description 70 | * 71 | * @return string 72 | */ 73 | public function getDescription(): string 74 | { 75 | return sprintf('New workspace info available.'); 76 | } 77 | 78 | /** 79 | * Checks whether this feedback is similar to another 80 | * 81 | * @param FeedbackInterface $feedback 82 | * @return boolean 83 | */ 84 | public function isSimilarTo(FeedbackInterface $feedback) 85 | { 86 | if (!$feedback instanceof UpdateWorkspaceInfo) { 87 | return false; 88 | } 89 | $feedbackWorkspaceName = $feedback->getWorkspaceName(); 90 | return $feedbackWorkspaceName !== null && $this->getWorkspaceName()->equals($feedbackWorkspaceName); 91 | } 92 | 93 | /** 94 | * Serialize the payload for this feedback 95 | * 96 | * @param ControllerContext $controllerContext 97 | * @return mixed 98 | */ 99 | public function serializePayload(ControllerContext $controllerContext) 100 | { 101 | $contentRepository = $this->contentRepositoryRegistry->get($this->contentRepositoryId); 102 | $workspace = $contentRepository->findWorkspaceByName($this->workspaceName); 103 | if ($workspace === null) { 104 | return null; 105 | } 106 | $publishableNodes = $this->uiWorkspaceService->getPublishableNodeInfo($workspace->workspaceName, $contentRepository->id); 107 | return [ 108 | 'name' => $this->workspaceName->value, 109 | 'totalNumberOfChanges' => count($publishableNodes), 110 | 'publishableNodes' => $publishableNodes, 111 | 'baseWorkspace' => $workspace->baseWorkspaceName?->value, 112 | 'status' => $workspace->status->value, 113 | ]; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Classes/Domain/Model/FeedbackCollection.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | protected $feedbacks = []; 27 | 28 | /** 29 | * @var ControllerContext 30 | */ 31 | protected $controllerContext; 32 | 33 | /** 34 | * Set the controller context 35 | * 36 | * @param ControllerContext $controllerContext 37 | * @return void 38 | */ 39 | public function setControllerContext(ControllerContext $controllerContext) 40 | { 41 | $this->controllerContext = $controllerContext; 42 | } 43 | 44 | /** 45 | * Add feedback 46 | * 47 | * @param FeedbackInterface $feedback 48 | * @return void 49 | */ 50 | public function add(FeedbackInterface $feedback) 51 | { 52 | foreach ($this->feedbacks as $i => $value) { 53 | if ($feedback->isSimilarTo($value)) { 54 | $this->feedbacks[$i] = $feedback; 55 | return; 56 | } 57 | } 58 | 59 | $this->feedbacks[] = $feedback; 60 | } 61 | 62 | /** 63 | * Serialize collection to `json_encode`able array 64 | * 65 | * @return array 66 | */ 67 | public function jsonSerialize(): array 68 | { 69 | $feedbacks = []; 70 | 71 | foreach ($this->feedbacks as $feedback) { 72 | $feedbacks[] = $feedback->serialize($this->controllerContext); 73 | } 74 | 75 | return [ 76 | 'timestamp' => new \DateTime(), 77 | 'feedbacks' => $feedbacks 78 | ]; 79 | } 80 | 81 | public function reset(): void 82 | { 83 | $this->feedbacks = []; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Classes/Domain/Model/FeedbackInterface.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public function serialize(ControllerContext $controllerContext): array; 29 | 30 | /** 31 | * Get the type identifier 32 | * 33 | * @return string 34 | */ 35 | public function getType(); 36 | 37 | /** 38 | * Get the description 39 | * 40 | * @return string 41 | */ 42 | public function getDescription(); 43 | 44 | /** 45 | * Checks whether this feedback is similar to another 46 | * 47 | * @param FeedbackInterface $feedback 48 | * @return boolean 49 | */ 50 | public function isSimilarTo(FeedbackInterface $feedback); 51 | 52 | /** 53 | * Serialize the payload for this feedback 54 | * 55 | * @param ControllerContext $controllerContext 56 | * @return mixed 57 | */ 58 | public function serializePayload(ControllerContext $controllerContext); 59 | } 60 | -------------------------------------------------------------------------------- /Classes/Domain/Model/ReferencingChangeInterface.php: -------------------------------------------------------------------------------- 1 | contextPath = $contextPath; 43 | } 44 | 45 | /** 46 | * Get the context path 47 | * 48 | * @return string 49 | */ 50 | public function getContextPath() 51 | { 52 | return $this->contextPath; 53 | } 54 | 55 | /** 56 | * Set the fusion path 57 | * 58 | * @param string $fusionPath 59 | * @return void 60 | */ 61 | public function setFusionPath($fusionPath) 62 | { 63 | $this->fusionPath = $fusionPath; 64 | } 65 | 66 | /** 67 | * Get the fusion path 68 | * 69 | * @return string 70 | */ 71 | public function getFusionPath() 72 | { 73 | return $this->fusionPath; 74 | } 75 | 76 | /** 77 | * Get the fusion path that should be used for rendering the addressed 78 | * content. For most contents that would be the closest `Neos.Neos:ContentCase` 79 | * within the path rather than the actual prototype that was used to 80 | * render the content. 81 | * 82 | * @return string 83 | */ 84 | public function getFusionPathForContentRendering(): string 85 | { 86 | $fusionPathForContentRendering = $this->getFusionPath(); 87 | /** @var string $fusionPathForContentRendering */ 88 | $fusionPathForContentRendering = preg_replace( 89 | '/(\/itemRenderer)\/([^<>\/]+)\/element(<[^>]+>)$/', 90 | '$1', 91 | $fusionPathForContentRendering 92 | ); 93 | 94 | return $fusionPathForContentRendering; 95 | } 96 | 97 | /** 98 | * Serialize to json 99 | * 100 | * @return array 101 | */ 102 | public function jsonSerialize(): array 103 | { 104 | return [ 105 | 'contextPath' => $this->getContextPath(), 106 | 'fusionPath' => $this->getFusionPath() 107 | ]; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Classes/Domain/NodeCreation/NodeCreationElements.php: -------------------------------------------------------------------------------- 1 | 57 | * @internal Especially the constructor and the serialized data 58 | */ 59 | final readonly class NodeCreationElements implements \IteratorAggregate 60 | { 61 | /** 62 | * @param array $elementValues 63 | * @param array $serializedValues 64 | * @internal you should not need to construct this 65 | */ 66 | public function __construct( 67 | private array $elementValues, 68 | private array $serializedValues, 69 | ) { 70 | } 71 | 72 | public function has(string $name): bool 73 | { 74 | return isset($this->elementValues[$name]); 75 | } 76 | 77 | /** 78 | * Returns the type according to the element schema 79 | * For elements that refer to a node {@see NodeAggregateIds} will be returned. 80 | */ 81 | public function get(string $name): mixed 82 | { 83 | return $this->elementValues[$name] ?? null; 84 | } 85 | 86 | /** 87 | * @internal returns values formatted by the internal format used for the Ui 88 | * @return \Traversable 89 | */ 90 | public function serialized(): \Traversable 91 | { 92 | yield from $this->serializedValues; 93 | } 94 | 95 | /** 96 | * @return \Traversable 97 | */ 98 | public function getIterator(): \Traversable 99 | { 100 | yield from $this->elementValues; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Classes/Domain/NodeCreation/NodeCreationHandlerFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | protected $fusionDefaultEelContext; 35 | 36 | /** 37 | * @Flow\InjectConfiguration(path="configurationDefaultEelContext") 38 | * @var array 39 | */ 40 | protected $additionalEelDefaultContext; 41 | 42 | /** 43 | * @param array $configuration 44 | * @param array $context 45 | * @return array 46 | * @throws \Neos\Eel\Exception 47 | */ 48 | public function computeConfiguration(array $configuration, array $context): array 49 | { 50 | $adjustedConfiguration = $configuration; 51 | $this->computeConfigurationInternally($adjustedConfiguration, $context); 52 | 53 | return $adjustedConfiguration; 54 | } 55 | 56 | /** 57 | * @param array $adjustedConfiguration 58 | * @param array $context 59 | * @throws \Neos\Eel\Exception 60 | */ 61 | protected function computeConfigurationInternally(array &$adjustedConfiguration, array $context): void 62 | { 63 | foreach ($adjustedConfiguration as $key => &$value) { 64 | if (is_array($value)) { 65 | $this->computeConfigurationInternally($value, $context); 66 | } elseif (is_string($value) && substr($value, 0, 2) === '${' && substr($value, -1) === '}') { 67 | $value = Utility::evaluateEelExpression( 68 | $value, 69 | $this->eelEvaluator, 70 | $context, 71 | array_merge($this->fusionDefaultEelContext, $this->additionalEelDefaultContext) 72 | ); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Classes/Domain/Service/UserLocaleService.php: -------------------------------------------------------------------------------- 1 | i18nService->getConfiguration()->setCurrentLocale($this->rememberedContentLocale); 64 | return; 65 | } 66 | $this->rememberedContentLocale = $this->i18nService->getConfiguration()->getCurrentLocale(); 67 | if ($this->userLocaleRuntimeCache) { 68 | $this->i18nService->getConfiguration()->setCurrentLocale($this->userLocaleRuntimeCache); 69 | return; 70 | } 71 | $userLocalePreference = ($this->userService->getCurrentUser() ? $this->userService->getCurrentUser()->getPreferences()->getInterfaceLanguage() : null); 72 | $defaultLocale = $this->i18nService->getConfiguration()->getDefaultLocale(); 73 | $userLocale = $userLocalePreference ? new Locale($userLocalePreference) : $defaultLocale; 74 | $this->userLocaleRuntimeCache = $userLocale; 75 | $this->i18nService->getConfiguration()->setCurrentLocale($userLocale); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Classes/Exception/InvalidNodeCreationHandlerException.php: -------------------------------------------------------------------------------- 1 | $context (or array-like object) onto which this operation should be applied 55 | * @return boolean TRUE if the operation can be applied onto the $context, FALSE otherwise 56 | */ 57 | public function canEvaluate($context) 58 | { 59 | return isset($context[0]) && ($context[0] instanceof Node); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | * 65 | * @param FlowQuery $flowQuery the FlowQuery object 66 | * @param array $arguments the arguments for this operation 67 | * @return void 68 | */ 69 | public function evaluate(FlowQuery $flowQuery, array $arguments) 70 | { 71 | $output = []; 72 | $outputNodeIdentifiers = []; 73 | 74 | /** @var Node $contextNode */ 75 | foreach ($flowQuery->getContext() as $contextNode) { 76 | $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); 77 | 78 | foreach ($subgraph->findChildNodes( 79 | $contextNode->aggregateId, 80 | FindChildNodesFilter::create(nodeTypes: $arguments[0] ?? null) 81 | ) as $childNode) { 82 | if (!isset($outputNodeIdentifiers[$childNode->aggregateId->value])) { 83 | $output[] = $childNode; 84 | $outputNodeIdentifiers[$childNode->aggregateId->value] = true; 85 | } 86 | } 87 | } 88 | $flowQuery->setContext($output); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Classes/FlowQueryOperations/SearchOperation.php: -------------------------------------------------------------------------------- 1 | $flowQuery the FlowQuery object 66 | * @param array $arguments the arguments for this operation 67 | */ 68 | public function evaluate(FlowQuery $flowQuery, array $arguments): void 69 | { 70 | /** @var array $context */ 71 | $context = $flowQuery->getContext(); 72 | /** @var Node $contextNode */ 73 | $contextNode = $context[0]; 74 | $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); 75 | $filter = FindDescendantNodesFilter::create(); 76 | if (isset($arguments[0]) && $arguments[0] !== '') { 77 | $filter = $filter->with(searchTerm: $arguments[0]); 78 | } 79 | if (isset($arguments[1]) && $arguments[1] !== '') { 80 | $filter = $filter->with(nodeTypes: $arguments[1]); 81 | } 82 | $nodes = $subgraph->findDescendantNodes( 83 | $contextNode->aggregateId, 84 | $filter 85 | ); 86 | $flowQuery->setContext(iterator_to_array($nodes)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Classes/Fusion/Helper/RenderingModeHelper.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | protected $editPreviewModes; 24 | 25 | /** 26 | * Returns the sorted configuration of all rendering modes {@see RenderingMode} 27 | * 28 | * TODO evaluate if this should be part of {@see RenderingModeService} 29 | * 30 | * @return array> 31 | */ 32 | public function findAllSorted(): array 33 | { 34 | // sorting seems expected for the Neos.Ui: https://github.com/neos/neos-ui/issues/1658 35 | return (new PositionalArraySorter($this->editPreviewModes))->toArray(); 36 | } 37 | 38 | public function allowsCallOfMethod($methodName) 39 | { 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Classes/Fusion/Helper/StaticResourcesHelper.php: -------------------------------------------------------------------------------- 1 | frontendDevelopmentMode) { 32 | return 'Neos.Neos.Ui'; 33 | } else { 34 | return 'Neos.Neos.Ui.Compiled'; 35 | } 36 | } 37 | 38 | /** 39 | * @param string $methodName 40 | * @return boolean 41 | */ 42 | public function allowsCallOfMethod($methodName) 43 | { 44 | return true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Classes/Fusion/Helper/WorkspaceHelper.php: -------------------------------------------------------------------------------- 1 | 67 | */ 68 | public function getPersonalWorkspace(ContentRepositoryId $contentRepositoryId): array 69 | { 70 | $currentUser = $this->userService->getCurrentUser(); 71 | if ($currentUser === null) { 72 | return []; 73 | } 74 | $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); 75 | $personalWorkspace = $this->workspaceService->getPersonalWorkspaceForUser($contentRepositoryId, $currentUser->getId()); 76 | $personalWorkspacePermissions = $this->contentRepositoryAuthorizationService->getWorkspacePermissions($contentRepositoryId, $personalWorkspace->workspaceName, $this->securityContext->getRoles(), $currentUser->getId()); 77 | $publishableNodes = $this->uiWorkspaceService->getPublishableNodeInfo($personalWorkspace->workspaceName, $contentRepository->id); 78 | return [ 79 | 'name' => $personalWorkspace->workspaceName->value, 80 | 'totalNumberOfChanges' => count($publishableNodes), 81 | 'publishableNodes' => $publishableNodes, 82 | 'baseWorkspace' => $personalWorkspace->baseWorkspaceName?->value, 83 | 'readOnly' => !($personalWorkspace->baseWorkspaceName !== null && $personalWorkspacePermissions->write), 84 | 'status' => $personalWorkspace->status->value, 85 | ]; 86 | } 87 | 88 | /** 89 | * @param string $methodName 90 | * @return bool 91 | */ 92 | public function allowsCallOfMethod($methodName) 93 | { 94 | return true; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Classes/Fusion/RenderConfigurationImplementation.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | protected $settings; 36 | 37 | /** 38 | * @return array 39 | */ 40 | protected function getContext(): array 41 | { 42 | return $this->fusionValue('context'); 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | protected function getPath(): string 49 | { 50 | return $this->fusionValue('path'); 51 | } 52 | 53 | /** 54 | * Appends an item to the given collection 55 | * 56 | * @return array 57 | * @throws Exception 58 | */ 59 | public function evaluate() 60 | { 61 | $context = $this->getContext(); 62 | $pathToRender = $this->getPath(); 63 | $actionRequest = $this->getRuntime()->fusionGlobals->get('request'); 64 | if (!$actionRequest instanceof ActionRequest) { 65 | throw new Exception('The request is expected to be an ActionRequest.', 1706639436); 66 | } 67 | $context['request'] = $actionRequest; 68 | 69 | if (!isset($this->settings[$pathToRender])) { 70 | throw new Exception('The path "Neos.Neos.Ui.' . $pathToRender . '" was not found in the settings.', 1458814468); 71 | } 72 | 73 | return $this->configurationRenderingService->computeConfiguration($this->settings[$pathToRender], $context); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Classes/Infrastructure/Cache/CacheConfigurationVersionProvider.php: -------------------------------------------------------------------------------- 1 | computedCacheConfigurationVersion ??= 45 | $this->computeCacheConfigurationVersion(); 46 | } 47 | 48 | private function computeCacheConfigurationVersion(): string 49 | { 50 | /** @var ?Account $account */ 51 | $account = $this->securityContext->getAccount(); 52 | 53 | // Get all roles and sort them by identifier 54 | $roles = $account ? array_map(static fn ($role) => $role->getIdentifier(), $account->getRoles()) : []; 55 | sort($roles); 56 | 57 | // Use the roles combination as cache key to allow multiple users sharing the same configuration version 58 | $configurationIdentifier = md5(implode('_', $roles)); 59 | $cacheKey = 'ConfigurationVersion_' . $configurationIdentifier; 60 | /** @var string|false $version */ 61 | $version = $this->configurationCache->get($cacheKey); 62 | 63 | if ($version === false) { 64 | $version = (string)time(); 65 | $this->configurationCache->set($cacheKey, $version); 66 | } 67 | return $configurationIdentifier . '_' . $version; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Classes/Infrastructure/Configuration/FrontendConfigurationProvider.php: -------------------------------------------------------------------------------- 1 | */ 32 | #[Flow\InjectConfiguration('frontendConfiguration')] 33 | protected array $frontendConfigurationBeforeProcessing; 34 | 35 | public function getFrontendConfiguration( 36 | ActionRequest $actionRequest 37 | ): array { 38 | return $this->configurationRenderingService->computeConfiguration( 39 | $this->frontendConfigurationBeforeProcessing, 40 | ['request' => $actionRequest] 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/Infrastructure/Configuration/InitialStateProvider.php: -------------------------------------------------------------------------------- 1 | */ 39 | #[Flow\InjectConfiguration('initialState')] 40 | protected array $initialStateBeforeProcessing; 41 | 42 | public function getInitialState( 43 | ActionRequest $actionRequest, 44 | ?Node $documentNode, 45 | ?Node $site, 46 | User $user, 47 | ): array { 48 | return $this->configurationRenderingService->computeConfiguration( 49 | $this->initialStateBeforeProcessing, 50 | [ 51 | 'request' => $actionRequest, 52 | 'documentNode' => $documentNode, 53 | 'site' => $site, 54 | 'user' => $user, 55 | 'clipboardNodes' => $this->clipboard->getSerializedNodeAddresses(), 56 | 'clipboardMode' => $this->clipboard->getMode(), 57 | ] 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Classes/Infrastructure/ContentRepository/CreationDialog/PromotedElementsCreationHandlerFactory.php: -------------------------------------------------------------------------------- 1 | getNodeTypeManager()) implements NodeCreationHandlerInterface { 29 | public function __construct( 30 | private readonly NodeTypeManager $nodeTypeManager 31 | ) { 32 | } 33 | 34 | public function handle(NodeCreationCommands $commands, NodeCreationElements $elements): NodeCreationCommands 35 | { 36 | $nodeType = $this->nodeTypeManager->getNodeType($commands->first->nodeTypeName); 37 | if (!$nodeType) { 38 | return $commands; 39 | } 40 | $propertyValues = $commands->first->initialPropertyValues; 41 | $initialReferences = $commands->first->references; 42 | foreach ($elements as $elementName => $elementValue) { 43 | // handle properties 44 | if ($nodeType->hasProperty($elementName)) { 45 | $propertyConfiguration = $nodeType->getProperties()[$elementName]; 46 | if ( 47 | ($propertyConfiguration['ui']['showInCreationDialog'] ?? false) === true 48 | ) { 49 | // a promoted element 50 | $propertyValues = $propertyValues->withValue($elementName, $elementValue); 51 | } 52 | } 53 | 54 | // handle references 55 | if ($nodeType->hasReference($elementName)) { 56 | assert($elementValue instanceof NodeAggregateIds); 57 | $referenceConfiguration = $nodeType->getReferences()[$elementName]; 58 | if (($referenceConfiguration['ui']['showInCreationDialog'] ?? false) === true) { 59 | $initialReferences = $initialReferences->withReference( 60 | NodeReferencesForName::fromTargets( 61 | ReferenceName::fromString($elementName), 62 | $elementValue 63 | ) 64 | ); 65 | } 66 | } 67 | } 68 | 69 | return $commands 70 | ->withInitialPropertyValues($propertyValues) 71 | ->withInitialReferences($initialReferences); 72 | } 73 | }; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Classes/Infrastructure/ContentRepository/NodeTypeGroupsAndRolesProvider.php: -------------------------------------------------------------------------------- 1 | */ 27 | #[Flow\InjectConfiguration(path: 'nodeTypeRoles')] 28 | protected array $roles; 29 | 30 | /** @var array */ 31 | #[Flow\InjectConfiguration(path: 'nodeTypes.groups', package: 'Neos.Neos')] 32 | protected array $groups; 33 | 34 | public function getNodeTypes(): array 35 | { 36 | return [ 37 | 'roles' => $this->roles, 38 | 'groups' => $this->groups, 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Classes/Infrastructure/MVC/RoutesProviderHelper.php: -------------------------------------------------------------------------------- 1 | uriBuilder->reset() 34 | ->setCreateAbsoluteUri(true) 35 | ->uriFor( 36 | actionName: $actionName, 37 | controllerArguments: [], 38 | controllerName: 'BackendService', 39 | packageKey: 'Neos.Neos.Ui', 40 | ); 41 | } 42 | 43 | /** 44 | * @param array $arguments 45 | */ 46 | public function buildCoreRoute( 47 | string $controllerName, 48 | string $actionName, 49 | ?string $subPackageKey = null, 50 | ?string $format = null, 51 | array $arguments = [], 52 | ): string { 53 | $this->uriBuilder->reset() 54 | ->setCreateAbsoluteUri(true); 55 | 56 | if ($format !== null) { 57 | $this->uriBuilder->setFormat($format); 58 | } 59 | 60 | return $this->uriBuilder->uriFor( 61 | actionName: $actionName, 62 | controllerArguments: $arguments, 63 | controllerName: $controllerName, 64 | packageKey: 'Neos.Neos', 65 | subPackageKey: $subPackageKey, 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Classes/Service/NodeClipboard.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | protected array $serializedNodeAddresses = []; 32 | 33 | /** 34 | * one of the NodeClipboard::MODE_* constants 35 | */ 36 | protected string $mode = ''; 37 | 38 | /** 39 | * Save copied node to clipboard. 40 | * 41 | * @param array $nodeAddresses 42 | * @Flow\Session(autoStart=true) 43 | */ 44 | public function copyNodes(array $nodeAddresses): void 45 | { 46 | $this->serializedNodeAddresses = array_map( 47 | fn (NodeAddress $nodeAddress) => $nodeAddress->toJson(), 48 | $nodeAddresses 49 | ); 50 | $this->mode = self::MODE_COPY; 51 | } 52 | 53 | /** 54 | * Save cut node to clipboard. 55 | * 56 | * @param array $nodeAddresses 57 | * @Flow\Session(autoStart=true) 58 | */ 59 | public function cutNodes(array $nodeAddresses): void 60 | { 61 | $this->serializedNodeAddresses = array_map( 62 | fn (NodeAddress $nodeAddress) => $nodeAddress->toJson(), 63 | $nodeAddresses 64 | ); 65 | $this->mode = self::MODE_MOVE; 66 | } 67 | 68 | /** 69 | * Reset clipboard. 70 | * 71 | * @Flow\Session(autoStart=true) 72 | */ 73 | public function clear(): void 74 | { 75 | $this->serializedNodeAddresses = []; 76 | $this->mode = ''; 77 | } 78 | 79 | /** 80 | * @return array 81 | */ 82 | public function getSerializedNodeAddresses(): array 83 | { 84 | return $this->serializedNodeAddresses; 85 | } 86 | 87 | /** 88 | * Get clipboard mode. 89 | */ 90 | public function getMode(): string 91 | { 92 | return $this->mode; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Classes/Service/NodePropertyValidationService.php: -------------------------------------------------------------------------------- 1 | $validatorConfiguration 44 | * @return bool 45 | */ 46 | public function validate(mixed $value, string $validatorName, array $validatorConfiguration): bool 47 | { 48 | $validator = $this->resolveValidator($validatorName, $validatorConfiguration); 49 | 50 | if ($validator === null) { 51 | return true; 52 | } 53 | 54 | // Fixme: The from the UI delivered datetime string (2019-03-21T00:00:00+01:00) is not parsed correctly by the DateTimeParser in the DateTimeValidator, 55 | // so we cast it in prior and use this as final validation result. 56 | if ($validator instanceof DateTimeValidator) { 57 | if ($value === '') { 58 | return true; 59 | } 60 | 61 | if ($this->dateTimeConverter->canConvertFrom($value, 'DateTime')) { 62 | $value = $this->dateTimeConverter->convertFrom($value, 'DateTime'); 63 | return $value instanceof \DateTime; 64 | } 65 | } 66 | 67 | $result = $validator->validate($value); 68 | return !$result->hasErrors(); 69 | } 70 | 71 | /** 72 | * @param string $validatorName 73 | * @param array $validatorConfiguration 74 | * @return ValidatorInterface|null 75 | */ 76 | protected function resolveValidator(string $validatorName, array $validatorConfiguration) 77 | { 78 | $nameParts = explode('/', $validatorName); 79 | if ($nameParts[0] !== 'Neos.Neos') { 80 | $this->logger->info(sprintf('The custom frontend property validator %s" is used. This property is not validated in the backend.', $validatorName), LogEnvironment::fromMethodName(__METHOD__)); 81 | return null; 82 | } 83 | 84 | $fullQualifiedValidatorClassName = '\\Neos\\Flow\\Validation\\Validator\\' . end($nameParts); 85 | 86 | if (!class_exists($fullQualifiedValidatorClassName)) { 87 | $this->logger->warning(sprintf('Could not find a backend validator fitting to the frontend validator "%s"', $validatorName), LogEnvironment::fromMethodName(__METHOD__)); 88 | return null; 89 | } 90 | 91 | /** @var ValidatorInterface $validator */ 92 | $validator = new $fullQualifiedValidatorClassName($validatorConfiguration); 93 | return $validator; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Classes/View/OutOfBandRenderingCapable.php: -------------------------------------------------------------------------------- 1 | setFusionPath($renderingEntryPoint); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Classes/View/OutOfBandRenderingViewFactory.php: -------------------------------------------------------------------------------- 1 | viewObjectName)) { 34 | throw new \DomainException( 35 | 'Declared view for out of band rendering (' . $this->viewObjectName . ') does not exist', 36 | 1697821296 37 | ); 38 | } 39 | $view = new $this->viewObjectName(); 40 | if (!$view instanceof AbstractView) { 41 | throw new \DomainException( 42 | 'Declared view (' . $this->viewObjectName . ') does not implement ' . AbstractView::class 43 | . ' required for out-of-band rendering', 44 | 1697821429 45 | ); 46 | } 47 | if (!$view instanceof OutOfBandRenderingCapable) { 48 | throw new \DomainException( 49 | 'Declared view (' . $this->viewObjectName . ') does not implement ' . OutOfBandRenderingCapable::class 50 | . ' required for out-of-band rendering', 51 | 1697821364 52 | ); 53 | } 54 | 55 | return $view; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Configuration/NodeTypes.yaml: -------------------------------------------------------------------------------- 1 | 'Neos.Neos:Document': 2 | postprocessors: 3 | 'CreationDialogPostprocessor': 4 | position: 'after NodeTypePresetPostprocessor' 5 | postprocessor: 'Neos\Neos\Ui\Infrastructure\ContentRepository\CreationDialog\CreationDialogNodeTypePostprocessor' 6 | ui: 7 | creationDialog: 8 | elements: 9 | title: 10 | position: 'start' 11 | properties: 12 | title: 13 | ui: 14 | showInCreationDialog: true 15 | uriPathSegment: 16 | ui: 17 | inspector: 18 | editor: "Neos.Neos/Inspector/Editors/UriPathSegmentEditor" 19 | editorOptions: 20 | title: "ClientEval:node.properties.title" 21 | options: 22 | nodeCreationHandlers: 23 | uriPathSegment: 24 | factoryClassName: 'Neos\Neos\Ui\Infrastructure\Neos\UriPathSegmentNodeCreationHandlerFactory' 25 | promotedElements: 26 | factoryClassName: 'Neos\Neos\Ui\Infrastructure\ContentRepository\CreationDialog\PromotedElementsCreationHandlerFactory' 27 | moveNodeStrategy: gatherAll 28 | 29 | 'Neos.Neos:Content': 30 | postprocessors: 31 | 'CreationDialogPostprocessor': 32 | position: 'after NodeTypePresetPostprocessor' 33 | postprocessor: 'Neos\Neos\Ui\Infrastructure\ContentRepository\CreationDialog\CreationDialogNodeTypePostprocessor' 34 | options: 35 | nodeCreationHandlers: 36 | promotedElements: 37 | factoryClassName: 'Neos\Neos\Ui\Infrastructure\ContentRepository\CreationDialog\PromotedElementsCreationHandlerFactory' 38 | moveNodeStrategy: scatter 39 | 40 | 'Neos.Neos:ContentCollection': 41 | ui: 42 | inlineEditable: true 43 | -------------------------------------------------------------------------------- /Configuration/Objects.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # InitialData Providers for booting the UI 3 | # 4 | Neos\Neos\Ui\Domain\InitialData\CacheConfigurationVersionProviderInterface: 5 | className: Neos\Neos\Ui\Infrastructure\Cache\CacheConfigurationVersionProvider 6 | 7 | Neos\Neos\Ui\Domain\InitialData\ConfigurationProviderInterface: 8 | className: Neos\Neos\Ui\Infrastructure\Configuration\ConfigurationProvider 9 | 10 | Neos\Neos\Ui\Domain\InitialData\FrontendConfigurationProviderInterface: 11 | className: Neos\Neos\Ui\Infrastructure\Configuration\FrontendConfigurationProvider 12 | 13 | Neos\Neos\Ui\Domain\InitialData\InitialStateProviderInterface: 14 | className: Neos\Neos\Ui\Infrastructure\Configuration\InitialStateProvider 15 | 16 | Neos\Neos\Ui\Domain\InitialData\MenuProviderInterface: 17 | className: Neos\Neos\Ui\Infrastructure\Neos\MenuProvider 18 | 19 | Neos\Neos\Ui\Domain\InitialData\NodeTypeGroupsAndRolesProviderInterface: 20 | className: Neos\Neos\Ui\Infrastructure\ContentRepository\NodeTypeGroupsAndRolesProvider 21 | 22 | Neos\Neos\Ui\Domain\InitialData\RoutesProviderInterface: 23 | className: Neos\Neos\Ui\Infrastructure\MVC\RoutesProvider 24 | 25 | Neos\Neos\Ui\Infrastructure\Cache\CacheConfigurationVersionProvider: 26 | properties: 27 | configurationCache: 28 | object: 29 | factoryObjectName: Neos\Flow\Cache\CacheManager 30 | factoryMethodName: getCache 31 | arguments: 32 | 1: 33 | value: Neos_Neos_Configuration_Version 34 | -------------------------------------------------------------------------------- /Configuration/Policy.yaml: -------------------------------------------------------------------------------- 1 | # # 2 | # Security policy for the Neos.Neos.Ui package # 3 | # # 4 | --- 5 | privilegeTargets: 6 | 7 | 'Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege': 8 | 9 | 'Neos.Neos.Ui:Backend.GeneralAccess': 10 | matcher: 'method(Neos\Neos\Ui\Controller\BackendController->.*())' 11 | 12 | 'Neos.Neos.Ui:Backend.ServiceAccess': 13 | matcher: 'method(Neos\Neos\Ui\Controller\BackendServiceController->.*())' 14 | 15 | 'Neos\Neos\Security\Authorization\Privilege\ModulePrivilege': 16 | 17 | 'Neos.Neos.Ui:Backend.Module.Content': 18 | matcher: 'content' 19 | 20 | roles: 21 | 22 | 'Neos.Neos:AbstractEditor': 23 | privileges: 24 | - 25 | privilegeTarget: 'Neos.Neos.Ui:Backend.Module.Content' 26 | permission: GRANT 27 | 28 | - 29 | privilegeTarget: 'Neos.Neos.Ui:Backend.GeneralAccess' 30 | permission: GRANT 31 | 32 | - 33 | privilegeTarget: 'Neos.Neos.Ui:Backend.ServiceAccess' 34 | permission: GRANT 35 | -------------------------------------------------------------------------------- /Configuration/Routes.Backend.yaml: -------------------------------------------------------------------------------- 1 | - 2 | name: 'Hostframe' 3 | uriPattern: 'content' 4 | defaults: 5 | '@controller': 'Backend' 6 | '@action': 'index' 7 | appendExceedingArguments: true 8 | 9 | - 10 | name: 'Redirect to frontend URL' 11 | uriPattern: 'redirect' 12 | defaults: 13 | '@controller': 'Backend' 14 | '@action': 'redirectTo' 15 | '@format': 'html' 16 | appendExceedingArguments: true 17 | -------------------------------------------------------------------------------- /Configuration/Routes.Service.yaml: -------------------------------------------------------------------------------- 1 | - 2 | name: 'Change' 3 | uriPattern: 'change' 4 | defaults: 5 | '@controller': 'BackendService' 6 | '@action': 'change' 7 | httpMethods: ['POST'] 8 | 9 | - 10 | name: 'Publish all changes in site' 11 | uriPattern: 'publish-changes-in-site' 12 | defaults: 13 | '@controller': 'BackendService' 14 | '@action': 'publishChangesInSite' 15 | httpMethods: ['POST'] 16 | 17 | - 18 | name: 'Publish all changes in document' 19 | uriPattern: 'publish-changes-in-document' 20 | defaults: 21 | '@controller': 'BackendService' 22 | '@action': 'publishChangesInDocument' 23 | httpMethods: ['POST'] 24 | 25 | - 26 | name: 'Discard all changes in workspace' 27 | uriPattern: 'discard-all-changes' 28 | defaults: 29 | '@controller': 'BackendService' 30 | '@action': 'discardAllChanges' 31 | httpMethods: ['POST'] 32 | 33 | - 34 | name: 'Discard all changes in site' 35 | uriPattern: 'discard-changes-in-site' 36 | defaults: 37 | '@controller': 'BackendService' 38 | '@action': 'discardChangesInSite' 39 | httpMethods: ['POST'] 40 | 41 | - 42 | name: 'Discard all changes in document' 43 | uriPattern: 'discard-changes-in-document' 44 | defaults: 45 | '@controller': 'BackendService' 46 | '@action': 'discardChangesInDocument' 47 | httpMethods: ['POST'] 48 | 49 | - 50 | name: 'Change Base Workspace' 51 | uriPattern: 'change-base-workspace' 52 | defaults: 53 | '@controller': 'BackendService' 54 | '@action': 'changeBaseWorkspace' 55 | httpMethods: ['POST'] 56 | 57 | - 58 | name: 'Sync Workspace' 59 | uriPattern: 'sync-workspace' 60 | defaults: 61 | '@controller': 'BackendService' 62 | '@action': 'syncWorkspace' 63 | httpMethods: ['POST'] 64 | - 65 | name: 'Copy nodes to clipboard' 66 | uriPattern: 'copy-nodes' 67 | defaults: 68 | '@controller': 'BackendService' 69 | '@action': 'copyNodes' 70 | httpMethods: ['POST'] 71 | 72 | - 73 | name: 'Cut nodes to clipboard' 74 | uriPattern: 'cut-nodes' 75 | defaults: 76 | '@controller': 'BackendService' 77 | '@action': 'cutNodes' 78 | httpMethods: ['POST'] 79 | 80 | - 81 | name: 'Clear clipboard' 82 | uriPattern: 'clear-clipboard' 83 | defaults: 84 | '@controller': 'BackendService' 85 | '@action': 'clearClipboard' 86 | httpMethods: ['POST'] 87 | 88 | - 89 | name: 'FlowQuery' 90 | uriPattern: 'flow-query' 91 | defaults: 92 | '@controller': 'BackendService' 93 | '@action': 'flowQuery' 94 | httpMethods: ['POST'] 95 | 96 | - 97 | name: 'Get Workspace Info' 98 | uriPattern: 'get-workspace-info' 99 | defaults: 100 | '@controller': 'BackendService' 101 | '@action': 'getWorkspaceInfo' 102 | httpMethods: ['GET'] 103 | - 104 | name: 'Get Additional Node Metadata' 105 | uriPattern: 'get-additional-node-metadata' 106 | defaults: 107 | '@controller': 'BackendService' 108 | '@action': 'getAdditionalNodeMetadata' 109 | httpMethods: ['POST'] 110 | 111 | - 112 | name: 'Generate UriPathSegment' 113 | uriPattern: 'generate-uri-path-segment' 114 | defaults: 115 | '@controller': 'BackendService' 116 | '@action': 'generateUriPathSegment' 117 | httpMethods: ['POST'] 118 | 119 | - 120 | name: 'Reload Nodes' 121 | uriPattern: 'reload-nodes' 122 | defaults: 123 | '@controller': 'BackendService' 124 | '@action': 'reloadNodes' 125 | httpMethods: ['POST'] 126 | -------------------------------------------------------------------------------- /Configuration/Routes.yaml: -------------------------------------------------------------------------------- 1 | ## 2 | # Backend 3 | 4 | - 5 | name: 'Backend' 6 | uriPattern: 'neos/' 7 | defaults: 8 | '@package': 'Neos.Neos.Ui' 9 | '@action': 'index' 10 | '@format': 'html' 11 | subRoutes: 12 | 'BackendSubRoutes': 13 | package: 'Neos.Neos.Ui' 14 | suffix: 'Backend' 15 | 16 | ## 17 | # Service 18 | 19 | - 20 | name: 'Backend' 21 | uriPattern: 'neos/ui-services/' 22 | defaults: 23 | '@package': 'Neos.Neos.Ui' 24 | '@action': 'index' 25 | '@format': 'html' 26 | subRoutes: 27 | 'ServiceSubRoutes': 28 | package: 'Neos.Neos.Ui' 29 | suffix: 'Service' 30 | -------------------------------------------------------------------------------- /Migrations/Code/Version20180907103800.php: -------------------------------------------------------------------------------- 1 | processConfiguration( 33 | 'NodeTypes', 34 | function (&$configuration) { 35 | foreach ($configuration as &$nodeType) { 36 | if (isset($nodeType['properties'])) { 37 | foreach ($nodeType['properties'] as &$propertyConfiguration) { 38 | if (isset($propertyConfiguration['ui']['aloha'])) { 39 | $editorOptions = $this->transformAlohaFormat($propertyConfiguration['ui']['aloha']); 40 | $propertyConfiguration['ui']['inline']['editorOptions'] = isset($propertyConfiguration['ui']['inline']['editorOptions']) ? array_merge_recursive($propertyConfiguration['ui']['inline']['editorOptions'], $editorOptions) : $editorOptions; 41 | unset($propertyConfiguration['ui']['aloha']); 42 | } 43 | } 44 | } 45 | } 46 | }, 47 | true 48 | ); 49 | } 50 | 51 | /** 52 | * Takes legacy aloha formatting and return editorOptions 53 | * 54 | * @param array $aloha 55 | * @return array $editorOptions 56 | */ 57 | protected function transformAlohaFormat($aloha) 58 | { 59 | $editorOptions = [ 60 | 'formatting' => [] 61 | ]; 62 | if (isset($aloha['format']) && is_array($aloha['format'])) { 63 | $editorOptions['formatting'] = array_merge($editorOptions['formatting'], $aloha['format']); 64 | } 65 | if (isset($aloha['table']) && is_array($aloha['table'])) { 66 | $editorOptions['formatting'] = array_merge($editorOptions['formatting'], $aloha['table']); 67 | } 68 | if (isset($aloha['link']) && is_array($aloha['link'])) { 69 | $editorOptions['formatting'] = array_merge($editorOptions['formatting'], $aloha['link']); 70 | } 71 | if (isset($aloha['list']) && is_array($aloha['list'])) { 72 | $editorOptions['formatting'] = array_merge($editorOptions['formatting'], $aloha['list']); 73 | } 74 | if (isset($aloha['alignment']) && is_array($aloha['alignment'])) { 75 | $editorOptions['formatting'] = array_merge($editorOptions['formatting'], $aloha['alignment']); 76 | } 77 | if (isset($aloha['autoparagraph'])) { 78 | $editorOptions['autoparagraph'] = $aloha['autoparagraph']; 79 | } 80 | if (isset($aloha['placeholder'])) { 81 | $editorOptions['placeholder'] = $aloha['placeholder']; 82 | } 83 | if (isset($editorOptions['formatting']['b'])) { 84 | $editorOptions['formatting']['strong'] = $editorOptions['formatting']['b']; 85 | unset($editorOptions['formatting']['b']); 86 | } 87 | if (isset($editorOptions['formatting']['i'])) { 88 | $editorOptions['formatting']['em'] = $editorOptions['formatting']['i']; 89 | unset($editorOptions['formatting']['i']); 90 | } 91 | if ($editorOptions['formatting'] === []) { 92 | unset($editorOptions['formatting']); 93 | } 94 | return $editorOptions; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Migrations/Code/Version20190319094900.php: -------------------------------------------------------------------------------- 1 | processConfiguration( 35 | 'NodeTypes', 36 | function (&$configuration) { 37 | foreach ($configuration as &$nodeType) { 38 | if (!isset($nodeType['properties'])) { 39 | continue; 40 | } 41 | 42 | foreach ($nodeType['properties'] as &$propertyConfiguration) { 43 | if (isset($propertyConfiguration['ui']['inline'])) { 44 | $this->transformLegacyFormatting($propertyConfiguration['ui']['inline']['editorOptions']); 45 | } 46 | } 47 | } 48 | }, 49 | true 50 | ); 51 | } 52 | 53 | /** 54 | * Takes legacy aloha formatting and return editorOptions 55 | * 56 | * @param array $editorOptions 57 | */ 58 | protected function transformLegacyFormatting(array &$editorOptions) 59 | { 60 | 61 | if (isset($editorOptions['formatting']['u'])) { 62 | $editorOptions['formatting']['underline'] = $editorOptions['formatting']['u']; 63 | unset($editorOptions['formatting']['u']); 64 | } 65 | if (isset($editorOptions['formatting']['del'])) { 66 | $editorOptions['formatting']['strikethrough'] = $editorOptions['formatting']['del']; 67 | unset($editorOptions['formatting']['del']); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Prototypes/Page.fusion: -------------------------------------------------------------------------------- 1 | // 2 | // Reset some rendering steps of the Neos backend, since the package 3 | // augments the website itself 4 | // 5 | prototype(Neos.Neos:Page) { 6 | // 7 | // Disable rendering of the Neos backend 8 | // 9 | head { 10 | 11 | javascriptBackendInformation = Neos.Neos.Ui:RenderConfiguration { 12 | path = 'documentNodeInformation' 13 | context { 14 | documentNode = ${documentNode} 15 | site = ${site} 16 | } 17 | 18 | @position = 'after javascripts' 19 | 20 | @process.json = ${Json.stringify(value)} 21 | @process.wrapInJsObject = ${''} 22 | @if.inBackend = ${renderingMode.isEdit || renderingMode.isPreview} 23 | 24 | // We need to ensure the JS backend information is always up to date, especially 25 | // when child nodes change. Otherwise errors like the following might happen: 26 | // - create a new child (document) node 27 | // - again visit the parent node 28 | // - the parent node still has the stale "children" infos (without the newly created child) 29 | // - thus, the document tree will have the newly created node REMOVED again (visually). 30 | // 31 | // as a fix, we ensure that the JS backend information creates an own cache entry, which is flushed 32 | // whenever children are modified. 33 | @cache { 34 | mode = 'cached' 35 | entryIdentifier { 36 | jsBackendInfo = 'javascriptBackendInformation' 37 | documentNode = ${Neos.Caching.entryIdentifierForNode(documentNode)} 38 | inBackend = ${renderingMode.isEdit || renderingMode.isPreview} 39 | } 40 | entryTags { 41 | 1 = ${Neos.Caching.nodeTag(documentNode)} 42 | 2 = ${(renderingMode.isEdit || renderingMode.isPreview) ? Neos.Caching.descendantOfTag(documentNode) : null} 43 | } 44 | } 45 | } 46 | 47 | guestFrameApplication = Neos.Fusion:Template { 48 | @position = 'after javascriptBackendInformation' 49 | 50 | templatePath = 'resource://Neos.Neos.Ui/Private/Templates/Backend/Guest.html' 51 | compiledResourcePackage = ${Neos.Ui.StaticResources.compiledResourcePackage()} 52 | 53 | sectionName = 'guestFrameApplication' 54 | @if.inBackend = ${renderingMode.isEdit || renderingMode.isPreview} 55 | } 56 | } 57 | 58 | neosBackendContainer = '
' 59 | neosBackendContainer.@position = 'before closingBodyTag' 60 | neosBackendContainer.@if.inBackend = ${renderingMode.isEdit || renderingMode.isPreview} 61 | 62 | neosBackendNotification = Neos.Fusion:Template { 63 | @position = 'before closingBodyTag' 64 | templatePath = 'resource://Neos.Neos.Ui/Private/Templates/Backend/GuestNotificationScript.html' 65 | @if.inBackend = ${renderingMode.isEdit || renderingMode.isPreview} 66 | } 67 | 68 | @exceptionHandler = 'Neos\\Neos\\Ui\\Fusion\\ExceptionHandler\\PageExceptionHandler' 69 | } 70 | 71 | prototype(Neos.Fusion:GlobalCacheIdentifiers) { 72 | presetBaseNodeType = ${request.arguments.presetBaseNodeType} 73 | } 74 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Prototypes/RenderConfiguration.fusion: -------------------------------------------------------------------------------- 1 | prototype(Neos.Neos.Ui:RenderConfiguration) { 2 | @class = 'Neos\\Neos\\Ui\\Fusion\\RenderConfigurationImplementation' 3 | 4 | context = Neos.Fusion:DataStructure 5 | } 6 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Root.fusion: -------------------------------------------------------------------------------- 1 | include: Prototypes/*.fusion 2 | 3 | root { 4 | # Catch all unhandled exceptions at the root 5 | @exceptionHandler = 'Neos\\Neos\\Ui\\Fusion\\ExceptionHandler\\PageExceptionHandler' 6 | } 7 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Backend/ConfigurationVersion.html: -------------------------------------------------------------------------------- 1 | {namespace neos=Neos\Neos\ViewHelpers} 2 | {neos:backend.configurationCacheVersion()} 3 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Backend/Guest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Backend/GuestNotificationScript.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Error/ErrorMessage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Neos Error 6 | 7 | {guestNotificationScript -> f:format.raw()} 8 | 9 | 10 |
{message -> f:format.raw()}
11 | 12 | 13 | -------------------------------------------------------------------------------- /Resources/Private/Translations/en/Error.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | An unkown error ocurred. 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Resources/Public/Build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neos/neos-ui/84d6069d8c66bece5f6396478875ed3979d5a34b/Resources/Public/Build/.gitkeep -------------------------------------------------------------------------------- /Resources/Public/Images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neos/neos-ui/84d6069d8c66bece5f6396478875ed3979d5a34b/Resources/Public/Images/apple-touch-icon.png -------------------------------------------------------------------------------- /Resources/Public/Images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neos/neos-ui/84d6069d8c66bece5f6396478875ed3979d5a34b/Resources/Public/Images/favicon-16x16.png -------------------------------------------------------------------------------- /Resources/Public/Images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neos/neos-ui/84d6069d8c66bece5f6396478875ed3979d5a34b/Resources/Public/Images/favicon-32x32.png -------------------------------------------------------------------------------- /Resources/Public/Images/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neos/neos-ui", 3 | "type": "neos-package", 4 | "description": "Neos CMS UI written in React", 5 | "license": [ 6 | "GPL-3.0-or-later" 7 | ], 8 | "require": { 9 | "neos/neos": "^9.0.0 || 9.0.x-dev", 10 | "neos/neos-ui-compiled": "self.version" 11 | }, 12 | "scripts": { 13 | "lint:phpstan": "../../../bin/phpstan analyse" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Neos\\Neos\\Ui\\": "Classes" 18 | } 19 | }, 20 | "archive": { 21 | "exclude": [ 22 | ".yarn" 23 | ] 24 | }, 25 | "extra": { 26 | "applied-flow-migrations": [ 27 | "TYPO3.FLOW3-201201261636", 28 | "TYPO3.Fluid-201205031303", 29 | "TYPO3.FLOW3-201205292145", 30 | "TYPO3.FLOW3-201206271128", 31 | "TYPO3.FLOW3-201209201112", 32 | "TYPO3.Flow-201209251426", 33 | "TYPO3.Flow-201211151101", 34 | "TYPO3.Flow-201212051340", 35 | "TYPO3.TypoScript-130516234520", 36 | "TYPO3.TypoScript-130516235550", 37 | "TYPO3.TYPO3CR-130523180140", 38 | "TYPO3.Neos.NodeTypes-201309111655", 39 | "TYPO3.Flow-201310031523", 40 | "TYPO3.Flow-201405111147", 41 | "TYPO3.Neos-201407061038", 42 | "TYPO3.Neos-201409071922", 43 | "TYPO3.TYPO3CR-140911160326", 44 | "TYPO3.Neos-201410010000", 45 | "TYPO3.TYPO3CR-141101082142", 46 | "TYPO3.Neos-20141113115300", 47 | "TYPO3.Fluid-20141113120800", 48 | "TYPO3.Flow-20141113121400", 49 | "TYPO3.Fluid-20141121091700", 50 | "TYPO3.Neos-20141218134700", 51 | "TYPO3.Fluid-20150214130800", 52 | "TYPO3.Neos-20150303231600", 53 | "TYPO3.TYPO3CR-20150510103823", 54 | "TYPO3.Flow-20151113161300", 55 | "TYPO3.Form-20160601101500", 56 | "TYPO3.Flow-20161115140400", 57 | "TYPO3.Flow-20161115140430", 58 | "Neos.Flow-20161124204700", 59 | "Neos.Flow-20161124204701", 60 | "Neos.Twitter.Bootstrap-20161124204912", 61 | "Neos.Form-20161124205254", 62 | "Neos.Flow-20161124224015", 63 | "Neos.Party-20161124225257", 64 | "Neos.Eel-20161124230101", 65 | "Neos.Kickstart-20161124230102", 66 | "Neos.Setup-20161124230842", 67 | "Neos.Imagine-20161124231742", 68 | "Neos.Media-20161124233100", 69 | "Neos.NodeTypes-20161125002300", 70 | "Neos.SiteKickstarter-20161125002311", 71 | "Neos.Neos-20161125002322", 72 | "Neos.ContentRepository-20161125012000", 73 | "Neos.Fusion-20161125013710", 74 | "Neos.Setup-20161125014759", 75 | "Neos.SiteKickstarter-20161125095901", 76 | "Neos.Fusion-20161125104701", 77 | "Neos.NodeTypes-20161125104800", 78 | "Neos.Neos-20161125104802", 79 | "Neos.Kickstarter-20161125110814", 80 | "Neos.Neos-20161125122412", 81 | "Neos.Flow-20161125124112", 82 | "TYPO3.FluidAdaptor-20161130112935", 83 | "Neos.Fusion-20161201202543", 84 | "Neos.Neos-20161201222211", 85 | "Neos.Fusion-20161202215034", 86 | "Neos.Fusion-20161219092345", 87 | "Neos.ContentRepository-20161219093512", 88 | "Neos.Media-20161219094126", 89 | "Neos.Neos-20161219094403", 90 | "Neos.Neos-20161219122512", 91 | "Neos.Fusion-20161219130100", 92 | "Neos.Neos-20161220163741", 93 | "Neos.Neos-20170115114620", 94 | "Neos.Fusion-20170120013047", 95 | "Neos.Flow-20170125103800", 96 | "Neos.Seo-20170127154600", 97 | "Neos.Flow-20170127183102", 98 | "Neos.Fusion-20180211175500", 99 | "Neos.Fusion-20180211184832", 100 | "Neos.Flow-20180415105700" 101 | ] 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /cssVariables.css: -------------------------------------------------------------------------------- 1 | /** 2 | * see https://github.com/neos/neos-ui/pull/3438 as to why we have in NeosUi 7.3 - 8.2 two declarations of the same set of css variables. 3 | */ 4 | 5 | /** Global CSS variables for Neos.Ui */ 6 | 7 | /** 8 | * They are compiled into the source code via our buildstack, and additionally exposed on the Host, to be consumed at runtime by plugins 9 | * 10 | * We currently dont use runtime variables in the NeosUi see: https://github.com/neos/neos-ui/issues/3201 11 | * This approach would work for the Host frame, but we inject some rendered html and css (the inline toolbar) into the guest frame where we also rely on css variables. 12 | * To not collide with consumer css variables in the iframe, we keep them compiled. 13 | * 14 | * While we do make sure that plugins are working who use the variable names, they are not marked as @api but only available for unplanned extensibility. 15 | * In the future we plan to transform the names to lowercase and might get rid of some unused/odd variables (like all the --zIndex- ones). 16 | */ 17 | :root { 18 | --spacing-GoldenUnit: 40px; 19 | --spacing-Full: 16px; 20 | --spacing-Half: 8px; 21 | --spacing-Quarter: 4px; 22 | --size-SidebarWidth: 320px; 23 | --transition-Fast: .1s; 24 | --transition-Default: .25s; 25 | --transition-Slow: .5s; 26 | --zIndex-SecondaryToolbar-LinkIconButtonFlyout: 1; 27 | --zIndex-FlashMessageContainer: 70; 28 | --zIndex-LoadingIndicatorContainer: 50; 29 | --zIndex-SecondaryInspector-Context: 1; 30 | --zIndex-SecondaryInspector-Iframe: 2; 31 | --zIndex-SecondaryInspector-Close: 3; 32 | --zIndex-SecondaryInspectorElevated: 60; 33 | --zIndex-SecondaryInspectorElevated-DropDownContents: 70; 34 | --zIndex-Dialog: 55; 35 | --zIndex-FullScreenClose-Context: 1; 36 | --zIndex-Drawer: 45; 37 | --zIndex-Bar-Context: 1; 38 | --zIndex-PrimaryToolbar: 40; 39 | --zIndex-CheckboxInput-Context: 1; 40 | --zIndex-DropdownContents-Context: 1; 41 | --zIndex-SelectBoxContents: 55; 42 | --zIndex-NotInlineEditableOverlay-Context: 1; 43 | --zIndex-CalendarFakeInputMirror-Context: 1; 44 | --zIndex-RdtPicker-Context: 1; 45 | --zIndex-SideBar-DropTargetBefore: 1; 46 | --zIndex-SideBar-DropTargetAfter: 2; 47 | --zIndex-WrapperDropdown-Context: 1; 48 | --zIndex-UnappliedChangesOverlay: 55; 49 | --zIndex-NodeToolBar: 2147483646; 50 | --fontSize-Base: 14px; 51 | --fontSize-Small: 12px; 52 | --fontsHeadings-Family: "Noto Sans"; 53 | --fontsHeadings-Style: "Regular"; 54 | --fontsHeadings-CssWeight: 400; 55 | --fontsCopy-Family: "Noto Sans"; 56 | --fontsCopy-Style: "Regular"; 57 | --fontsCopy-CssWeight: 400; 58 | --colors-PrimaryViolet: #26224C; 59 | --colors-PrimaryVioletHover: #342f5f; 60 | --colors-PrimaryBlue: #00ADEE; 61 | --colors-PrimaryBlueHover: #35c3f8; 62 | --colors-ContrastDarkest: #141414; 63 | --colors-ContrastDarker: #222; 64 | --colors-ContrastDark: #3f3f3f; 65 | --colors-ContrastNeutral: #323232; 66 | --colors-ContrastBright: #999; 67 | --colors-ContrastBrighter: #adadad; 68 | --colors-ContrastBrightest: #FFF; 69 | --colors-Success: #00a338; 70 | --colors-SuccessHover: #0bb344; 71 | --colors-Warn: #ff8700; 72 | --colors-WarnHover: #fda23d; 73 | --colors-Error: #ff460d; 74 | --colors-ErrorHover: #ff6a3c; 75 | --colors-UncheckedCheckboxTick: #5B5B5B; 76 | } 77 | -------------------------------------------------------------------------------- /cssVariables.js: -------------------------------------------------------------------------------- 1 | const { transform } = require('lightningcss'); 2 | const { readFileSync } = require("fs"); 3 | const { join } = require("path") 4 | 5 | /** @type {Record} */ 6 | const cssVariables = {} 7 | 8 | // we extract all custom variable declarations so we can use the css syntax 9 | // to declare the variables and dont need to write them in lightningcss AST ourselves 10 | transform({ 11 | code: readFileSync(join(__dirname, "cssVariables.css")), 12 | visitor: { 13 | Declaration: ({property, value}) => { 14 | if (property !== "custom") { 15 | throw new Error("Only variable declarations expected.") 16 | } 17 | cssVariables[value.name] = value.value 18 | } 19 | } 20 | }) 21 | 22 | /** 23 | * lightningcss AST visitor (plugin), to compile the used CSS variables directly into the css 24 | * 25 | * @returns {import("lightningcss").Visitor} 26 | */ 27 | const compileWithCssVariables = () => ({ 28 | Variable: (variable) => { 29 | const variableValueTokens = cssVariables[variable.name.ident]; 30 | if (!variableValueTokens) { 31 | throw new Error(`The css variable "${variable.name.ident}" cannot be compiled, because it was not declared.`); 32 | } 33 | return [ 34 | ...variableValueTokens, 35 | { 36 | // we a append a single space to the tokenList so that when the value is insertet 37 | // it wont collide with further parameters like in this case: 38 | // padding: 0 var(--spacing-GoldenUnit) 0 var(--spacing-Full); 39 | // otherwise we would get: 40 | // padding: 0 40px0 16px 41 | // |__________ here must be a space 42 | type: "token", 43 | value: { 44 | type: "delim", 45 | value: " " 46 | } 47 | } 48 | ] 49 | } 50 | }) 51 | 52 | module.exports = { compileWithCssVariables } 53 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const {sep} = require('path') 2 | const {compileWithCssVariables} = require('./cssVariables'); 3 | const {cssModules} = require('./cssModules'); 4 | const esbuild = require('esbuild'); 5 | 6 | const isProduction = process.argv.includes('--production'); 7 | const isE2ETesting = process.argv.includes('--e2e-testing'); 8 | const isWatch = process.argv.includes('--watch'); 9 | const isAnalyze = process.argv.includes('--analyze'); 10 | 11 | const NEOS_UI_VERSION = process.env.NEOS_UI_VERSION ?? (isProduction ? 'production-build' : 'dev') 12 | 13 | if (isE2ETesting) { 14 | console.log('Building for E2E testing'); 15 | } 16 | 17 | /** @type {import("esbuild").BuildOptions} */ 18 | const options = { 19 | entryPoints: { 20 | 'Host': './packages/neos-ui/src/index.js', 21 | 'HostOnlyStyles': './packages/neos-ui/src/styleHostOnly.css' 22 | }, 23 | outdir: './Resources/Public/Build', 24 | sourcemap: true, 25 | minify: isProduction, 26 | logLevel: 'info', 27 | target: 'es2020', 28 | color: true, 29 | bundle: true, 30 | keepNames: isE2ETesting, // for react magic selectors, 31 | metafile: isAnalyze, 32 | legalComments: "linked", 33 | loader: { 34 | '.js': 'tsx', 35 | '.dataurl.svg': 'dataurl', 36 | '.svg': 'text', 37 | '.vanilla-css': 'css', 38 | '.woff2': 'file' 39 | }, 40 | plugins: [ 41 | { 42 | name: 'neos-ui-build', 43 | setup: ({onResolve, onLoad, resolve}) => { 44 | // exclude CKEditor styles 45 | // the filter must match the import statement - and as one usually uses relative paths we cannot look for `@ckeditor` here 46 | // we are currently intercepting all `/\.css/` files, as this is the most accurate way and has nearly no impact on performance 47 | onResolve({filter: /\.css$/, namespace: 'file'}, ({path, ...options}) => { 48 | if (!options.importer.includes(`${sep}@ckeditor${sep}`)) { 49 | return resolve(path, {...options, namespace: 'noRecurse'}) 50 | } 51 | return { 52 | external: true, 53 | sideEffects: false 54 | } 55 | }) 56 | 57 | // prefix Fontawesome with "neos-" to prevent clashes with customer Fontawesome 58 | onLoad({filter: /@fortawesome\/fontawesome-svg-core\/styles\.css$/}, async ({path}) => { 59 | const contents = (await require('fs/promises').readFile(path)).toString(); 60 | 61 | const replacedStyle = contents.replace(/svg-inline--fa/g, 'neos-svg-inline--fa'); 62 | 63 | return { 64 | contents: replacedStyle, 65 | loader: 'css' 66 | } 67 | }) 68 | } 69 | }, 70 | cssModules( 71 | { 72 | visitor: compileWithCssVariables(), 73 | targets: { // only support es2020 browser 74 | // only supports browserList format 75 | // https://lightningcss.dev/transpilation.html 76 | // list of supported browser version per es version 77 | // https://github.com/evanw/esbuild/issues/121#issuecomment-646956379 78 | chrome: (80 << 16), // 80 79 | safari: (13 << 16) | (1 << 8), // 13.1 80 | firefox: (72 << 16), // 72 81 | edge: (80 << 16) // 80 82 | }, 83 | drafts: { 84 | nesting: true 85 | } 86 | } 87 | ) 88 | ], 89 | define: { 90 | // we dont declare `global = window` as we want to control everything and notice it, when something is odd 91 | NEOS_UI_VERSION: JSON.stringify(NEOS_UI_VERSION) 92 | } 93 | } 94 | 95 | if (isWatch) { 96 | esbuild.context(options).then((ctx) => ctx.watch()) 97 | } else { 98 | esbuild.build(options).then(result => { 99 | if (isAnalyze) { 100 | require("fs").writeFileSync('meta.json', JSON.stringify(result.metafile)) 101 | console.log("\nUpload './meta.json' to https://esbuild.github.io/analyze/ to analyze the bundle.") 102 | } 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /legacyDependencies.js: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // This is a workaround since yarns package.json`s publishConfig doesnt support "dependencies" (and this is fine btw) 4 | // I just find it odd in this super modern build stack to still have webpack and babel laying around, so you now can modifiy the package json like this: 5 | // 6 | // "neosPublishConfig": { 7 | // "legacyDependencies": { 8 | // "webpack": "^4.41.5" 9 | // } 10 | // } 11 | // 12 | // webpack will only be a "real" dependency once this package is published. 13 | // 14 | // NOTE: This is an optional hack - if it ever stops working, just remove this plugin and revert the package.json to normal dependencies 15 | // 16 | // yarn is fun :D 17 | // 18 | 19 | module.exports = { 20 | name: `legacyDependencies`, 21 | factory: require => { 22 | /** 23 | * Called before a workspace is packed. The `rawManifest` value passed in 24 | * parameter is allowed to be mutated at will, with the changes being only 25 | * applied to the packed manifest (the original one won't be mutated). 26 | * 27 | * @api 28 | * @see https://yarnpkg.com/advanced/plugin-tutorial#hook-beforeWorkspacePacking 29 | * @see https://github.com/yarnpkg/berry/blob/fb77381410b795893d25321583bf9a6d25a758f0/packages/plugin-pack/sources/index.ts#L16 30 | * 31 | * @param {*} workspace 32 | * @param {object} rawManifest 33 | * @returns {Promise | void} 34 | */ 35 | const beforeWorkspacePacking = (workspace, rawManifest) => { 36 | if (rawManifest.neosPublishConfig && rawManifest.neosPublishConfig.legacyDependencies) { 37 | rawManifest.dependencies = {...rawManifest.dependencies, ...rawManifest.neosPublishConfig.legacyDependencies} 38 | 39 | delete rawManifest.neosPublishConfig 40 | } 41 | } 42 | 43 | return { 44 | hooks: { 45 | // https://github.com/yarnpkg/berry/blob/fb77381410b795893d25321583bf9a6d25a758f0/packages/plugin-pack/sources/index.ts#L16 46 | beforeWorkspacePacking 47 | } 48 | } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /patches/isemail-npm-3.2.0-browserified.patch: -------------------------------------------------------------------------------- 1 | 2 | fixes https://github.com/neos/neos-ui/pull/3200#issuecomment-1284419262 3 | in combination with introducing the dependency "buffer" via .yarnrc.yml 4 | 5 | diff --git a/lib/index.js b/lib/index.js 6 | index 4f3d4cc33d13e68476dbe9ff0021081458af3291..087e62527d80e4e0f76773cc35a3c10b39b0a236 100644 7 | --- a/lib/index.js 8 | +++ b/lib/index.js 9 | @@ -3,7 +3,8 @@ 10 | // Load modules 11 | 12 | const Punycode = require('punycode'); 13 | -const Util = require('util'); 14 | + 15 | +const Buffer = require('buffer/').Buffer 16 | 17 | // Declare internals 18 | 19 | @@ -213,16 +214,9 @@ internals.isIterable = Array.isArray; 20 | if (typeof Symbol !== 'undefined') { 21 | internals.isIterable = (value) => Array.isArray(value) || (!!value && typeof value === 'object' && typeof value[Symbol.iterator] === 'function'); 22 | } 23 | -/* $lab:coverage:on$ */ 24 | - 25 | 26 | -// Node 10 introduced isSet and isMap, which are useful for cross-context type 27 | -// checking. 28 | -// $lab:coverage:off$ 29 | -internals._isSet = (value) => value instanceof Set; 30 | -internals._isMap = (value) => value instanceof Map; 31 | -internals.isSet = Util.types && Util.types.isSet || internals._isSet; 32 | -internals.isMap = Util.types && Util.types.isMap || internals._isMap; 33 | +internals.isSet = (value) => value instanceof Set; 34 | +internals.isMap = (value) => value instanceof Map; 35 | // $lab:coverage:on$ 36 | 37 | 38 | -------------------------------------------------------------------------------- /patches/react-codemirror2-npm-7.2.1-browserified.patch: -------------------------------------------------------------------------------- 1 | 2 | fixes https://github.com/scniro/react-codemirror2/pull/260#issuecomment-1023202972 3 | this bug has been fixed and merged to the source code of react-codemirror2, 4 | but the maintainers said that they won't release it, because they are not using the project anymore. 5 | 6 | diff --git a/index.js b/index.js 7 | index d109de477e2b4afa44836ac8da63e75542743919..823a6b014ad15b895dcaf69f88b81f6c575320bd 100644 8 | --- a/index.js 9 | +++ b/index.js 10 | @@ -63,7 +63,7 @@ exports.UnControlled = exports.Controlled = void 0; 11 | 12 | var React = require('react'); 13 | 14 | -var SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true; 15 | +var SERVER_RENDERED = false; 16 | var cm; 17 | 18 | if (!SERVER_RENDERED) { 19 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - Classes 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "packages/*/src" 4 | ], 5 | "compilerOptions": { 6 | "esModuleInterop": true, 7 | "target": "es2020", 8 | "moduleResolution": "node", 9 | "declaration": false, 10 | "experimentalDecorators": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "jsx": "react", 13 | "lib": [ 14 | "dom", 15 | "es2020", 16 | ], 17 | "noEmitOnError": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noImplicitAny": true, 20 | "noImplicitReturns": true, 21 | "noImplicitThis": true, 22 | "noUnusedLocals": true, 23 | "strictNullChecks": true, 24 | "noUnusedParameters": true, 25 | "strict": true, 26 | "pretty": true, 27 | "removeComments": true, 28 | "sourceMap": true, 29 | "incremental": true, 30 | "skipLibCheck": true 31 | } 32 | } 33 | --------------------------------------------------------------------------------