├── .nvmrc ├── .yarnrc.yml ├── Neos.Ui ├── plugin │ ├── src │ │ ├── index.js │ │ └── manifest.js │ ├── package.json │ └── esbuild.js ├── core │ ├── src │ │ ├── application │ │ │ ├── LinkTypes │ │ │ │ ├── Web │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── WebSpecification.ts │ │ │ │ │ └── WebSpecification.test.js │ │ │ │ ├── Asset │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── AssetSpecification.ts │ │ │ │ │ ├── AssetSpecification.test.js │ │ │ │ │ ├── MediaBrowser.tsx │ │ │ │ │ └── Asset.tsx │ │ │ │ ├── Node │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── NodeSpecification.ts │ │ │ │ │ └── NodeSpecification.test.js │ │ │ │ ├── MailTo │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── MailToSpecification.ts │ │ │ │ │ └── MailToSpecification.test.js │ │ │ │ ├── CustomLink │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── CustomLinkSpecification.ts │ │ │ │ │ ├── CustomLinkSpecification.test.js │ │ │ │ │ └── CustomLink.tsx │ │ │ │ ├── PhoneNumber │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── PhoneNumberSpecification.ts │ │ │ │ │ └── PhoneNumberSpecification.test.js │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── Dialog │ │ │ │ ├── index.tsx │ │ │ │ └── LinkEditor.tsx │ │ ├── framework │ │ │ ├── Process │ │ │ │ ├── index.ts │ │ │ │ └── Process.ts │ │ │ ├── Form │ │ │ │ ├── index.ts │ │ │ │ ├── Field.tsx │ │ │ │ └── EditorEnvelope.tsx │ │ │ └── index.ts │ │ ├── globals.d.ts │ │ ├── domain │ │ │ ├── Editor │ │ │ │ ├── index.ts │ │ │ │ ├── EditorAction.ts │ │ │ │ └── Editor.test.js │ │ │ ├── Link │ │ │ │ ├── Link.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── presentation │ │ │ ├── Ellipsis.tsx │ │ │ ├── index.ts │ │ │ ├── Layout.tsx │ │ │ ├── IconLabel.tsx │ │ │ ├── CardSubTitle.tsx │ │ │ ├── CardTitle.tsx │ │ │ ├── Deletable.tsx │ │ │ ├── Form.tsx │ │ │ ├── ImageCard.tsx │ │ │ ├── IconCard.tsx │ │ │ └── Modal.tsx │ │ ├── infrastructure │ │ │ └── http │ │ │ │ ├── index.ts │ │ │ │ └── getNodeSummary.ts │ │ └── index.ts │ └── package.json ├── error-handling │ ├── src │ │ ├── domain │ │ │ ├── index.ts │ │ │ └── verror.ts │ │ ├── globals.d.ts │ │ ├── presentation │ │ │ ├── index.ts │ │ │ ├── Trace.tsx │ │ │ └── Alert.tsx │ │ ├── index.ts │ │ └── application │ │ │ ├── index.ts │ │ │ ├── decodeError.ts │ │ │ └── ErrorBoundary.tsx │ └── package.json ├── link-button │ ├── src │ │ ├── globals.d.ts │ │ ├── index.tsx │ │ └── LinkButtonPreview.tsx │ └── package.json ├── neos-bridge │ ├── src │ │ ├── domain │ │ │ ├── Media │ │ │ │ ├── index.ts │ │ │ │ └── Asset.ts │ │ │ ├── Backend │ │ │ │ ├── index.ts │ │ │ │ └── Endpoints.ts │ │ │ ├── Extensibility │ │ │ │ ├── index.ts │ │ │ │ ├── GlobalRegistry.ts │ │ │ │ ├── Routes.ts │ │ │ │ ├── Translation.ts │ │ │ │ ├── NeosContext.ts │ │ │ │ ├── Configuration.ts │ │ │ │ └── Store.ts │ │ │ ├── ContentRepository │ │ │ │ ├── index.ts │ │ │ │ ├── Dimensions.ts │ │ │ │ ├── Workspace.ts │ │ │ │ └── ContextPath.ts │ │ │ └── index.ts │ │ ├── globals.d.ts │ │ ├── application │ │ │ ├── index.ts │ │ │ └── fetchWithErrorHandling.ts │ │ └── index.ts │ └── package.json ├── custom-node-tree │ ├── src │ │ ├── presentation │ │ │ ├── index.ts │ │ │ └── SearchInput.tsx │ │ ├── globals.d.ts │ │ ├── index.ts │ │ ├── application │ │ │ ├── index.ts │ │ │ ├── Search.tsx │ │ │ └── SelectNodeTypeFilter.tsx │ │ ├── domain │ │ │ ├── index.ts │ │ │ ├── NodeTypeFilterOptionDTO.ts │ │ │ └── TreeNodeDTO.ts │ │ └── infrastructure │ │ │ └── http │ │ │ ├── index.ts │ │ │ ├── getNodeTypeFilterOptions.ts │ │ │ ├── getChildrenForTreeNode.ts │ │ │ └── getTree.ts │ └── package.json └── inspector-editor │ ├── src │ ├── globals.d.ts │ └── index.tsx │ └── package.json ├── phpstan.neon ├── Docs ├── Web.png ├── Asset.png ├── Demo.gif ├── Document.png ├── Mail To.png ├── CustomLink.png ├── Link Options.png ├── Phone Number.png ├── RTE toolbar.png ├── Inspector - asset.png ├── Inspector - empty.png └── Inspector - document.png ├── .gitignore ├── docker_build.sh ├── docker_test.sh ├── Configuration ├── Settings.I18n.yaml ├── Policy.yaml ├── Settings.Neos.Flow.yaml ├── Settings.Neos.Ui.yaml └── Routes.yaml ├── .editorconfig ├── Resources ├── Private │ └── Translations │ │ ├── en │ │ ├── LinkTypes │ │ │ ├── Asset.xlf │ │ │ ├── CustomLink.xlf │ │ │ ├── Node.xlf │ │ │ ├── Web.xlf │ │ │ ├── PhoneNumber.xlf │ │ │ └── MailTo.xlf │ │ └── Main.xlf │ │ ├── pl │ │ ├── LinkTypes │ │ │ ├── Asset.xlf │ │ │ ├── Node.xlf │ │ │ ├── Web.xlf │ │ │ └── PhoneNumber.xlf │ │ └── Main.xlf │ │ ├── de │ │ ├── LinkTypes │ │ │ ├── Asset.xlf │ │ │ ├── CustomLink.xlf │ │ │ ├── Node.xlf │ │ │ ├── Web.xlf │ │ │ └── PhoneNumber.xlf │ │ └── Main.xlf │ │ └── fr │ │ ├── LinkTypes │ │ ├── Asset.xlf │ │ ├── Node.xlf │ │ ├── CustomLink.xlf │ │ ├── Web.xlf │ │ └── PhoneNumber.xlf │ │ └── Main.xlf └── Public │ └── JavaScript │ └── Plugin.js.LEGAL.txt ├── tsconfig.json ├── package.json ├── Classes ├── Application │ ├── GetNodeSummary │ │ ├── Breadcrumb.php │ │ ├── Breadcrumbs.php │ │ ├── GetNodeSummaryQueryResult.php │ │ ├── Controller │ │ │ └── GetNodeSummaryController.php │ │ ├── GetNodeSummaryQuery.php │ │ └── GetNodeSummaryQueryHandler.php │ ├── GetTree │ │ ├── GetTreeQueryResult.php │ │ ├── StartingPointWasNotFound.php │ │ └── Controller │ │ │ └── GetTreeController.php │ ├── Shared │ │ ├── TreeNodes.php │ │ ├── TreeNode.php │ │ ├── NodeWasNotFound.php │ │ └── NodeTypeNames.php │ ├── GetNodeTypeFilterOptions │ │ ├── GetNodeTypeFilterOptionsQueryResult.php │ │ ├── GetNodeTypeFilterOptionsQuery.php │ │ ├── Controller │ │ │ └── GetNodeTypeFilterOptionsController.php │ │ ├── NodeTypeFilterOption.php │ │ ├── NodeTypeFilterOptions.php │ │ └── GetNodeTypeFilterOptionsQueryHandler.php │ └── GetChildrenForTreeNode │ │ ├── GetChildrenForTreeNodeQueryResult.php │ │ ├── Controller │ │ └── GetChildrenForTreeNodeController.php │ │ ├── GetChildrenForTreeNodeQuery.php │ │ └── GetChildrenForTreeNodeQueryHandler.php ├── Infrastructure │ └── ContentRepository │ │ ├── LinkableNodeSpecification.php │ │ ├── NodeSearchSpecification.php │ │ ├── NodeSearchService.php │ │ └── NodeTypeFilter.php ├── Framework │ └── MVC │ │ ├── QueryController.php │ │ └── QueryResponse.php └── LinkToArrayForNeosUiConverter.php ├── LICENSE ├── composer.json └── .github └── workflows ├── CI-Client.yaml └── CI-Server.yml /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /Neos.Ui/plugin/src/index.js: -------------------------------------------------------------------------------- 1 | require('./manifest'); -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/Web/index.ts: -------------------------------------------------------------------------------- 1 | export {Web} from './Web'; -------------------------------------------------------------------------------- /Neos.Ui/error-handling/src/domain/index.ts: -------------------------------------------------------------------------------- 1 | export {VError} from './verror' 2 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/Asset/index.ts: -------------------------------------------------------------------------------- 1 | export {Asset} from './Asset'; -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/Node/index.ts: -------------------------------------------------------------------------------- 1 | export {Node} from './Node'; -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - Classes 5 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/MailTo/index.ts: -------------------------------------------------------------------------------- 1 | export {MailTo} from './MailTo'; -------------------------------------------------------------------------------- /Neos.Ui/link-button/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@neos-project/react-ui-components'; -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/Media/index.ts: -------------------------------------------------------------------------------- 1 | export {useAssetSummary} from './Asset'; -------------------------------------------------------------------------------- /Docs/Web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/Sitegeist.Archaeopteryx/HEAD/Docs/Web.png -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/presentation/index.ts: -------------------------------------------------------------------------------- 1 | export {SearchInput} from './SearchInput'; -------------------------------------------------------------------------------- /Neos.Ui/error-handling/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@neos-project/react-ui-components'; -------------------------------------------------------------------------------- /Neos.Ui/inspector-editor/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@neos-project/react-ui-components'; -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/Backend/index.ts: -------------------------------------------------------------------------------- 1 | export {endpoints} from './Endpoints'; 2 | -------------------------------------------------------------------------------- /Docs/Asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/Sitegeist.Archaeopteryx/HEAD/Docs/Asset.png -------------------------------------------------------------------------------- /Docs/Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/Sitegeist.Archaeopteryx/HEAD/Docs/Demo.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tsconfig.tsbuildinfo 3 | .yarn 4 | 5 | Resources/Public/JavaScript 6 | -------------------------------------------------------------------------------- /Docs/Document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/Sitegeist.Archaeopteryx/HEAD/Docs/Document.png -------------------------------------------------------------------------------- /Docs/Mail To.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/Sitegeist.Archaeopteryx/HEAD/Docs/Mail To.png -------------------------------------------------------------------------------- /Docs/CustomLink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/Sitegeist.Archaeopteryx/HEAD/Docs/CustomLink.png -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/CustomLink/index.ts: -------------------------------------------------------------------------------- 1 | export { CustomLink } from './CustomLink'; 2 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/PhoneNumber/index.ts: -------------------------------------------------------------------------------- 1 | export { PhoneNumber } from "./PhoneNumber"; 2 | -------------------------------------------------------------------------------- /Docs/Link Options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/Sitegeist.Archaeopteryx/HEAD/Docs/Link Options.png -------------------------------------------------------------------------------- /Docs/Phone Number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/Sitegeist.Archaeopteryx/HEAD/Docs/Phone Number.png -------------------------------------------------------------------------------- /Docs/RTE toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/Sitegeist.Archaeopteryx/HEAD/Docs/RTE toolbar.png -------------------------------------------------------------------------------- /Neos.Ui/error-handling/src/presentation/index.ts: -------------------------------------------------------------------------------- 1 | export {Alert} from './Alert'; 2 | export {Trace} from './Trace'; -------------------------------------------------------------------------------- /Docs/Inspector - asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/Sitegeist.Archaeopteryx/HEAD/Docs/Inspector - asset.png -------------------------------------------------------------------------------- /Docs/Inspector - empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/Sitegeist.Archaeopteryx/HEAD/Docs/Inspector - empty.png -------------------------------------------------------------------------------- /Docs/Inspector - document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/Sitegeist.Archaeopteryx/HEAD/Docs/Inspector - document.png -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/index.ts: -------------------------------------------------------------------------------- 1 | export {registerLinkTypes} from './LinkTypes'; 2 | export {registerDialog} from './Dialog'; -------------------------------------------------------------------------------- /Neos.Ui/core/src/framework/Process/index.ts: -------------------------------------------------------------------------------- 1 | export type {IProcess} from './Process'; 2 | export * as Process from './Process'; 3 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/framework/Form/index.ts: -------------------------------------------------------------------------------- 1 | export {Field, FieldGroup} from './Field'; 2 | export {EditorEnvelope} from './EditorEnvelope'; -------------------------------------------------------------------------------- /Neos.Ui/error-handling/src/index.ts: -------------------------------------------------------------------------------- 1 | export {ErrorBoundary, decodeError} from './application'; 2 | export {VError} from './domain'; 3 | -------------------------------------------------------------------------------- /Neos.Ui/error-handling/src/application/index.ts: -------------------------------------------------------------------------------- 1 | export {ErrorBoundary} from './ErrorBoundary'; 2 | export {decodeError} from './decodeError'; -------------------------------------------------------------------------------- /Neos.Ui/core/src/framework/index.ts: -------------------------------------------------------------------------------- 1 | export {IProcess, Process} from './Process'; 2 | 3 | export {Field, FieldGroup, EditorEnvelope} from './Form'; -------------------------------------------------------------------------------- /docker_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | set -e 5 | 6 | docker run -it -v $(pwd):/app -w /app node:16 sh -c "yarn && yarn build" 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docker_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | set -e 5 | 6 | docker run -it -v $(pwd):/app -w /app node:16 sh -c "yarn && yarn test" 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/Node/NodeSpecification.ts: -------------------------------------------------------------------------------- 1 | import {ILink} from "../../../domain"; 2 | 3 | export const isSuitableFor = (link: ILink) => link.href.startsWith("node://"); 4 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@neos-project/react-ui-components'; 2 | declare module '@neos-project/neos-ui-backend-connector'; 3 | declare module '@neos-project/neos-ui-editors'; -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/MailTo/MailToSpecification.ts: -------------------------------------------------------------------------------- 1 | import {ILink} from "../../../domain"; 2 | 3 | export const isSuitableFor = (link: ILink) => link.href.startsWith('mailto:'); 4 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/PhoneNumber/PhoneNumberSpecification.ts: -------------------------------------------------------------------------------- 1 | import {ILink} from "../../../domain"; 2 | 3 | export const isSuitableFor = (link: ILink) => link.href.startsWith('tel:'); 4 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@neos-project/react-ui-components'; 2 | declare module '@neos-project/neos-ui-backend-connector'; 3 | declare module '@neos-project/neos-ui-redux-store'; -------------------------------------------------------------------------------- /Neos.Ui/core/src/domain/Editor/index.ts: -------------------------------------------------------------------------------- 1 | export type {IEditor} from './Editor'; 2 | export { 3 | createEditor, 4 | EditorContext, 5 | useEditorState, 6 | useEditorTransactions 7 | } from './Editor'; -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@neos-project/react-ui-components'; 2 | declare module '@neos-project/neos-ui-backend-connector'; 3 | declare module '@neos-project/neos-ui-redux-store'; -------------------------------------------------------------------------------- /Configuration/Settings.I18n.yaml: -------------------------------------------------------------------------------- 1 | Neos: 2 | Neos: 3 | userInterface: 4 | translation: 5 | autoInclude: 6 | Sitegeist.Archaeopteryx: 7 | - 'Main' 8 | - 'LinkTypes/*' -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/Asset/AssetSpecification.ts: -------------------------------------------------------------------------------- 1 | import {ILink} from "../../../domain"; 2 | 3 | export const isSuitableFor = (link: ILink) => link.href.startsWith('asset://') && !link.href.includes('#'); 4 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/presentation/Ellipsis.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Ellipsis = styled.span` 4 | display: block; 5 | overflow: hidden; 6 | white-space: nowrap; 7 | text-overflow: ellipsis; 8 | `; 9 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/Backend/Endpoints.ts: -------------------------------------------------------------------------------- 1 | import backend from '@neos-project/neos-ui-backend-connector'; 2 | 3 | export const endpoints = () => backend.get().endpoints as { 4 | assetDetail: (assetIdentifier: string) => Promise 5 | }; 6 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/domain/Link/Link.ts: -------------------------------------------------------------------------------- 1 | export interface ILink { 2 | href: string 3 | options?: ILinkOptions 4 | } 5 | 6 | export interface ILinkOptions { 7 | anchor?: string 8 | title?: string 9 | targetBlank?: boolean 10 | relNofollow?: boolean 11 | } -------------------------------------------------------------------------------- /Neos.Ui/error-handling/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sitegeist/archaeopteryx-error-handling", 3 | "license": "GPL-3.0-or-later", 4 | "version": "", 5 | "main": "./src/index.ts", 6 | "dependencies": { 7 | "react-error-boundary": "^3.1.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/domain/Link/index.ts: -------------------------------------------------------------------------------- 1 | export type {ILink, ILinkOptions} from './Link'; 2 | 3 | export type {ILinkType} from './LinkType'; 4 | export { 5 | makeLinkType, 6 | useLinkTypes, 7 | useLinkTypeForHref, 8 | useSortedAndFilteredLinkTypes 9 | } from './LinkType'; 10 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sitegeist/archaeopteryx-neos-bridge", 3 | "license": "GPL-3.0-or-later", 4 | "version": "", 5 | "main": "./src/index.ts", 6 | "dependencies": { 7 | "react-use": "^17.2.4", 8 | "ts-toolbelt": "^9.6.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/Web/WebSpecification.ts: -------------------------------------------------------------------------------- 1 | import {ILink} from "../../../domain"; 2 | 3 | export const isSuitableFor = (link: ILink) => { 4 | const isHttp = link.href.startsWith('http://'); 5 | const isHttps = link.href.startsWith('https://'); 6 | 7 | return isHttp || isHttps; 8 | }; 9 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/presentation/index.ts: -------------------------------------------------------------------------------- 1 | export {IconCard} from './IconCard'; 2 | export {ImageCard} from './ImageCard'; 3 | export {Tabs} from './Tabs'; 4 | export {Form} from './Form'; 5 | export {Modal} from './Modal'; 6 | export {Deletable} from './Deletable'; 7 | export {IconLabel} from './IconLabel'; 8 | 9 | export * as Layout from './Layout'; -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | export { Tree } from "./application"; 9 | -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/application/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | export { Tree } from "./Tree"; 9 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/infrastructure/http/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | export { getNodeSummary } from "./getNodeSummary"; 9 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/application/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | export { fetchWithErrorHandling } from "./fetchWithErrorHandling"; 9 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export {registerLinkTypes, registerDialog} from './application'; 2 | 3 | export type {IEditor, ILinkType} from './domain'; 4 | export { 5 | makeLinkType, 6 | useLinkTypeForHref, 7 | createEditor, 8 | EditorContext, 9 | useEditorState, 10 | useEditorTransactions 11 | } from './domain'; 12 | 13 | export {Deletable} from './presentation'; -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.{yaml,yml}] 12 | indent_size = 2 13 | 14 | [{package.json,.eslintrc,.stylelintrc}] 15 | indent_size = 2 16 | 17 | [Makefile] 18 | indent_style = tab 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /Neos.Ui/error-handling/src/domain/verror.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Custom error class with chains of causes 4 | */ 5 | export class VError extends Error { 6 | constructor( 7 | message: string, 8 | private readonly previous: Error | undefined = undefined 9 | ) { 10 | super(message); 11 | }; 12 | 13 | public cause(): Error | undefined { 14 | return this.previous; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/domain/index.ts: -------------------------------------------------------------------------------- 1 | export type {ILink, ILinkOptions, ILinkType} from './Link'; 2 | export { 3 | makeLinkType, 4 | useLinkTypes, 5 | useLinkTypeForHref, 6 | useSortedAndFilteredLinkTypes 7 | } from './Link'; 8 | 9 | export type {IEditor} from './Editor'; 10 | export { 11 | createEditor, 12 | EditorContext, 13 | useEditorState, 14 | useEditorTransactions 15 | } from './Editor'; 16 | -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/domain/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | export { NodeTypeFilterOptionDTO } from "./NodeTypeFilterOptionDTO"; 9 | export { TreeNodeDTO } from "./TreeNodeDTO"; 10 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/Extensibility/index.ts: -------------------------------------------------------------------------------- 1 | export {IGlobalRegistry, useGlobalRegistry} from './GlobalRegistry'; 2 | export {useConfiguration} from './Configuration'; 3 | export {useRoutes} from './Routes'; 4 | 5 | export type {INeosContextProperties} from './NeosContext'; 6 | export {NeosContext, useNeos} from './NeosContext'; 7 | 8 | export {useSelector} from './Store'; 9 | 10 | export {useI18n} from './Translation'; 11 | -------------------------------------------------------------------------------- /Neos.Ui/link-button/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sitegeist/archaeopteryx-link-button", 3 | "license": "MIT", 4 | "version": "", 5 | "main": "./src/index.tsx", 6 | "dependencies": { 7 | "@neos-project/react-ui-components": "~8.3.0", 8 | "@sitegeist/archaeopteryx-core": "workspace:*", 9 | "react": "^17.0.2" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^4.2.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/domain/NodeTypeFilterOptionDTO.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | export type NodeTypeFilterOptionDTO = { 9 | value: string; 10 | icon: string; 11 | label: string; 12 | }; 13 | -------------------------------------------------------------------------------- /Configuration/Policy.yaml: -------------------------------------------------------------------------------- 1 | 2 | privilegeTargets: 3 | 4 | 'Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege': 5 | 6 | 'Sitegeist.Archaeopteryx:ApiAccess': 7 | matcher: 'method(Sitegeist\Archaeopteryx\Application\.*\Controller\.*Controller->processRequest())' 8 | 9 | roles: 10 | 11 | 'Neos.Neos:AbstractEditor': 12 | privileges: 13 | - privilegeTarget: 'Sitegeist.Archaeopteryx:ApiAccess' 14 | permission: GRANT 15 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/presentation/Layout.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | padding: 16px; 5 | `; 6 | 7 | export const Stack = styled.div` 8 | > * + * { 9 | margin-top: 16px; 10 | } 11 | `; 12 | 13 | export const Columns = styled.div` 14 | display: grid; 15 | gap: 16px; 16 | grid-template-columns: repeat(auto-fill, minmax(max(160px, calc(50% - 8px)), calc(33.3333% - 8px))); 17 | min-width: 600px; 18 | `; -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/Media/Asset.ts: -------------------------------------------------------------------------------- 1 | import {useAsync} from 'react-use'; 2 | 3 | import { endpoints } from '../Backend'; 4 | 5 | export interface IAssetSummary { 6 | label: string 7 | preview: string 8 | } 9 | 10 | export function useAssetSummary(assetIdentifier: string) { 11 | return useAsync(async () => { 12 | const result = await endpoints().assetDetail(assetIdentifier); 13 | return result as null | IAssetSummary; 14 | }, [assetIdentifier]); 15 | } -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/infrastructure/http/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | export { getChildrenForTreeNode } from "./getChildrenForTreeNode"; 9 | export { getNodeTypeFilterOptions } from "./getNodeTypeFilterOptions"; 10 | export { getTree } from "./getTree"; 11 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/Extensibility/GlobalRegistry.ts: -------------------------------------------------------------------------------- 1 | import {useNeos} from './NeosContext'; 2 | 3 | export interface IGlobalRegistry { 4 | get(key: string): { 5 | get: (key: string) => T 6 | getAllAsList: () => T[] 7 | set(key: string, value: any): void 8 | } | undefined 9 | set(key: string, value: any): void 10 | } 11 | 12 | export function useGlobalRegistry(): IGlobalRegistry { 13 | const neos = useNeos(); 14 | return neos.globalRegistry; 15 | } 16 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/presentation/IconLabel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import {Icon} from '@neos-project/react-ui-components'; 5 | 6 | const Container = styled.div` 7 | display: flex; 8 | align-items: center; 9 | gap: 8px; 10 | white-space: nowrap; 11 | `; 12 | 13 | export const IconLabel: React.FC<{ 14 | icon: string 15 | }> = props => ( 16 | 17 | 18 | {props.children} 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/ContentRepository/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | export {ContextPath, useSiteNodeContextPath, useDocumentNodeContextPath} from './ContextPath'; 9 | 10 | export {useDimensionValues} from './Dimensions'; 11 | 12 | export {usePersonalWorkspaceName} from './Workspace'; 13 | -------------------------------------------------------------------------------- /Configuration/Settings.Neos.Flow.yaml: -------------------------------------------------------------------------------- 1 | Neos: 2 | Flow: 3 | mvc: 4 | routes: 5 | 'Sitegeist.Archaeopteryx': 6 | position: 'before Neos.Neos' 7 | security: 8 | authentication: 9 | providers: 10 | 'Neos.Neos:Backend': 11 | requestPatterns: 12 | 'Sitegeist.Archaeopteryx:ApiControllers': 13 | pattern: ControllerObjectName 14 | patternOptions: 15 | controllerObjectNamePattern: 'Sitegeist\Archaeopteryx\Application\.*\Controller\.*Controller' 16 | -------------------------------------------------------------------------------- /Neos.Ui/inspector-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sitegeist/archaeopteryx-inspector-editor", 3 | "license": "MIT", 4 | "version": "", 5 | "main": "./src/index.tsx", 6 | "dependencies": { 7 | "@neos-project/react-ui-components": "~8.3.0", 8 | "@sitegeist/archaeopteryx-core": "workspace:*", 9 | "@sitegeist/archaeopteryx-error-handling": "workspace:*", 10 | "react": "^17.0.2", 11 | "styled-components": "^5.3.0" 12 | }, 13 | "devDependencies": { 14 | "typescript": "^4.2.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ContextPath, 3 | useSiteNodeContextPath, 4 | useDocumentNodeContextPath, 5 | useDimensionValues, 6 | usePersonalWorkspaceName, 7 | } from "./ContentRepository"; 8 | 9 | export type { INeosContextProperties, IGlobalRegistry } from "./Extensibility"; 10 | export { 11 | NeosContext, 12 | useNeos, 13 | useGlobalRegistry, 14 | useConfiguration, 15 | useRoutes, 16 | useSelector, 17 | useI18n, 18 | } from "./Extensibility"; 19 | 20 | export { useAssetSummary } from "./Media"; 21 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/CustomLink/CustomLinkSpecification.ts: -------------------------------------------------------------------------------- 1 | import {ILink} from "../../../domain"; 2 | 3 | export const isSuitableFor = (link: ILink) => { 4 | if (link.href.startsWith('asset://') && link.href.includes('#')) { 5 | // hack to allow to append assets an anchor 6 | return true; 7 | } 8 | return !link.href.startsWith('node://') && !link.href.startsWith('asset://') && !link.href.startsWith('mailto:') 9 | && !link.href.startsWith('tel:') && !link.href.startsWith('http://') && !link.href.startsWith('https://'); 10 | } 11 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/presentation/CardSubTitle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import {Ellipsis} from './Ellipsis'; 5 | 6 | const Container = styled.span` 7 | display: flex; 8 | align-items: center; 9 | line-height: 1; 10 | font-size: 12px; 11 | color: #999; 12 | grid-column: 1 / span 2; 13 | grid-row: 2 / span 1; 14 | min-width: 0; 15 | `; 16 | 17 | export const CardSubTitle: React.FC<{ 18 | }> = props => ( 19 | 20 | {props.children} 21 | 22 | ); -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sitegeist/archaeopteryx-custom-node-tree", 3 | "license": "GPL-3.0-or-later", 4 | "version": "", 5 | "main": "./src/index.ts", 6 | "dependencies": { 7 | "@neos-project/react-ui-components": "~8.3.0", 8 | "@sitegeist/archaeopteryx-error-handling": "workspace:*", 9 | "@sitegeist/archaeopteryx-neos-bridge": "workspace:*", 10 | "react": "^17.0.2", 11 | "react-use": "^17.2.4", 12 | "rxjs": "^6.6.7", 13 | "styled-components": "^5.3.0", 14 | "typesafe-actions": "^5.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Neos.Ui/plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sitegeist/archaeopteryx-plugin", 3 | "license": "MIT", 4 | "version": "", 5 | "scripts": { 6 | "build": "node esbuild.js", 7 | "watch": "node esbuild.js --watch" 8 | }, 9 | "dependencies": { 10 | "@neos-project/neos-ui-extensibility": "~8.3.0", 11 | "@sitegeist/archaeopteryx-core": "workspace:*", 12 | "@sitegeist/archaeopteryx-inspector-editor": "workspace:*", 13 | "@sitegeist/archaeopteryx-link-button": "workspace:*", 14 | "@sitegeist/archaeopteryx-neos-bridge": "workspace:*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/ContentRepository/Dimensions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | import { useSelector } from "../Extensibility/Store"; 9 | 10 | export function useDimensionValues(): null | Record { 11 | const dimensionValues = useSelector( 12 | (state) => state.cr?.contentDimensions?.active 13 | ); 14 | 15 | return dimensionValues ?? null; 16 | } 17 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/ContentRepository/Workspace.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | import { useSelector } from "../Extensibility/Store"; 9 | 10 | export function usePersonalWorkspaceName(): null | string { 11 | const personalWorkspaceName = useSelector( 12 | (state) => state.cr?.workspaces?.personalWorkspace?.name 13 | ); 14 | 15 | return personalWorkspaceName ?? null; 16 | } 17 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/presentation/CardTitle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import {Ellipsis} from './Ellipsis'; 5 | 6 | const Container = styled.span` 7 | display: flex; 8 | align-items: center; 9 | line-height: 1; 10 | grid-column: 2 / span 1; 11 | grid-row: 1 / span ${(props: { span: number; }) => props.span}; 12 | font-size: 14px; 13 | color: #FFF; 14 | min-width: 0; 15 | `; 16 | 17 | export const CardTitle: React.FC<{ 18 | span: number 19 | }> = props => ( 20 | 21 | {props.children} 22 | 23 | ); -------------------------------------------------------------------------------- /Neos.Ui/error-handling/src/application/decodeError.ts: -------------------------------------------------------------------------------- 1 | export function decodeError(error: string | Error) { 2 | if (error instanceof Error) { 3 | error.message = decodeErrorMessage(error.message); 4 | return error; 5 | } else { 6 | return new Error(decodeErrorMessage(error)); 7 | } 8 | } 9 | 10 | function decodeErrorMessage(errorMessage: string) { 11 | if (errorMessage.includes('')) { 12 | const dom = new DOMParser().parseFromString( 13 | errorMessage, 14 | 'text/html' 15 | ); 16 | 17 | return `[${dom.title}] ${dom.querySelector('h1')?.innerText}`; 18 | } 19 | 20 | return errorMessage; 21 | } -------------------------------------------------------------------------------- /Neos.Ui/error-handling/src/presentation/Trace.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Container = styled.details` 5 | ` 6 | 7 | const Title = styled.summary` 8 | cursor: pointer; 9 | ` 10 | 11 | const Content = styled.pre` 12 | height: 60vh; 13 | max-height: 600px; 14 | padding: 8px; 15 | margin: 0; 16 | overflow: scroll; 17 | background-color: #111; 18 | box-shadow: inset 5px 5px 5px #000; 19 | `; 20 | 21 | export const Trace: React.FC<{ 22 | title: string 23 | }> = props => ( 24 | 25 | {props.title} 26 | {props.children} 27 | 28 | ); -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/domain/TreeNodeDTO.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | export type TreeNodeDTO = { 9 | nodeAggregateIdentifier: string; 10 | icon: string; 11 | label: string; 12 | nodeTypeLabel: string; 13 | isMatchedByFilter: boolean; 14 | isLinkable: boolean; 15 | isDisabled: boolean; 16 | isHiddenInMenu: boolean; 17 | hasScheduledDisabledState: boolean; 18 | hasUnloadedChildren: boolean; 19 | children: TreeNodeDTO[]; 20 | }; 21 | -------------------------------------------------------------------------------- /Neos.Ui/plugin/esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const extensibilityMap = require("@neos-project/neos-ui-extensibility/extensibilityMap.json"); 3 | const isWatch = process.argv.includes('--watch'); 4 | 5 | /** @type {import("esbuild").BuildOptions} */ 6 | const options = { 7 | logLevel: "info", 8 | bundle: true, 9 | minify: !isWatch, 10 | sourcemap: "linked", 11 | legalComments: "linked", 12 | target: "es2020", 13 | entryPoints: { "Plugin": "./src/index.js" }, 14 | outdir: "../../Resources/Public/JavaScript", 15 | alias: extensibilityMap 16 | } 17 | 18 | if (isWatch) { 19 | esbuild.context(options).then((ctx) => ctx.watch()) 20 | } else { 21 | esbuild.build(options) 22 | } 23 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/presentation/Deletable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import {IconButton} from '@neos-project/react-ui-components'; 5 | 6 | const Container = styled.div` 7 | display: grid; 8 | grid-template-columns: 1fr 40px; 9 | justify-content: stretch; 10 | border: 1px solid #3f3f3f; 11 | max-width: 420px; 12 | `; 13 | 14 | const StyledIconButton = styled(IconButton)` 15 | height: 100%; 16 | `; 17 | 18 | export const Deletable: React.FC<{ 19 | onDelete(): void 20 | }> = props => ( 21 | 22 |
{props.children}
23 | 24 |
25 | ) -------------------------------------------------------------------------------- /Resources/Private/Translations/en/LinkTypes/Asset.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Asset 8 | 9 | 10 | 11 | 12 | Asset is required 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Configuration/Settings.Neos.Ui.yaml: -------------------------------------------------------------------------------- 1 | Neos: 2 | Neos: 3 | userInterface: 4 | inspector: 5 | dataTypes: 6 | Sitegeist\Archaeopteryx\Link: 7 | # the option `typeConverter` must not be specified here. 8 | # from array|string to Link the Neos\Flow\Property\TypeConverter\DenormalizingObjectConverter is used 9 | # for Link to array the NodePropertyConverterService natively just works with the JsonSerializable 10 | editor: Sitegeist.Archaeopteryx/Inspector/Editors/ValueObjectLinkEditor 11 | Ui: 12 | resources: 13 | javascript: 14 | '@sitegeist/archaeopteryx-plugin': 15 | resource: 'resource://Sitegeist.Archaeopteryx/Public/JavaScript/Plugin.js' 16 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/Extensibility/Routes.ts: -------------------------------------------------------------------------------- 1 | import {useNeos} from './NeosContext'; 2 | 3 | export interface IRoutes { 4 | core?: { 5 | modules?: { 6 | mediaBrowser?: string 7 | } 8 | } 9 | } 10 | 11 | export function useRoutes(): undefined | IRoutes; 12 | export function useRoutes( 13 | selector: (configuration: IRoutes) => R 14 | ): undefined | R; 15 | export function useRoutes(selector?: (configuration: IRoutes) => R): undefined | R { 16 | const neos = useNeos(); 17 | 18 | if (neos.routes) { 19 | if (selector) { 20 | return selector(neos.routes); 21 | } else { 22 | return neos.routes as R; 23 | } 24 | } 25 | 26 | return; 27 | } 28 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/application/fetchWithErrorHandling.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | import { fetchWithErrorHandling as _fetchWithErrorHandling } from "@neos-project/neos-ui-backend-connector"; 9 | 10 | type MakeFetchRequest = (csrf: string) => RequestInit & { url?: string }; 11 | 12 | export const fetchWithErrorHandling: { 13 | withCsrfToken(makeFetchRequest: MakeFetchRequest): Promise; 14 | parseJson(response: Body): any; 15 | generalErrorHandler(reason: string | Error): void; 16 | } = _fetchWithErrorHandling; 17 | -------------------------------------------------------------------------------- /Neos.Ui/error-handling/src/presentation/Alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import {Icon} from '@neos-project/react-ui-components'; 5 | 6 | const Container = styled.div` 7 | padding: 8px; 8 | background-color: #ff6a3c; 9 | color: #fff; 10 | 11 | > * + * { 12 | margin-top: 8px; 13 | } 14 | `; 15 | 16 | const Header = styled.header` 17 | display: flex; 18 | gap: 8px; 19 | align-items: center; 20 | `; 21 | 22 | export const Alert: React.FC<{ 23 | title: string 24 | }> = props => ( 25 | 26 |
27 | 28 | {props.title} 29 |
30 | {props.children} 31 |
32 | ); -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/Extensibility/Translation.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {useGlobalRegistry} from './GlobalRegistry'; 3 | 4 | export function useI18n() { 5 | const globalRegistry = useGlobalRegistry(); 6 | const i18nRegistry = globalRegistry.get('i18n'); 7 | 8 | return React.useMemo(() => ( 9 | idOrig: string, 10 | fallbackOrig?: string, 11 | params: Record = {}, 12 | packageKeyOrig: string = 'Neos.Neos', 13 | sourceNameOrig: string = 'Main', 14 | quantity: number = 0 15 | ) => (i18nRegistry as any).translate( 16 | idOrig, 17 | fallbackOrig, 18 | params, 19 | packageKeyOrig, 20 | sourceNameOrig, 21 | quantity 22 | ), [i18nRegistry]); 23 | } -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/Extensibility/NeosContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {IConfiguration} from './Configuration'; 4 | import {IGlobalRegistry} from './GlobalRegistry'; 5 | import {IRoutes} from './Routes'; 6 | import {IStore} from './Store'; 7 | 8 | export interface INeosContextProperties { 9 | globalRegistry: IGlobalRegistry 10 | store: IStore 11 | configuration: IConfiguration 12 | routes?: IRoutes 13 | } 14 | 15 | export const NeosContext = React.createContext(null); 16 | 17 | export function useNeos() { 18 | const neos = React.useContext(NeosContext); 19 | 20 | if (!neos) { 21 | throw new Error('[Sitegeist.Archaeopteryx]: Could not determine Neos Context.'); 22 | } 23 | 24 | return neos; 25 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "Neos.Ui/*/src" 4 | ], 5 | "compilerOptions": { 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "target": "es2020", 9 | "moduleResolution": "node", 10 | "declaration": false, 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 | } 27 | } 28 | -------------------------------------------------------------------------------- /Neos.Ui/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sitegeist/archaeopteryx-core", 3 | "license": "GPL-3.0-or-later", 4 | "version": "", 5 | "main": "./src/index.ts", 6 | "dependencies": { 7 | "@neos-project/positional-array-sorter": "~8.3.0", 8 | "@neos-project/react-ui-components": "~8.3.0", 9 | "@sitegeist/archaeopteryx-custom-node-tree": "workspace:*", 10 | "@sitegeist/archaeopteryx-error-handling": "workspace:*", 11 | "@sitegeist/archaeopteryx-neos-bridge": "workspace:*", 12 | "final-form": "^4.20.2", 13 | "libphonenumber-js": "^1.9.52", 14 | "react-final-form": "^6.5.3", 15 | "react-use": "^17.2.4", 16 | "rxjs": "^6.6.7", 17 | "ts-toolbelt": "^9.6.0", 18 | "typesafe-actions": "^5.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Neos.Ui/plugin/src/manifest.js: -------------------------------------------------------------------------------- 1 | import manifest from '@neos-project/neos-ui-extensibility'; 2 | 3 | import {registerLinkTypes, registerDialog, createEditor} from '@sitegeist/archaeopteryx-core'; 4 | import {registerInspectorEditors} from '@sitegeist/archaeopteryx-inspector-editor'; 5 | import {registerLinkButton} from '@sitegeist/archaeopteryx-link-button'; 6 | 7 | manifest('@sitegeist/archaeopteryx-plugin', {}, (globalRegistry, {store, configuration, routes}) => { 8 | const editor = createEditor(); 9 | const neosContextProperties = {globalRegistry, store, configuration, routes}; 10 | 11 | registerLinkTypes(globalRegistry); 12 | registerDialog(neosContextProperties, editor); 13 | registerInspectorEditors(neosContextProperties, editor); 14 | registerLinkButton(neosContextProperties, editor); 15 | }); 16 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | export { fetchWithErrorHandling } from "./application"; 9 | 10 | export type { 11 | INeosContextProperties, 12 | IGlobalRegistry, 13 | } from "./domain"; 14 | export { 15 | NeosContext, 16 | ContextPath, 17 | useSiteNodeContextPath, 18 | useDocumentNodeContextPath, 19 | useAssetSummary, 20 | useDimensionValues, 21 | usePersonalWorkspaceName, 22 | useGlobalRegistry, 23 | useConfiguration, 24 | useRoutes, 25 | useSelector, 26 | useI18n, 27 | useNeos, 28 | } from "./domain"; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sitegeist/archaeopteryx", 3 | "license": "MIT", 4 | "private": true, 5 | "scripts": { 6 | "lint": "tsc --noemit", 7 | "build": "yarn workspace @sitegeist/archaeopteryx-plugin run build", 8 | "watch": "yarn workspace @sitegeist/archaeopteryx-plugin run watch", 9 | "test": "node --import=tsx --test Neos.Ui" 10 | }, 11 | "workspaces": [ 12 | "Neos.Ui/*" 13 | ], 14 | "devDependencies": { 15 | "@simbathesailor/use-what-changed": "^2.0.0", 16 | "@types/react": "^17.0.3", 17 | "@types/react-dom": "^17.0.3", 18 | "@types/styled-components": "^5.1.9", 19 | "esbuild": "~0.25.0", 20 | "tsx": "^4.19.3", 21 | "typescript": "^4.2.4" 22 | }, 23 | "resolutions": { 24 | "@types/react": "^17.0.3" 25 | }, 26 | "packageManager": "yarn@3.2.0" 27 | } 28 | -------------------------------------------------------------------------------- /Resources/Private/Translations/pl/LinkTypes/Asset.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Asset 8 | Zasób 9 | 10 | 11 | 12 | 13 | Asset is required 14 | Zasób jest wymagany 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Resources/Private/Translations/de/LinkTypes/Asset.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Asset 8 | Mediendatei 9 | 10 | 11 | 12 | 13 | Asset is required 14 | Mediendatei ist erforderlich 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Classes/Application/GetNodeSummary/Breadcrumb.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Asset 8 | date des médias 9 | 10 | 11 | 12 | 13 | 14 | Asset is required 15 | fichier média est nécessaire 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/presentation/Form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const StyledForm = styled.form` 5 | > * + * { 6 | margin-top: 16px; 7 | } 8 | `; 9 | 10 | const StyledFormBody = styled.div` 11 | padding: 0 16px; 12 | `; 13 | 14 | const StyledFormActions = styled.div` 15 | display: flex; 16 | justify-content: flex-end; 17 | `; 18 | 19 | export const Form: React.FC<{ 20 | onSubmit(ev: React.SyntheticEvent): void 21 | renderBody(): React.ReactNode 22 | renderActions(): React.ReactNode 23 | }> = props => ( 24 | 25 | 26 | {props.renderBody()} 27 | 28 | 29 | {props.renderActions()} 30 | 31 | 32 | ); -------------------------------------------------------------------------------- /Classes/Application/GetTree/GetTreeQueryResult.php: -------------------------------------------------------------------------------- 1 | items = array_values($items); 29 | } 30 | 31 | public function jsonSerialize(): mixed 32 | { 33 | return $this->items; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Classes/Application/GetNodeTypeFilterOptions/GetNodeTypeFilterOptionsQueryResult.php: -------------------------------------------------------------------------------- 1 | items = array_values($items); 29 | } 30 | 31 | public function jsonSerialize(): mixed 32 | { 33 | return $this->items; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/domain/Editor/EditorAction.ts: -------------------------------------------------------------------------------- 1 | import {createAction} from 'typesafe-actions'; 2 | 3 | import {ILink, ILinkOptions} from '../Link'; 4 | 5 | export const EditorWasOpened = createAction( 6 | 'http://sitegeist.de/Sitegeist.Archaeopteryx/EditorWasOpened', 7 | ( 8 | initialValue: null | ILink, 9 | enabledLinkOptions: (keyof ILinkOptions)[], 10 | editorOptions: Record = {} 11 | ) => ({initialValue, enabledLinkOptions, editorOptions}) 12 | )(); 13 | 14 | export const EditorWasDismissed = createAction( 15 | 'http://sitegeist.de/Sitegeist.Archaeopteryx/EditorWasDismissed' 16 | )(); 17 | 18 | export const ValueWasUnset = createAction( 19 | 'http://sitegeist.de/Sitegeist.Archaeopteryx/ValueWasUnset' 20 | )(); 21 | 22 | export const ValueWasApplied = createAction( 23 | 'http://sitegeist.de/Sitegeist.Archaeopteryx/ValueWasApplied', 24 | (value: ILink) => value 25 | )(); 26 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/framework/Process/Process.ts: -------------------------------------------------------------------------------- 1 | import { AsyncState } from 'react-use/lib/useAsync'; 2 | 3 | export type IProcess = 4 | | {busy: true, error: null, result: null} 5 | | {busy: false, error: Error, result: null} 6 | | {busy: false, error: null, result: R} 7 | ; 8 | 9 | const BUSY: IProcess = {busy: true, error: null, result: null}; 10 | 11 | export function busy(): IProcess { 12 | return BUSY; 13 | } 14 | 15 | export function error(error: Error): IProcess { 16 | return {busy: false, error, result: null}; 17 | } 18 | 19 | export function success(result: R): IProcess { 20 | return {busy: false, error: null, result}; 21 | } 22 | 23 | export function fromAsyncState(asyncState: AsyncState): IProcess { 24 | return { 25 | busy: asyncState.loading, 26 | error: asyncState.error ?? null, 27 | result: asyncState.value ?? null 28 | } as IProcess; 29 | } -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/Extensibility/Configuration.ts: -------------------------------------------------------------------------------- 1 | import {useNeos} from './NeosContext'; 2 | 3 | export interface IConfiguration { 4 | nodeTree?: { 5 | loadingDepth?: number 6 | presets?: { 7 | [key: string]: { 8 | baseNodeType?: string 9 | ui?: { 10 | label?: string 11 | icon?: string 12 | } 13 | } 14 | } 15 | } 16 | } 17 | 18 | export function useConfiguration(): undefined | IConfiguration; 19 | export function useConfiguration( 20 | selector: (configuration: IConfiguration) => R 21 | ): undefined | R; 22 | export function useConfiguration(selector?: (configuration: IConfiguration) => R): R { 23 | const neos = useNeos(); 24 | 25 | if (selector) { 26 | return selector(neos.configuration); 27 | } else { 28 | return neos.configuration as R; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Classes/Infrastructure/ContentRepository/LinkableNodeSpecification.php: -------------------------------------------------------------------------------- 1 | linkableNodeTypes->isSatisfiedByNode($node); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Classes/Application/GetChildrenForTreeNode/GetChildrenForTreeNodeQueryResult.php: -------------------------------------------------------------------------------- 1 | (null); 5 | 6 | export function Field< 7 | FieldValue = any, 8 | T extends HTMLElement = HTMLElement 9 | >(props: FieldProps, T>): React.ReactElement { 10 | const groupPrefix = React.useContext(FieldGroupContext); 11 | const name = groupPrefix !== null 12 | ? `${groupPrefix}.${props.name}` 13 | : props.name; 14 | 15 | return ( {...props} name={name}/>); 16 | }; 17 | 18 | export const FieldGroup: React.FC<{ 19 | prefix: string 20 | }> = props => { 21 | return ( 22 | 23 | {props.children} 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/Dialog/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {INeosContextProperties, NeosContext} from '@sitegeist/archaeopteryx-neos-bridge'; 3 | 4 | import {createEditor, EditorContext} from '../../domain'; 5 | import {Dialog} from './Dialog'; 6 | 7 | export function registerDialog( 8 | neosContextProperties: INeosContextProperties, 9 | editor: ReturnType 10 | ): void { 11 | const {globalRegistry} = neosContextProperties; 12 | const containersRegistry = globalRegistry.get('containers'); 13 | 14 | containersRegistry?.set( 15 | 'Modals/Sitegeist.Archaeopteryx', 16 | (props: any) => ( 17 | 18 | 19 | {React.createElement(Dialog, props)} 20 | 21 | 22 | ) 23 | ); 24 | } -------------------------------------------------------------------------------- /Resources/Private/Translations/en/LinkTypes/CustomLink.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Custom Link 7 | 8 | 9 | Custom Link: 10 | 11 | 12 | Enter your text here 13 | 14 | 15 | This field is required 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/index.ts: -------------------------------------------------------------------------------- 1 | import {SynchronousRegistry} from '@neos-project/neos-ui-extensibility'; 2 | import {IGlobalRegistry} from '@sitegeist/archaeopteryx-neos-bridge'; 3 | 4 | import {Web} from './Web'; 5 | import {Node} from './Node'; 6 | import {Asset} from './Asset'; 7 | import {MailTo} from './MailTo'; 8 | import { PhoneNumber } from './PhoneNumber'; 9 | import { CustomLink } from './CustomLink'; 10 | 11 | export function registerLinkTypes(globalRegistry: IGlobalRegistry): void { 12 | const linkTypeRegistry = new SynchronousRegistry(` 13 | # Sitegeist.Archaeopteryx LinkType Registry 14 | `); 15 | 16 | linkTypeRegistry.set(Node.id, Node); 17 | linkTypeRegistry.set(Asset.id, Asset); 18 | linkTypeRegistry.set(Web.id, Web); 19 | linkTypeRegistry.set(MailTo.id, MailTo); 20 | linkTypeRegistry.set(PhoneNumber.id, PhoneNumber); 21 | linkTypeRegistry.set(CustomLink.id, CustomLink); 22 | 23 | globalRegistry.set('@sitegeist/archaeopteryx/link-types', linkTypeRegistry); 24 | } 25 | -------------------------------------------------------------------------------- /Resources/Private/Translations/en/LinkTypes/Node.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | Deleted Node 12 | 13 | 14 | 15 | Loading ... 16 | 17 | 18 | 19 | 20 | Node is required 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Resources/Private/Translations/pl/LinkTypes/Node.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | Dokument 9 | 10 | 11 | 12 | Deleted Node 13 | Usunięty węzeł 14 | 15 | 16 | 17 | 18 | Node is required 19 | Węzeł jest wymagany 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Resources/Private/Translations/fr/LinkTypes/Node.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | Document 9 | 10 | 11 | 12 | Deleted Node 13 | Node supprimé 14 | 15 | 16 | 17 | 18 | Node is required 19 | Le document est nécessaire 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Martin Ficzel , Wilhelm Behncke 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Classes/Application/GetNodeSummary/GetNodeSummaryQueryResult.php: -------------------------------------------------------------------------------- 1 | void; 16 | } 17 | 18 | export const Search: React.FC = (props) => { 19 | const [value, setValue] = React.useState(props.initialValue); 20 | const handleClear = React.useCallback(() => { 21 | setValue(""); 22 | }, [setValue]); 23 | 24 | useDebounce( 25 | () => { 26 | if (value.length !== 0 && value.length < 2) return 27 | props.onChange(value); 28 | }, 29 | 300, 30 | [value] 31 | ); 32 | 33 | return ( 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/framework/Form/EditorEnvelope.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import {FieldInputProps, FieldMetaState} from 'react-final-form'; 4 | 5 | import {EditorEnvelope as NeosEditorEnvelope} from '@neos-project/neos-ui-editors'; 6 | 7 | const IsolationLayer = styled.div` 8 | ul, li { 9 | margin: 0; 10 | padding: 0; 11 | list-style-type: none; 12 | } 13 | `; 14 | 15 | export const EditorEnvelope: React.FC<{ 16 | label: string 17 | editor: string 18 | editorOptions?: any 19 | input: FieldInputProps 20 | meta: FieldMetaState 21 | }> = props => ( 22 | 23 | 32 | 33 | ); 34 | -------------------------------------------------------------------------------- /Configuration/Routes.yaml: -------------------------------------------------------------------------------- 1 | - 2 | name: 'GetChildrenForTreeNode Query' 3 | uriPattern: 'neos/archaeopteryx/get-children-for-tree-node' 4 | defaults: 5 | '@package': 'Sitegeist.Archaeopteryx' 6 | '@subpackage': 'Application\GetChildrenForTreeNode' 7 | '@controller': 'GetChildrenForTreeNode' 8 | '@action': 'ignored' 9 | 10 | - 11 | name: 'GetNodeSummary Query' 12 | uriPattern: 'neos/archaeopteryx/get-node-summary' 13 | defaults: 14 | '@package': 'Sitegeist.Archaeopteryx' 15 | '@subpackage': 'Application\GetNodeSummary' 16 | '@controller': 'GetNodeSummary' 17 | '@action': 'ignored' 18 | 19 | - 20 | name: 'GetNodeTypeFilterOptions Query' 21 | uriPattern: 'neos/archaeopteryx/get-node-type-filter-options' 22 | defaults: 23 | '@package': 'Sitegeist.Archaeopteryx' 24 | '@subpackage': 'Application\GetNodeTypeFilterOptions' 25 | '@controller': 'GetNodeTypeFilterOptions' 26 | '@action': 'ignored' 27 | 28 | - 29 | name: 'GetTree Query' 30 | uriPattern: 'neos/archaeopteryx/get-tree' 31 | defaults: 32 | '@package': 'Sitegeist.Archaeopteryx' 33 | '@subpackage': 'Application\GetTree' 34 | '@controller': 'GetTree' 35 | '@action': 'ignored' 36 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/presentation/ImageCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import {CardTitle} from './CardTitle'; 5 | 6 | const Container = styled.div` 7 | display: grid; 8 | grid-gap: 8px; 9 | grid-template-columns: 75px 1fr; 10 | background-color: #141414; 11 | padding: 0 16px 0 0; 12 | `; 13 | 14 | const Image = styled.img` 15 | display: block; 16 | grid-column: 1 / span 1; 17 | width: 100%; 18 | height: 56px; 19 | object-fit: contain; 20 | background-color: #fff; 21 | background-size: 10px 10px; 22 | background-position: 0 0, 25px 25px; 23 | background-image: linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%, #cccccc), linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%, #cccccc); 24 | `; 25 | 26 | interface Props { 27 | label: string 28 | src: string 29 | } 30 | 31 | export const ImageCard: React.FC = props => { 32 | return ( 33 | 34 | 35 | 36 | {props.label} 37 | 38 | 39 | ); 40 | } -------------------------------------------------------------------------------- /Resources/Private/Translations/de/LinkTypes/CustomLink.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Custom Link 7 | Beliebiger Link 8 | 9 | 10 | Custom Link: 11 | Beliebiger Link: 12 | 13 | 14 | Enter your text here 15 | Hier ihren Text eingeben 16 | 17 | 18 | This field is required 19 | Dieses Feld ist erforderlich 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Resources/Private/Translations/fr/LinkTypes/CustomLink.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Custom Link 7 | Lien personnalisé 8 | 9 | 10 | Custom Link: 11 | Lien personnalisé: 12 | 13 | 14 | Enter your text here 15 | Entrez votre texte ici 16 | 17 | 18 | This field is required 19 | Ce champ est nécessaire 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Resources/Private/Translations/en/LinkTypes/Web.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Web 8 | 9 | 10 | Link 11 | 12 | 13 | 14 | 15 | Protocol is required 16 | 17 | 18 | 19 | 20 | e.g. www.example.com 21 | 22 | 23 | URL is required 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Resources/Private/Translations/de/LinkTypes/Node.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | Dokument 9 | 10 | 11 | 12 | Deleted Node 13 | Gelöschte Node 14 | 15 | 16 | 17 | Loading ... 18 | Laden ... 19 | 20 | 21 | 22 | 23 | Node is required 24 | Eine Seite ist erforderlich 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Classes/Application/GetNodeTypeFilterOptions/GetNodeTypeFilterOptionsQuery.php: -------------------------------------------------------------------------------- 1 | $array 30 | */ 31 | public static function fromArray(array $array): self 32 | { 33 | isset($array['baseNodeTypeFilter']) 34 | or throw new \InvalidArgumentException('Base node type filter must be set'); 35 | is_string($array['baseNodeTypeFilter']) 36 | or throw new \InvalidArgumentException('Base node type filter must be a string'); 37 | 38 | return new self( 39 | baseNodeTypeFilter: $array['baseNodeTypeFilter'], 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Resources/Public/JavaScript/Plugin.js.LEGAL.txt: -------------------------------------------------------------------------------- 1 | Bundled license information: 2 | 3 | react-is/cjs/react-is.production.min.js: 4 | /** @license React v16.13.1 5 | * react-is.production.min.js 6 | * 7 | * Copyright (c) Facebook, Inc. and its affiliates. 8 | * 9 | * This source code is licensed under the MIT license found in the 10 | * LICENSE file in the root directory of this source tree. 11 | */ 12 | 13 | tslib/tslib.es6.js: 14 | /*! ***************************************************************************** 15 | Copyright (c) Microsoft Corporation. 16 | 17 | Permission to use, copy, modify, and/or distribute this software for any 18 | purpose with or without fee is hereby granted. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 21 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 22 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 23 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 24 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 25 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 26 | PERFORMANCE OF THIS SOFTWARE. 27 | ***************************************************************************** */ 28 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/presentation/IconCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import {Icon} from '@neos-project/react-ui-components'; 5 | import {CardTitle} from './CardTitle'; 6 | import {CardSubTitle} from './CardSubTitle'; 7 | 8 | const Container = styled.div` 9 | display: grid; 10 | grid-gap: 8px; 11 | grid-template-columns: 20px 1fr; 12 | grid-template-rows: repeat(2, 1fr); 13 | background-color: #141414; 14 | padding: 8px 16px; 15 | min-height: 50px; 16 | `; 17 | 18 | const IconWrapper = styled.span` 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | grid-column: 1 / span 1; 23 | grid-row: 1 / span ${(props: {span: number}) => props.span}; 24 | `; 25 | 26 | interface Props { 27 | icon: string; 28 | title: string; 29 | subTitle?: string; 30 | } 31 | 32 | export const IconCard: React.FC = props => { 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | {props.title} 40 | 41 | 42 | {props.subTitle} 43 | 44 | 45 | ); 46 | } -------------------------------------------------------------------------------- /Classes/Application/GetNodeTypeFilterOptions/Controller/GetNodeTypeFilterOptionsController.php: -------------------------------------------------------------------------------- 1 | queryHandler->handle($query); 31 | 32 | return QueryResponse::success($queryResult); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Classes/Application/GetTree/StartingPointWasNotFound.php: -------------------------------------------------------------------------------- 1 | getProperties(), JSON_PRETTY_PRINT), 39 | ), 40 | 1715082893 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/Framework/MVC/QueryController.php: -------------------------------------------------------------------------------- 1 | setDispatched(true); 24 | $response->setContentType('application/json'); 25 | 26 | try { 27 | $queryResponse = $this->processQuery($request->getArguments()); 28 | } catch (\InvalidArgumentException $e) { 29 | $queryResponse = QueryResponse::clientError($e); 30 | } catch (\Exception $e) { 31 | $queryResponse = QueryResponse::serverError($e); 32 | } 33 | 34 | $queryResponse->applyToActionResponse($response); 35 | } 36 | 37 | /** 38 | * @param array $arguments 39 | */ 40 | abstract public function processQuery(array $arguments): QueryResponse; 41 | } 42 | -------------------------------------------------------------------------------- /Classes/Application/Shared/TreeNode.php: -------------------------------------------------------------------------------- 1 | ; 17 | }; 18 | }; 19 | ui?: { 20 | pageTree?: { 21 | query?: string; 22 | filterNodeType?: string; 23 | }; 24 | }; 25 | system?: { 26 | authenticationTimeout?: boolean; 27 | }; 28 | } 29 | 30 | export interface IStore { 31 | getState(): IState; 32 | subscribe(listener: () => void): () => void; 33 | } 34 | 35 | export function useSelector(selector: (state: IState) => R): R { 36 | const neos = useNeos(); 37 | const [result, setResult] = React.useState( 38 | selector(neos.store.getState()) 39 | ); 40 | 41 | React.useEffect( 42 | () => 43 | neos.store.subscribe(() => { 44 | const state = neos.store.getState(); 45 | const result = selector(state); 46 | 47 | setResult(result); 48 | }), 49 | [] 50 | ); 51 | 52 | return result; 53 | } 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sitegeist/archaeopteryx", 3 | "type": "neos-plugin", 4 | "description": "The missing link editor for Neos", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Martin Ficzel", 9 | "email": "ficzel@sitegeist.de", 10 | "role": "Developer" 11 | }, 12 | { 13 | "name": "Wilhelm Behncke", 14 | "email": "behncke@sitegeist.de", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "neos/neos": "^7.0 || ^8.0" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^9.4", 24 | "phpstan/phpstan": "^1.10", 25 | "neos/buildessentials": "^6.3", 26 | "mikey179/vfsstream": "^1.6", 27 | "squizlabs/php_codesniffer": "^3.6" 28 | }, 29 | "scripts": { 30 | "lint:phpstan": "../../../bin/phpstan analyse", 31 | "lint:phpcs": "../../../bin/phpcs --standard=PSR12 --extensions=php --exclude=Generic.Files.LineLength Classes/", 32 | "lint": [ 33 | "@lint:phpstan", 34 | "@lint:phpcs" 35 | ] 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Sitegeist\\Archaeopteryx\\": "Classes" 40 | } 41 | }, 42 | "extra": { 43 | "neos": { 44 | "package-key": "Sitegeist.Archaeopteryx" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Classes/Application/GetTree/Controller/GetTreeController.php: -------------------------------------------------------------------------------- 1 | queryHandler->handle($query); 33 | 34 | return QueryResponse::success($queryResult); 35 | } catch (StartingPointWasNotFound $e) { 36 | return QueryResponse::clientError($e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Classes/Application/Shared/NodeWasNotFound.php: -------------------------------------------------------------------------------- 1 | getProperties(), JSON_PRETTY_PRINT), 39 | ), 40 | 1715082627 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Resources/Private/Translations/en/LinkTypes/PhoneNumber.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Phone Number 7 | 8 | 9 | Phone Number: 10 | 11 | 12 | your phone number without country code 13 | 14 | 15 | A phone number is required 16 | 17 | 18 | Please select a valid country calling code 19 | 20 | 21 | Please enter only number values 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Classes/Application/GetNodeSummary/Controller/GetNodeSummaryController.php: -------------------------------------------------------------------------------- 1 | queryHandler->handle($query); 33 | 34 | return QueryResponse::success($queryResult); 35 | } catch (NodeWasNotFound $e) { 36 | return QueryResponse::clientError($e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Classes/LinkToArrayForNeosUiConverter.php: -------------------------------------------------------------------------------- 1 | $convertedChildProperties 31 | */ 32 | public function convertFrom($source, $targetType, array $convertedChildProperties = [], ?PropertyMappingConfigurationInterface $configuration = null) 33 | { 34 | if (!$source instanceof Link) { 35 | throw new \InvalidArgumentException('Expected argument $source to be of type Link, got: ' . get_debug_type($source), 1697972165); 36 | } 37 | return $source->jsonSerialize(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Resources/Private/Translations/pl/LinkTypes/Web.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Web 8 | Sieć 9 | 10 | 11 | Link 12 | Link 13 | 14 | 15 | 16 | 17 | Protocol is required 18 | Protokół jest wymagany 19 | 20 | 21 | 22 | 23 | e.g. www.example.com 24 | np. www.przyklad.pl 25 | 26 | 27 | URL is required 28 | URL jest wymagany 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Classes/Application/GetChildrenForTreeNode/Controller/GetChildrenForTreeNodeController.php: -------------------------------------------------------------------------------- 1 | queryHandler->handle($query); 33 | 34 | return QueryResponse::success($queryResult); 35 | } catch (NodeWasNotFound $e) { 36 | return QueryResponse::clientError($e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Resources/Private/Translations/de/LinkTypes/Web.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Web 8 | Web 9 | 10 | 11 | Link 12 | Link 13 | 14 | 15 | 16 | 17 | Protocol is required 18 | Protokoll ist erforderlich 19 | 20 | 21 | 22 | 23 | e.g. www.example.com 24 | z.B. www.example.com 25 | 26 | 27 | URL is required 28 | URL ist erforderlich 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Resources/Private/Translations/fr/LinkTypes/Web.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Web 8 | Web 9 | 10 | 11 | Link 12 | Lien 13 | 14 | 15 | 16 | 17 | Protocol is required 18 | Le protocole est nécessaire 19 | 20 | 21 | 22 | 23 | e.g. www.example.com 24 | p. ex. www.example.com 25 | 26 | 27 | URL is required 28 | L'URL est nécessaire 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/presentation/Modal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import styled from 'styled-components'; 4 | 5 | import {Dialog} from '@neos-project/react-ui-components'; 6 | 7 | // 8 | // Dear Reader: 9 | // The styled component below this comment is a desparate hack 10 | // to let the native Neos UI Dialog position in center, 11 | // calculate its width by its contents and allow for more height, 12 | // which is behavior that the Dialog does not anticipate. 13 | // 14 | // This is a **VERY FRAGILE** hack, so please do not copy it. A 15 | // fix for the native Dialog in Neos UI will follow soon. 16 | // 17 | const StyledDialog = styled(Dialog)` 18 | z-index: 69; 19 | 20 | [class*="_dialog__contentsPosition "], 21 | [class$="_dialog__contentsPosition"] { 22 | top: 50%; 23 | transform: translateY(-50%)translateX(-50%)scale(1); 24 | } 25 | [class*="_dialog__contents "], 26 | [class$="_dialog__contents"] { 27 | width: auto; 28 | max-width: calc(100vw - 40px * 2); 29 | } 30 | [class*="_dialog__body "], 31 | [class$="_dialog__body"] { 32 | max-height: 80vh; 33 | } 34 | `; 35 | 36 | export const Modal: React.FC<{ 37 | renderTitle(): React.ReactNode 38 | renderBody(): React.ReactNode 39 | }> = props => ReactDOM.createPortal( 40 | {}} 44 | preventClosing 45 | > 46 | {props.renderBody()} 47 | , 48 | document.body 49 | ); 50 | -------------------------------------------------------------------------------- /.github/workflows/CI-Client.yaml: -------------------------------------------------------------------------------- 1 | name: CI Client 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ main, '[0-9]+.[0-9]' ] 6 | pull_request: 7 | branches: [ main, '[0-9]+.[0-9]' ] 8 | 9 | jobs: 10 | 11 | client-build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Enable Corepack 16 | run: corepack enable 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 18 20 | cache: 'yarn' 21 | - name: Install node modules 22 | run: yarn 23 | - name: Lint 24 | run: yarn lint 25 | - name: Run build 26 | run: yarn build 27 | - name: Push build files to GitHub 28 | shell: bash 29 | run: | 30 | set -ex 31 | 32 | BASE_BRANCH_PATTERN="^refs\/heads\/(main|[0-9]+\.[0-9])$" 33 | if ! [[ "$GITHUB_REF" =~ $BASE_BRANCH_PATTERN ]]; then 34 | echo "not pushing because ref is $GITHUB_REF" 35 | exit 0 36 | fi 37 | 38 | set +e 39 | git status -s | grep Resources/Public/JavaScript 40 | if [ $? -eq 0 ]; then 41 | set -e 42 | git config --global user.name "github-actions[bot]" 43 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 44 | git add -f Resources/Public/JavaScript 45 | git commit -m "Update: Build frontend files for $GITHUB_SHA on $GITHUB_REF_NAME" 46 | git push 47 | else 48 | set -e 49 | echo "No changes since last run" 50 | fi 51 | 52 | - name: Run test 53 | run: yarn test 54 | -------------------------------------------------------------------------------- /Neos.Ui/link-button/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {SynchronousRegistry} from '@neos-project/neos-ui-extensibility'; 3 | 4 | import {INeosContextProperties, NeosContext} from '@sitegeist/archaeopteryx-neos-bridge'; 5 | import {IEditor, EditorContext} from '@sitegeist/archaeopteryx-core'; 6 | 7 | import {LinkButton} from './LinkButton'; 8 | 9 | export function registerLinkButton( 10 | neosContextProperties: INeosContextProperties, 11 | editor: IEditor 12 | ): void { 13 | const {globalRegistry} = neosContextProperties; 14 | const ckeditor5Registry = globalRegistry.get('ckEditor5'); 15 | if (!ckeditor5Registry) { 16 | console.warn('[Sitegeist.Archaeopteryx]: Could not find ckeditor5 registry.'); 17 | console.warn('[Sitegeist.Archaeopteryx]: Skipping registration of RTE formatter...'); 18 | return; 19 | } 20 | 21 | const richtextToolbarRegistry = ckeditor5Registry.get>('richtextToolbar'); 22 | if (!richtextToolbarRegistry) { 23 | console.warn('[Sitegeist.Archaeopteryx]: Could not find ckeditor5 richtextToolbar registry.'); 24 | console.warn('[Sitegeist.Archaeopteryx]: Skipping registration of RTE formatter...'); 25 | return; 26 | } 27 | 28 | richtextToolbarRegistry.set('link', { 29 | commandName: 'link', 30 | component: (props: any) => ( 31 | 32 | 33 | {React.createElement(LinkButton, props)} 34 | 35 | 36 | ), 37 | isVisible: (config: any) => Boolean(config && config.formatting && config.formatting.a) 38 | }); 39 | } -------------------------------------------------------------------------------- /Classes/Application/Shared/NodeTypeNames.php: -------------------------------------------------------------------------------- 1 | items = array_values($items); 31 | } 32 | 33 | /** 34 | * @param array $array 35 | */ 36 | public static function fromArray(array $array): self 37 | { 38 | $items = []; 39 | foreach ($array as $nodeTypeNameAsString) { 40 | is_string($nodeTypeNameAsString) 41 | or throw new \InvalidArgumentException('Node type name must be a string'); 42 | 43 | $items[] = NodeTypeName::fromString($nodeTypeNameAsString); 44 | } 45 | 46 | return new self(...$items); 47 | } 48 | 49 | public function includesSuperTypeOf(NodeType $nodeType): bool 50 | { 51 | if (empty($this->items)) { 52 | return true; 53 | } 54 | 55 | foreach ($this->items as $item) { 56 | if ($nodeType->isOfType((string) $item)) { 57 | return true; 58 | } 59 | } 60 | 61 | return false; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/MailTo/MailToSpecification.test.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from 'node:test'; 2 | import {equal} from 'node:assert/strict'; 3 | 4 | import {isSuitableFor} from './MailToSpecification'; 5 | 6 | describe('LinkType: MailTo', () => { 7 | it('is not satisfied by http:// links', () => { 8 | const link = { 9 | href: 'http://www.example.com' 10 | }; 11 | 12 | equal(isSuitableFor(link), false); 13 | }); 14 | 15 | it('is not satisfied by https:// links', () => { 16 | const link = { 17 | href: 'https://www.example.com' 18 | }; 19 | 20 | equal(isSuitableFor(link), false); 21 | }); 22 | 23 | it('is not satisfied by node:// links', () => { 24 | const link = { 25 | href: 'node://97c9a6e3-4b50-4559-9f60-b5ad68f25758' 26 | }; 27 | 28 | equal(isSuitableFor(link), false); 29 | }); 30 | 31 | it('is not satisfied by asset:// links', () => { 32 | const link = { 33 | href: 'asset://97c9a6e3-4b50-4559-9f60-b5ad68f25758' 34 | }; 35 | 36 | equal(isSuitableFor(link), false); 37 | }); 38 | 39 | it('is satisfied by mailto: links', () => { 40 | const link = { 41 | href: 'mailto:foo@example.com' 42 | }; 43 | 44 | equal(isSuitableFor(link), true); 45 | }); 46 | 47 | it('is not satisfied by invalid links', () => { 48 | const link = { 49 | href: 'Think of Beethoven\'s 5th: foo foo foo bar' 50 | }; 51 | 52 | equal(isSuitableFor(link), false); 53 | }); 54 | 55 | it('is not satisfied by tel: links', () => { 56 | const link = { 57 | href: 'tel:+491258795857' 58 | }; 59 | 60 | equal(isSuitableFor(link), false); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/PhoneNumber/PhoneNumberSpecification.test.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from 'node:test'; 2 | import {equal} from 'node:assert/strict'; 3 | 4 | import {isSuitableFor} from './PhoneNumberSpecification'; 5 | 6 | describe('LinkType: PhoneNumber', () => { 7 | it('is not satisfied by http:// links', () => { 8 | const link = { 9 | href: 'http://www.example.com' 10 | }; 11 | 12 | equal(isSuitableFor(link), false); 13 | }); 14 | 15 | it('is not satisfied by https:// links', () => { 16 | const link = { 17 | href: 'https://www.example.com' 18 | }; 19 | 20 | equal(isSuitableFor(link), false); 21 | }); 22 | 23 | it('is not satisfied by node:// links', () => { 24 | const link = { 25 | href: 'node://97c9a6e3-4b50-4559-9f60-b5ad68f25758' 26 | }; 27 | 28 | equal(isSuitableFor(link), false); 29 | }); 30 | 31 | it('is not satisfied by asset:// links', () => { 32 | const link = { 33 | href: 'asset://97c9a6e3-4b50-4559-9f60-b5ad68f25758' 34 | }; 35 | 36 | equal(isSuitableFor(link), false); 37 | }); 38 | 39 | it('is not satisfied by mailto: links', () => { 40 | const link = { 41 | href: 'mailto:foo@example.com' 42 | }; 43 | 44 | equal(isSuitableFor(link), false); 45 | }); 46 | 47 | it('is not satisfied by invalid links', () => { 48 | const link = { 49 | href: 'Think of Beethoven\'s 5th: foo foo foo bar' 50 | }; 51 | 52 | equal(isSuitableFor(link), false); 53 | }); 54 | 55 | it('is satisfied by tel: links', () => { 56 | const link = { 57 | href: 'tel:+491258795857' 58 | }; 59 | 60 | equal(isSuitableFor(link), true); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /Resources/Private/Translations/de/LinkTypes/PhoneNumber.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Phone Number 7 | Telefonnummer 8 | 9 | 10 | Phone Number: 11 | Telefonnummer: 12 | 13 | 14 | your phone number without country code 15 | Ihre Rufnummer ohne Landesvorwahl 16 | 17 | 18 | A phone number is required 19 | Eine Telefonnummer ist erforderlich 20 | 21 | 22 | Please select a valid country calling code 23 | Bitte wählen Sie eine gültige Landesvorwahl 24 | 25 | 26 | Please enter only number values 27 | Bitte nur Zahlenwerte eingeben 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Resources/Private/Translations/pl/LinkTypes/PhoneNumber.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Phone Number 7 | Numer telefonu 8 | 9 | 10 | Phone Number: 11 | Numer telefonu: 12 | 13 | 14 | your phone number without area code 15 | twój numer telefonu bez kodu kierunkowego 16 | 17 | 18 | A phone number is required 19 | Numer telefonu jest wymagany 20 | 21 | 22 | Please select a valid country calling code 23 | Proszę wybrać prawidłowy kod kierunkowy kraju 24 | 25 | 26 | Please enter only number values 27 | Wprowadź tylko wartości liczbowe 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Resources/Private/Translations/fr/LinkTypes/PhoneNumber.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Phone Number 7 | Numéro de téléphone 8 | 9 | 10 | Phone Number: 11 | Numéro de téléphone: 12 | 13 | 14 | your phone number without country code 15 | votre numéro de téléphone sans l'indicatif du pays 16 | 17 | 18 | A phone number is required 19 | Un numéro de téléphone est requis 20 | 21 | 22 | Please select a valid country calling code 23 | Veuillez sélectionner un code d'appel de pays valide 24 | 25 | 26 | Please enter only number values 27 | Veuillez saisir uniquement des valeurs numériques 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/presentation/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import {TextInput, Icon, IconButton} from '@neos-project/react-ui-components'; 5 | 6 | import {useI18n} from '@sitegeist/archaeopteryx-neos-bridge'; 7 | 8 | const SearchIcon = styled(Icon)` 9 | position: absolute; 10 | top: 50%; 11 | left: 21px; 12 | transform: translate(-50%, -50%); 13 | `; 14 | 15 | const ClearIcon = styled(IconButton)` 16 | position: absolute; 17 | top: 0; 18 | right: 0; 19 | color: #000; 20 | `; 21 | 22 | const StyledTextInput = styled(TextInput)` 23 | padding-left: 42px; 24 | 25 | &:focus { 26 | background: #3f3f3f; 27 | color: #fff; 28 | } 29 | `; 30 | 31 | const SearchInputContainer = styled.div` 32 | position: relative; 33 | `; 34 | 35 | interface Props { 36 | value: string 37 | onChange: (value: string) => void 38 | onClear: () => void 39 | } 40 | 41 | export const SearchInput: React.FC = props => { 42 | const i18n = useI18n(); 43 | const latestValue = React.useRef(props.value); 44 | 45 | React.useEffect(() => { 46 | if (latestValue.current !== props.value && !props.value) { 47 | props.onClear(); 48 | } 49 | 50 | latestValue.current = props.value; 51 | }, [props.value]) 52 | 53 | return ( 54 | 55 | 56 | 62 | {props.value && ( 63 | 67 | )} 68 | 69 | ) 70 | }; -------------------------------------------------------------------------------- /Neos.Ui/error-handling/src/application/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {FallbackProps, ErrorBoundary as ReactErrorBoundary} from 'react-error-boundary'; 3 | import {VError} from '@sitegeist/archaeopteryx-error-handling'; 4 | 5 | import {Alert, Trace} from '../presentation'; 6 | 7 | const ErrorFallback: React.FC = props => { 8 | const isDev = Boolean(document.querySelector('[data-env^="Development"]')); 9 | 10 | return ( 11 | 12 |
13 | {props.error.message} 14 |
15 | {props.error instanceof VError ? ( 16 | 17 | ) : null} 18 | {isDev ? ( 19 | 20 | {props.error.stack} 21 | 22 | ) : null} 23 |
24 | ); 25 | } 26 | 27 | const RecursiveCauseChain: React.FC<{ 28 | error: undefined | Error 29 | level: number 30 | }> = props => props.error ? ( 31 | 32 | {props.error.message} 33 | {props.error instanceof VError && props.level < 10 ? ( 34 | 35 | ) : null} 36 | 37 | ) : null; 38 | 39 | function logError(error: Error, info: { componentStack: string }) { 40 | console.warn(`[Sitegeist.Archaeopteryx::${error.name}]: An error occurred.`); 41 | console.error(`[Sitegeist.Archaeopteryx::${error.name}]: `, error); 42 | console.error(`[Sitegeist.Archaeopteryx::${error.name}]: Component Stack:`, info.componentStack); 43 | } 44 | 45 | export const ErrorBoundary: React.FC = props => ( 46 | 47 | {props.children} 48 | 49 | ); 50 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/Web/WebSpecification.test.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from 'node:test'; 2 | import {equal} from 'node:assert/strict'; 3 | 4 | 5 | import {isSuitableFor} from './WebSpecification'; 6 | 7 | describe('LinkType: Web', () => { 8 | it('is satisfied by http:// links', () => { 9 | const link = { 10 | href: 'http://www.example.com' 11 | }; 12 | 13 | equal(isSuitableFor(link), true); 14 | }); 15 | 16 | it('is satisfied by https:// links', () => { 17 | const link = { 18 | href: 'https://www.example.com' 19 | }; 20 | 21 | equal(isSuitableFor(link), true); 22 | }); 23 | 24 | it('is satisfied by https:// links with a hash', () => { 25 | const link = { 26 | href: 'https://www.example.com#section' 27 | }; 28 | 29 | equal(isSuitableFor(link), true); 30 | }); 31 | 32 | it('is not satisfied by node:// links', () => { 33 | const link = { 34 | href: 'node://97c9a6e3-4b50-4559-9f60-b5ad68f25758' 35 | }; 36 | 37 | equal(isSuitableFor(link), false); 38 | }); 39 | 40 | it('is not satisfied by asset:// links', () => { 41 | const link = { 42 | href: 'asset://97c9a6e3-4b50-4559-9f60-b5ad68f25758' 43 | }; 44 | 45 | equal(isSuitableFor(link), false); 46 | }); 47 | 48 | it('is not satisfied by mailto: links', () => { 49 | const link = { 50 | href: 'mailto:foo@example.com' 51 | }; 52 | 53 | equal(isSuitableFor(link), false); 54 | }); 55 | 56 | it('is not satisfied by invalid links', () => { 57 | const link = { 58 | href: 'Think of Beethoven\'s 5th: foo foo foo bar' 59 | }; 60 | 61 | equal(isSuitableFor(link), false); 62 | }); 63 | 64 | it('is not satisfied by tel: links', () => { 65 | const link = { 66 | href: 'tel:+491258795857' 67 | }; 68 | 69 | equal(isSuitableFor(link), false); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /Classes/Application/GetNodeTypeFilterOptions/NodeTypeFilterOption.php: -------------------------------------------------------------------------------- 1 | getName(), 35 | icon: $nodeType->getConfiguration('ui.icon') ?: 'question', 36 | label: $nodeType->getConfiguration('ui.label') ?: $nodeType->getName(), 37 | ); 38 | } 39 | 40 | /** 41 | * @param array $preset 42 | */ 43 | public static function fromNodeTreePresetConfiguration(array $preset): self 44 | { 45 | return new self( 46 | value: isset($preset['baseNodeType']) && is_string($preset['baseNodeType']) 47 | ? $preset['baseNodeType'] 48 | : '', 49 | icon: isset($preset['ui']['icon']) && is_string($preset['ui']['icon']) 50 | ? $preset['ui']['icon'] 51 | : 'filter', 52 | label: isset($preset['ui']['label']) && is_string($preset['ui']['label']) 53 | ? $preset['ui']['label'] 54 | : 'N/A', 55 | ); 56 | } 57 | 58 | public function jsonSerialize(): mixed 59 | { 60 | return get_object_vars($this); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/Node/NodeSpecification.test.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from 'node:test'; 2 | import {equal} from 'node:assert/strict'; 3 | 4 | import {isSuitableFor} from './NodeSpecification'; 5 | 6 | describe('LinkType: Node', () => { 7 | it('is not satisfied by http:// links', () => { 8 | const link = { 9 | href: 'http://www.example.com' 10 | }; 11 | 12 | equal(isSuitableFor(link), false); 13 | }); 14 | 15 | it('is not satisfied by https:// links', () => { 16 | const link = { 17 | href: 'https://www.example.com' 18 | }; 19 | 20 | equal(isSuitableFor(link), false); 21 | }); 22 | 23 | it('is satisfied by node:// links', () => { 24 | const link = { 25 | href: 'node://97c9a6e3-4b50-4559-9f60-b5ad68f25758' 26 | }; 27 | 28 | equal(isSuitableFor(link), true); 29 | }); 30 | 31 | it('is satisfied by node:// links with a hash', () => { 32 | const link = { 33 | href: 'node://97c9a6e3-4b50-4559-9f60-b5ad68f25758#section' 34 | }; 35 | 36 | equal(isSuitableFor(link), true); 37 | }); 38 | 39 | it('is not satisfied by asset:// links', () => { 40 | const link = { 41 | href: 'asset://97c9a6e3-4b50-4559-9f60-b5ad68f25758' 42 | }; 43 | 44 | equal(isSuitableFor(link), false); 45 | }); 46 | 47 | it('is not satisfied by mailto: links', () => { 48 | const link = { 49 | href: 'mailto:foo@example.com' 50 | }; 51 | 52 | equal(isSuitableFor(link), false); 53 | }); 54 | 55 | it('is not satisfied by invalid links', () => { 56 | const link = { 57 | href: 'Think of Beethoven\'s 5th: foo foo foo bar' 58 | }; 59 | 60 | equal(isSuitableFor(link), false); 61 | }); 62 | 63 | it('is not satisfied by tel: links', () => { 64 | const link = { 65 | href: 'tel:+491258795857' 66 | }; 67 | 68 | equal(isSuitableFor(link), false); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/Asset/AssetSpecification.test.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from 'node:test'; 2 | import {equal} from 'node:assert/strict'; 3 | 4 | import {isSuitableFor} from './AssetSpecification'; 5 | 6 | describe('LinkType: Asset', () => { 7 | it('is not satisfied by http:// links', () => { 8 | const link = { 9 | href: 'http://www.example.com' 10 | }; 11 | 12 | equal(isSuitableFor(link), false); 13 | }); 14 | 15 | it('is not satisfied by https:// links', () => { 16 | const link = { 17 | href: 'https://www.example.com' 18 | }; 19 | 20 | equal(isSuitableFor(link), false); 21 | }); 22 | 23 | it('is not satisfied by node:// links', () => { 24 | const link = { 25 | href: 'node://97c9a6e3-4b50-4559-9f60-b5ad68f25758' 26 | }; 27 | 28 | equal(isSuitableFor(link), false); 29 | }); 30 | 31 | it('is satisfied by asset:// links', () => { 32 | const link = { 33 | href: 'asset://97c9a6e3-4b50-4559-9f60-b5ad68f25758' 34 | }; 35 | 36 | equal(isSuitableFor(link), true); 37 | }); 38 | 39 | it('is not satisfied by asset:// links with a hash', () => { 40 | const link = { 41 | href: 'asset://97c9a6e3-4b50-4559-9f60-b5ad68f25758#section' 42 | }; 43 | 44 | equal(isSuitableFor(link), false); 45 | }); 46 | 47 | it('is not satisfied by mailto: links', () => { 48 | const link = { 49 | href: 'mailto:foo@example.com' 50 | }; 51 | 52 | equal(isSuitableFor(link), false); 53 | }); 54 | 55 | it('is not satisfied by invalid links', () => { 56 | const link = { 57 | href: 'Think of Beethoven\'s 5th: foo foo foo bar' 58 | }; 59 | 60 | equal(isSuitableFor(link), false); 61 | }); 62 | 63 | it('is not satisfied by tel: links', () => { 64 | const link = { 65 | href: 'tel:+491258795857' 66 | }; 67 | 68 | equal(isSuitableFor(link), false); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/CustomLink/CustomLinkSpecification.test.js: -------------------------------------------------------------------------------- 1 | import {describe, it} from 'node:test'; 2 | import {equal} from 'node:assert/strict'; 3 | 4 | import {isSuitableFor} from './CustomLinkSpecification'; 5 | 6 | describe('LinkType: Node', () => { 7 | it('is not satisfied by http:// links', () => { 8 | const link = { 9 | href: 'http://www.example.com' 10 | }; 11 | 12 | equal(isSuitableFor(link), false); 13 | }); 14 | 15 | it('is not satisfied by https:// links', () => { 16 | const link = { 17 | href: 'https://www.example.com' 18 | }; 19 | 20 | equal(isSuitableFor(link), false); 21 | }); 22 | 23 | it('is not satisfied by node:// links', () => { 24 | const link = { 25 | href: 'node://97c9a6e3-4b50-4559-9f60-b5ad68f25758' 26 | }; 27 | 28 | equal(isSuitableFor(link), false); 29 | }); 30 | 31 | it('is not satisfied by asset:// links', () => { 32 | const link = { 33 | href: 'asset://97c9a6e3-4b50-4559-9f60-b5ad68f25758' 34 | }; 35 | 36 | equal(isSuitableFor(link), false); 37 | }); 38 | 39 | it('is satisfied by asset:// links with a hash', () => { 40 | const link = { 41 | href: 'asset://97c9a6e3-4b50-4559-9f60-b5ad68f25758#section' 42 | }; 43 | 44 | equal(isSuitableFor(link), true); 45 | }); 46 | 47 | it('is not satisfied by mailto: links', () => { 48 | const link = { 49 | href: 'mailto:foo@example.com' 50 | }; 51 | 52 | equal(isSuitableFor(link), false); 53 | }); 54 | 55 | it('is satisfied by invalid links', () => { 56 | const link = { 57 | href: 'Think of Beethoven\'s 5th: foo foo foo bar' 58 | }; 59 | 60 | equal(isSuitableFor(link), true); 61 | }); 62 | 63 | it('is not satisfied by tel: links', () => { 64 | const link = { 65 | href: 'tel:+491258795857' 66 | }; 67 | 68 | equal(isSuitableFor(link), false); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /Classes/Application/GetNodeTypeFilterOptions/NodeTypeFilterOptions.php: -------------------------------------------------------------------------------- 1 | items = array_values($items); 31 | } 32 | 33 | /** 34 | * @param array $nodeTypeNames 35 | */ 36 | public static function fromNodeTypeNames(array $nodeTypeNames, NodeTypeManager $nodeTypeManager): self 37 | { 38 | $items = []; 39 | 40 | foreach ($nodeTypeNames as $nodeTypeName) { 41 | $nodeType = $nodeTypeManager->getNodeType((string) $nodeTypeName); 42 | $items[] = NodeTypeFilterOption::fromNodeType($nodeType); 43 | } 44 | 45 | return new self(...$items); 46 | } 47 | 48 | /** 49 | * @param array> $presets 50 | */ 51 | public static function fromNodeTreePresetsConfiguration(array $presets): self 52 | { 53 | $items = []; 54 | 55 | foreach ($presets as $presetName => $preset) { 56 | if ($presetName === 'default') { 57 | continue; 58 | } 59 | 60 | $items[] = NodeTypeFilterOption::fromNodeTreePresetConfiguration($preset); 61 | } 62 | 63 | return new self(...$items); 64 | } 65 | 66 | public function jsonSerialize(): mixed 67 | { 68 | return $this->items; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/infrastructure/http/getNodeTypeFilterOptions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | import { fetchWithErrorHandling } from "@sitegeist/archaeopteryx-neos-bridge"; 9 | 10 | import { NodeTypeFilterOptionDTO } from "../../domain"; 11 | 12 | type GetNodeTypeFilterOptionsQuery = { 13 | baseNodeTypeFilter: string; 14 | signal?: AbortSignal; 15 | }; 16 | 17 | type GetNodeTypeFilterOptionsQueryResultEnvelope = 18 | | { 19 | success: { 20 | options: NodeTypeFilterOptionDTO[]; 21 | }; 22 | } 23 | | { 24 | error: { 25 | type: string; 26 | code: number; 27 | message: string; 28 | }; 29 | }; 30 | 31 | export async function getNodeTypeFilterOptions( 32 | query: GetNodeTypeFilterOptionsQuery 33 | ): Promise { 34 | const searchParams = new URLSearchParams(); 35 | 36 | searchParams.set("baseNodeTypeFilter", query.baseNodeTypeFilter); 37 | 38 | try { 39 | const response = await fetchWithErrorHandling.withCsrfToken( 40 | (csrfToken) => ({ 41 | url: 42 | "/neos/archaeopteryx/get-node-type-filter-options?" + 43 | searchParams.toString(), 44 | method: "GET", 45 | credentials: "include", 46 | headers: { 47 | "X-Flow-Csrftoken": csrfToken, 48 | "Content-Type": "application/json", 49 | }, 50 | signal: query.signal, 51 | }) 52 | ); 53 | 54 | return fetchWithErrorHandling.parseJson(response); 55 | } catch (error) { 56 | if (error instanceof Error && error.name === 'AbortError') { 57 | throw error; 58 | } 59 | fetchWithErrorHandling.generalErrorHandler(error as any); 60 | throw error; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Neos.Ui/inspector-editor/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {SynchronousRegistry} from '@neos-project/neos-ui-extensibility'; 3 | 4 | import {INeosContextProperties, NeosContext} from '@sitegeist/archaeopteryx-neos-bridge'; 5 | import {EditorContext, IEditor} from '@sitegeist/archaeopteryx-core'; 6 | 7 | import {createInspectorEditor} from './InspectorEditor'; 8 | import {LinkDataType} from "./serialisation"; 9 | 10 | export function registerInspectorEditors( 11 | neosContextProperties: INeosContextProperties, 12 | editor: IEditor 13 | ): void { 14 | const {globalRegistry} = neosContextProperties; 15 | const inspectorRegistry = globalRegistry.get('inspector'); 16 | if (!inspectorRegistry) { 17 | console.warn('[Sitegeist.Archaeopteryx]: Could not find inspector registry.'); 18 | console.warn('[Sitegeist.Archaeopteryx]: Skipping registration of InspectorEditor...'); 19 | return; 20 | } 21 | 22 | const editorsRegistry = inspectorRegistry.get>('editors'); 23 | if (!editorsRegistry) { 24 | console.warn('[Sitegeist.Archaeopteryx]: Could not find inspector editors registry.'); 25 | console.warn('[Sitegeist.Archaeopteryx]: Skipping registration of InspectorEditor...'); 26 | return; 27 | } 28 | 29 | editorsRegistry.set('Sitegeist.Archaeopteryx/Inspector/Editors/ValueObjectLinkEditor', { 30 | component: (props: any) => ( 31 | 32 | 33 | {React.createElement(createInspectorEditor(LinkDataType.valueObject), props)} 34 | 35 | 36 | ) 37 | }); 38 | 39 | editorsRegistry.set('Sitegeist.Archaeopteryx/Inspector/Editors/LinkEditor', { 40 | component: (props: any) => ( 41 | 42 | 43 | {React.createElement(createInspectorEditor(LinkDataType.string), props)} 44 | 45 | 46 | ) 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /Classes/Application/GetNodeSummary/GetNodeSummaryQuery.php: -------------------------------------------------------------------------------- 1 | > $dimensionValues 26 | */ 27 | public function __construct( 28 | public readonly string $workspaceName, 29 | public readonly array $dimensionValues, 30 | public readonly NodeAggregateIdentifier $nodeId, 31 | ) { 32 | } 33 | 34 | /** 35 | * @param array $array 36 | */ 37 | public static function fromArray(array $array): self 38 | { 39 | isset($array['workspaceName']) 40 | or throw new \InvalidArgumentException('Workspace name must be set'); 41 | is_string($array['workspaceName']) 42 | or throw new \InvalidArgumentException('Workspace name must be a string'); 43 | 44 | isset($array['nodeId']) 45 | or throw new \InvalidArgumentException('Node id must be set'); 46 | is_string($array['nodeId']) 47 | or throw new \InvalidArgumentException('Node id must be a string'); 48 | 49 | return new self( 50 | workspaceName: $array['workspaceName'], 51 | dimensionValues: $array['dimensionValues'] ?? [], 52 | nodeId: NodeAggregateIdentifier::fromString($array['nodeId']), 53 | ); 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | public function getTargetDimensionValues(): array 60 | { 61 | $result = []; 62 | 63 | foreach ($this->dimensionValues as $dimensionName => $fallbackChain) { 64 | $result[$dimensionName] = $fallbackChain[0] ?? ''; 65 | } 66 | 67 | return $result; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/domain/Editor/Editor.test.js: -------------------------------------------------------------------------------- 1 | import {Subscription} from 'rxjs'; 2 | import {createEditor} from './Editor'; 3 | import {describe, beforeEach, afterEach, it} from "node:test"; 4 | import {equal, deepEqual} from "node:assert/strict"; 5 | 6 | describe('Editor', () => { 7 | const {state$, tx: {editLink, dismiss, unset, apply}} = createEditor(); 8 | /** @type IEditorState */ 9 | let state; 10 | /** @type Subscription */ 11 | let subscription; 12 | 13 | beforeEach(() => { 14 | subscription = state$.subscribe(latest => { 15 | state = latest; 16 | }); 17 | }); 18 | 19 | afterEach(() => { 20 | subscription.unsubscribe(); 21 | }); 22 | 23 | it('applies links', (_t, done) => { 24 | editLink({href: 'http://example.com/'}).then(result => { 25 | equal(state.isOpen, false); 26 | equal(result.change, true); 27 | deepEqual(result.value, {href: 'https://changed.com/'}); 28 | 29 | setImmediate(done); 30 | }); 31 | setImmediate(() => { 32 | equal(state.isOpen, true); 33 | deepEqual(state.initialValue, {href: 'http://example.com/'}); 34 | 35 | apply({href: 'https://changed.com/'}); 36 | }) 37 | }); 38 | 39 | it('unsets links', (_t, done) => { 40 | editLink({href: 'http://example.com/'}).then(result => { 41 | equal(state.isOpen, false); 42 | equal(result.change, true); 43 | equal(result.value, null); 44 | setImmediate(done); 45 | }); 46 | 47 | setImmediate(() => { 48 | equal(state.isOpen, true); 49 | deepEqual(state.initialValue, {href: 'http://example.com/'}); 50 | 51 | unset(); 52 | }); 53 | }); 54 | 55 | it('can be dismissed', (_t, done) => { 56 | editLink({href: 'http://example.com'}).then(result => { 57 | equal(state.isOpen, false); 58 | equal(result.change, false); 59 | setImmediate(done); 60 | }); 61 | 62 | setImmediate(() => { 63 | equal(state.isOpen, true); 64 | deepEqual(state.initialValue, {href: 'http://example.com'}); 65 | 66 | dismiss(); 67 | }); 68 | }); 69 | }); 70 | 71 | 72 | -------------------------------------------------------------------------------- /Resources/Private/Translations/en/Main.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Link 8 | 9 | 10 | 11 | 12 | Create Link 13 | 14 | 15 | Edit Link 16 | 17 | 18 | Could not find an editor suitable for {href} 19 | 20 | 21 | 22 | 23 | Edit Link 24 | 25 | 26 | Cancel 27 | 28 | 29 | Apply 30 | 31 | 32 | 33 | 34 | Anchor 35 | 36 | 37 | Title 38 | 39 | 40 | Open in new window 41 | 42 | 43 | rel="nofollow" 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/infrastructure/http/getNodeSummary.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | import { fetchWithErrorHandling } from "@sitegeist/archaeopteryx-neos-bridge"; 9 | 10 | type GetNodeSummaryQuery = { 11 | workspaceName: string; 12 | dimensionValues: Record; 13 | nodeId: string; 14 | }; 15 | 16 | type GetNodeSummaryQueryResultEnvelope = 17 | | { 18 | success: { 19 | icon: string; 20 | label: string; 21 | uri: string; 22 | breadcrumbs: { 23 | icon: string; 24 | label: string; 25 | }[]; 26 | }; 27 | } 28 | | { 29 | error: { 30 | type: string; 31 | code: number; 32 | message: string; 33 | }; 34 | }; 35 | 36 | export async function getNodeSummary( 37 | query: GetNodeSummaryQuery 38 | ): Promise { 39 | const searchParams = new URLSearchParams(); 40 | 41 | searchParams.set("workspaceName", query.workspaceName); 42 | for (const [dimensionName, fallbackChain] of Object.entries( 43 | query.dimensionValues 44 | )) { 45 | for (const fallbackValue of fallbackChain) { 46 | searchParams.set( 47 | `dimensionValues[${dimensionName}][]`, 48 | fallbackValue 49 | ); 50 | } 51 | } 52 | searchParams.set("nodeId", query.nodeId); 53 | 54 | try { 55 | const response = await fetchWithErrorHandling.withCsrfToken( 56 | (csrfToken) => ({ 57 | url: 58 | "/neos/archaeopteryx/get-node-summary?" + 59 | searchParams.toString(), 60 | method: "GET", 61 | credentials: "include", 62 | headers: { 63 | "X-Flow-Csrftoken": csrfToken, 64 | "Content-Type": "application/json", 65 | }, 66 | }) 67 | ); 68 | 69 | return fetchWithErrorHandling.parseJson(response); 70 | } catch (error) { 71 | fetchWithErrorHandling.generalErrorHandler(error as any); 72 | throw error; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/Asset/MediaBrowser.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { useEffect, useRef } from 'react' 3 | 4 | import { useGlobalRegistry } from '@sitegeist/archaeopteryx-neos-bridge' 5 | import styled from 'styled-components' 6 | 7 | interface Props { 8 | assetIdentifier: null | string 9 | onSelectAsset: (assetIdentifier: string) => void 10 | } 11 | 12 | const Container = styled.div` 13 | width: calc(-80px - 64px + 100vw); 14 | max-width: 1260px; 15 | 16 | & > div { 17 | padding: 0; 18 | } 19 | 20 | & > iframe { 21 | width: calc(100vw - 160px); 22 | max-width: 100%; 23 | height: calc(100vh - 580px); 24 | position: relative; 25 | } 26 | ` 27 | 28 | export const MediaBrowser: React.FC = (props) => { 29 | const globalRegistry = useGlobalRegistry() 30 | const containerRef = useRef(null) 31 | const selectionRef = useRef(null) 32 | 33 | const secondaryEditorsRegistry = globalRegistry?.get('inspector')?.get('secondaryEditors') as { 34 | get: (identifier: string) => { 35 | component: React.ComponentType 36 | } 37 | } 38 | const { component: MediaSelectionScreenComponent } = secondaryEditorsRegistry?.get( 39 | 'Neos.Neos/Inspector/Secondary/Editors/MediaSelectionScreen' 40 | ) 41 | 42 | // The standard MediaBrowser of Neos uses an iframe and requires some styles to be applied to the iframe content 43 | const iframe = containerRef.current?.querySelector('& > iframe') as HTMLIFrameElement 44 | 45 | useEffect(() => { 46 | const handleIframeLoad = (ev: Event) => { 47 | const iframeDocument = (ev.target as HTMLIFrameElement).contentDocument 48 | if (iframeDocument) { 49 | iframeDocument.body.style.overflowX = 'hidden' 50 | iframeDocument.body.style.padding = '0' 51 | iframeDocument.querySelector('form > .neos-footer')?.remove() 52 | iframeDocument.querySelectorAll('input, select, textarea')?.forEach((input) => { 53 | ;(input as HTMLInputElement).readOnly = true 54 | }) 55 | } 56 | } 57 | 58 | iframe?.addEventListener('load', handleIframeLoad) 59 | 60 | return () => { 61 | iframe?.removeEventListener('load', handleIframeLoad) 62 | } 63 | }, [iframe]) 64 | 65 | return ( 66 | 67 | 73 | 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/CI-Server.yml: -------------------------------------------------------------------------------- 1 | name: CI Server 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ main, '[0-9]+.[0-9]' ] 6 | pull_request: 7 | branches: [ main, '[0-9]+.[0-9]' ] 8 | 9 | jobs: 10 | server-build: 11 | env: 12 | FLOW_CONTEXT: Testing 13 | FLOW_PATH_ROOT: ../neos-base-distribution 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | include: 21 | - php-version: 8.1 22 | neos-version: 8.3 23 | - php-version: 8.3 24 | neos-version: 8.3 25 | - php-version: 8.3 26 | neos-version: 8.4 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | 31 | - name: Setup PHP 32 | uses: shivammathur/setup-php@v2 33 | with: 34 | php-version: ${{ matrix.php-version }} 35 | extensions: mbstring, xml, json, zlib, iconv, intl, pdo_sqlite, mysql 36 | 37 | - id: composer-cache 38 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 39 | shell: bash 40 | 41 | - uses: actions/cache@v3 42 | with: 43 | path: ${{ steps.composer-cache.outputs.dir }} 44 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 45 | restore-keys: ${{ runner.os }}-composer- 46 | 47 | - name: Prepare Neos distribution 48 | run: | 49 | git clone --depth 1 --branch ${{ matrix.neos-version }} https://github.com/neos/neos-base-distribution.git ${FLOW_PATH_ROOT} 50 | cd ${FLOW_PATH_ROOT} 51 | composer config --no-plugins allow-plugins.neos/composer-plugin true 52 | composer config repositories.package '{ "type": "path", "url": "../Sitegeist.Archaeopteryx", "options": { "symlink": false } }' 53 | composer require --no-update --no-interaction sitegeist/archaeopteryx:@dev 54 | # the dev distribution does not ship these depenencies 55 | composer require --dev --no-update --no-interaction phpstan/phpstan:^1.10 56 | composer require --dev --no-update --no-interaction squizlabs/php_codesniffer:^3.6 57 | 58 | - name: Install dependencies 59 | run: | 60 | cd ${FLOW_PATH_ROOT} 61 | composer install --no-interaction --no-progress --prefer-dist 62 | 63 | - name: Linting 64 | run: | 65 | cd ${FLOW_PATH_ROOT}/Packages/Plugins/Sitegeist.Archaeopteryx 66 | composer run lint 67 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/Dialog/LinkEditor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {ErrorBoundary} from '@sitegeist/archaeopteryx-error-handling'; 4 | 5 | import {FieldGroup} from '../../framework'; 6 | import {ILink, ILinkType, useEditorState} from '../../domain'; 7 | 8 | function useLastNonNull(value: null | V) { 9 | const valueRef = React.useRef(value); 10 | 11 | if (value !== null) { 12 | valueRef.current = value; 13 | } 14 | 15 | return valueRef.current; 16 | } 17 | 18 | export const LinkEditor: React.FC<{ 19 | link: null | ILink 20 | linkType: ILinkType 21 | }> = props => ( 22 | 23 | {props.link === null ? ( 24 | 27 | ) : ( 28 | 32 | )} 33 | 34 | ); 35 | 36 | const LinkEditorWithoutValue: React.FC<{ 37 | linkType: ILinkType 38 | }> = props => { 39 | const {editorOptions} = useEditorState(); 40 | const {Editor} = props.linkType; 41 | const prefix = `linkTypeProps.${props.linkType.id.split('.').join('_')}`; 42 | 43 | return ( 44 | 45 | 50 | 51 | ); 52 | } 53 | 54 | const LinkEditorWithValue: React.FC<{ 55 | link: ILink 56 | linkType: ILinkType 57 | }> = props => { 58 | const {editorOptions} = useEditorState(); 59 | const {busy, error, result} = props.linkType.useResolvedModel(props.link); 60 | const model = useLastNonNull(result); 61 | const {Editor, LoadingEditor} = props.linkType; 62 | 63 | if (error) { 64 | throw error; 65 | } else if (busy && !model) { 66 | return ( 67 | 71 | ); 72 | } else { 73 | return ( 74 | 75 | 80 | 81 | ); 82 | } 83 | } -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/Asset/Asset.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {useAssetSummary, useI18n} from '@sitegeist/archaeopteryx-neos-bridge'; 4 | 5 | import {Process, Field} from '../../../framework'; 6 | import {ILink, makeLinkType} from '../../../domain'; 7 | import {ImageCard, IconLabel} from '../../../presentation'; 8 | 9 | import {MediaBrowser} from './MediaBrowser'; 10 | import { Nullable } from 'ts-toolbelt/out/Union/Nullable'; 11 | import {isSuitableFor} from "./AssetSpecification"; 12 | 13 | type AssetLinkModel = { 14 | identifier: string 15 | } 16 | 17 | export const Asset = makeLinkType('Sitegeist.Archaeopteryx:Asset', ({createError}) => ({ 18 | supportedLinkOptions: ['title', 'targetBlank', 'relNofollow'], 19 | 20 | isSuitableFor, 21 | 22 | useResolvedModel: (link: ILink) => { 23 | const match = /asset:\/\/(.*)/.exec(link.href); 24 | 25 | if (match) { 26 | return Process.success({identifier: match[1]}); 27 | } 28 | 29 | return Process.error( 30 | createError(`Cannot handle href "${link.href}".`) 31 | ); 32 | }, 33 | 34 | convertModelToLink: (asset: AssetLinkModel) => ({ 35 | href: `asset://${asset.identifier}` 36 | }), 37 | 38 | TabHeader: () => { 39 | const i18n = useI18n(); 40 | 41 | return ( 42 | 43 | {i18n('Sitegeist.Archaeopteryx:LinkTypes.Asset:title')} 44 | 45 | ); 46 | }, 47 | 48 | Preview: ({model}: {model: AssetLinkModel}) => { 49 | const asset = useAssetSummary(model.identifier); 50 | 51 | if (!asset.value) { 52 | return null; 53 | } 54 | 55 | return ( 56 | 60 | ); 61 | }, 62 | 63 | Editor: ({model}: {model: Nullable}) => { 64 | const i18n = useI18n(); 65 | 66 | return ( 67 | { 71 | if (!value) { 72 | return i18n('Sitegeist.Archaeopteryx:LinkTypes.Asset:identifier.validation.required'); 73 | } 74 | }} 75 | >{({input}) => ( 76 | 80 | )} 81 | ); 82 | } 83 | })); 84 | 85 | -------------------------------------------------------------------------------- /Classes/Application/GetNodeTypeFilterOptions/GetNodeTypeFilterOptionsQueryHandler.php: -------------------------------------------------------------------------------- 1 | > */ 27 | #[Flow\InjectConfiguration(package: 'Neos.Neos', path: 'userInterface.navigateComponent.nodeTree.presets')] 28 | protected array $nodeTreePresets; 29 | 30 | #[Flow\Inject] 31 | protected NodeTypeManager $nodeTypeManager; 32 | 33 | #[Flow\Inject] 34 | protected NodeTypeConstraintFactory $nodeTypeConstraintFactory; 35 | 36 | public function handle(GetNodeTypeFilterOptionsQuery $query): GetNodeTypeFilterOptionsQueryResult 37 | { 38 | return new GetNodeTypeFilterOptionsQueryResult( 39 | options: $this->thereAreNodeTreePresetsOtherThanDefault() 40 | ? $this->createNodeTypeFilterOptionsForNodeTreePresets() 41 | : $this->createNodeTypeFilterOptionsForNodeTypes($query->baseNodeTypeFilter), 42 | ); 43 | } 44 | 45 | private function thereAreNodeTreePresetsOtherThanDefault(): bool 46 | { 47 | $defaultExists = isset($this->nodeTreePresets['default']); 48 | $numberOfPresets = count($this->nodeTreePresets); 49 | 50 | return ($defaultExists && $numberOfPresets > 1) 51 | || (!$defaultExists && $numberOfPresets > 0); 52 | } 53 | 54 | private function createNodeTypeFilterOptionsForNodeTreePresets(): NodeTypeFilterOptions 55 | { 56 | return NodeTypeFilterOptions::fromNodeTreePresetsConfiguration( 57 | $this->nodeTreePresets 58 | ); 59 | } 60 | 61 | private function createNodeTypeFilterOptionsForNodeTypes( 62 | string $baseNodeTypeFilter 63 | ): NodeTypeFilterOptions { 64 | $nodeTypeFilter = NodeTypeFilter::fromFilterString( 65 | $baseNodeTypeFilter, 66 | $this->nodeTypeConstraintFactory, 67 | $this->nodeTypeManager, 68 | ); 69 | 70 | return NodeTypeFilterOptions::fromNodeTypeNames( 71 | $nodeTypeFilter->allowedNodeTypeNames, 72 | $this->nodeTypeManager, 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/infrastructure/http/getChildrenForTreeNode.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | import { fetchWithErrorHandling } from "@sitegeist/archaeopteryx-neos-bridge"; 9 | 10 | import { TreeNodeDTO } from "../../domain"; 11 | 12 | type GetChildrenForTreeNodeQuery = { 13 | workspaceName: string; 14 | dimensionValues: Record; 15 | treeNodeId: string; 16 | nodeTypeFilter: string; 17 | linkableNodeTypes?: string[]; 18 | signal?: AbortSignal; 19 | }; 20 | 21 | type GetChildrenForTreeNodeQueryResultEnvelope = 22 | | { 23 | success: { 24 | children: TreeNodeDTO[]; 25 | }; 26 | } 27 | | { 28 | error: { 29 | type: string; 30 | code: number; 31 | message: string; 32 | }; 33 | }; 34 | 35 | export async function getChildrenForTreeNode( 36 | query: GetChildrenForTreeNodeQuery 37 | ): Promise { 38 | const searchParams = new URLSearchParams(); 39 | 40 | searchParams.set("workspaceName", query.workspaceName); 41 | for (const [dimensionName, fallbackChain] of Object.entries( 42 | query.dimensionValues 43 | )) { 44 | for (const fallbackValue of fallbackChain) { 45 | searchParams.set( 46 | `dimensionValues[${dimensionName}][]`, 47 | fallbackValue 48 | ); 49 | } 50 | } 51 | searchParams.set("treeNodeId", query.treeNodeId); 52 | searchParams.set("nodeTypeFilter", query.nodeTypeFilter); 53 | 54 | for (const linkableNodeType of query.linkableNodeTypes ?? []) { 55 | searchParams.append(`linkableNodeTypes[]`, linkableNodeType); 56 | } 57 | 58 | try { 59 | const response = await fetchWithErrorHandling.withCsrfToken( 60 | (csrfToken) => ({ 61 | url: 62 | "/neos/archaeopteryx/get-children-for-tree-node?" + 63 | searchParams.toString(), 64 | method: "GET", 65 | credentials: "include", 66 | headers: { 67 | "X-Flow-Csrftoken": csrfToken, 68 | "Content-Type": "application/json", 69 | }, 70 | signal: query.signal, 71 | }) 72 | ); 73 | 74 | return fetchWithErrorHandling.parseJson(response); 75 | } catch (error) { 76 | if (error instanceof Error && error.name === 'AbortError') { 77 | throw error; 78 | } 79 | fetchWithErrorHandling.generalErrorHandler(error as any); 80 | throw error; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Resources/Private/Translations/pl/Main.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Link 8 | Link 9 | 10 | 11 | 12 | 13 | Create Link 14 | Utwórz link 15 | 16 | 17 | Edit Link 18 | Edytuj link 19 | 20 | 21 | Could not find an editor suitable for {href} 22 | Nie można znaleźć odpowiedniego edytora dla {href} 23 | 24 | 25 | 26 | 27 | Edit Link 28 | Edytuj link 29 | 30 | 31 | Cancel 32 | Anuluj 33 | 34 | 35 | Apply 36 | Zastosuj 37 | 38 | 39 | 40 | 41 | Anchor 42 | Kotwica 43 | 44 | 45 | Title 46 | Tytuł 47 | 48 | 49 | Open in new window 50 | Otwórz w nowym oknie 51 | 52 | 53 | rel="nofollow" 54 | rel="nofollow" 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Resources/Private/Translations/de/Main.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Link 8 | Link 9 | 10 | 11 | 12 | 13 | Create Link 14 | Link erstellen 15 | 16 | 17 | Edit Link 18 | Link editieren 19 | 20 | 21 | Could not find an editor suitable for {href} 22 | Konnte keinen geeigneten Editor für {href} finden 23 | 24 | 25 | 26 | 27 | Edit Link 28 | Link bearbeiten 29 | 30 | 31 | Cancel 32 | Abbrechen 33 | 34 | 35 | Apply 36 | Anwenden 37 | 38 | 39 | 40 | 41 | Anchor 42 | Anchor 43 | 44 | 45 | Title 46 | Titel 47 | 48 | 49 | Open in new window 50 | In einem neuen Fenster öffnen 51 | 52 | 53 | rel="nofollow" 54 | rel="nofollow" 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Classes/Framework/MVC/QueryResponse.php: -------------------------------------------------------------------------------- 1 | |\JsonSerializable $payload 33 | */ 34 | private function __construct( 35 | private readonly int $statusCode, 36 | private readonly string $discriminator, 37 | private readonly array|\JsonSerializable $payload, 38 | ) { 39 | } 40 | 41 | /** 42 | * @param array|\JsonSerializable $payload 43 | */ 44 | public static function success(array|\JsonSerializable $payload): self 45 | { 46 | return new self( 47 | statusCode: self::STATUS_CODE_SUCCESS, 48 | discriminator: self::DISCRIMINATOR_SUCCESS, 49 | payload: $payload, 50 | ); 51 | } 52 | 53 | public static function clientError(\Exception $exception): self 54 | { 55 | return new self( 56 | statusCode: self::STATUS_CODE_CLIENT_ERROR, 57 | discriminator: self::DISCRIMINATOR_ERROR, 58 | payload: [ 59 | 'type' => $exception::class, 60 | 'code' => $exception->getCode(), 61 | 'message' => $exception->getMessage(), 62 | ], 63 | ); 64 | } 65 | 66 | public static function serverError(\Exception $exception): self 67 | { 68 | return new self( 69 | statusCode: self::STATUS_CODE_SERVER_ERROR, 70 | discriminator: self::DISCRIMINATOR_ERROR, 71 | payload: [ 72 | 'type' => $exception::class, 73 | 'code' => $exception->getCode(), 74 | 'message' => $exception->getMessage(), 75 | ], 76 | ); 77 | } 78 | 79 | public function applyToActionResponse(ActionResponse $actionResponse): void 80 | { 81 | $actionResponse->setContentType('application/json'); 82 | $actionResponse->setStatusCode($this->statusCode); 83 | $actionResponse->setContent( 84 | json_encode( 85 | [$this->discriminator => $this->payload], 86 | JSON_THROW_ON_ERROR 87 | ) 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Resources/Private/Translations/fr/Main.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Link 8 | lien 9 | 10 | 11 | 12 | 13 | Create Link 14 | Créer un lien 15 | 16 | 17 | Edit Link 18 | Modifier le lien 19 | 20 | 21 | Could not find an editor suitable for {href} 22 | Impossible de trouver un éditeur convenant à {href} 23 | 24 | 25 | 26 | 27 | Edit Link 28 | Modifier le lien 29 | 30 | 31 | Cancel 32 | Annuler 33 | 34 | 35 | Apply 36 | Appliquer 37 | 38 | 39 | 40 | 41 | Anchor 42 | Ancrage 43 | 44 | 45 | Title 46 | Titre 47 | 48 | 49 | Open in new window 50 | Ouvrir dans une nouvelle fenêtre 51 | 52 | 53 | rel="nofollow" 54 | rel="nofollow" 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Neos.Ui/core/src/application/LinkTypes/CustomLink/CustomLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {useI18n} from "@sitegeist/archaeopteryx-neos-bridge"; 4 | 5 | import {TextInput} from '@neos-project/react-ui-components'; 6 | 7 | import {ILink, makeLinkType} from "../../../domain"; 8 | import {Process, Field} from '../../../framework'; 9 | import {IconCard, IconLabel} from "../../../presentation"; 10 | import {Nullable} from 'ts-toolbelt/out/Union/Nullable'; 11 | import {isSuitableFor} from "./CustomLinkSpecification"; 12 | 13 | type CustomLinkModel = { 14 | customLink: string, 15 | } 16 | 17 | export const CustomLink = makeLinkType('Sitegeist.Archaeopteryx:CustomLink', () => ({ 18 | isSuitableFor, 19 | 20 | useResolvedModel: (link: ILink) => { 21 | return Process.success({ 22 | customLink: link.href, 23 | }); 24 | }, 25 | 26 | convertModelToLink: (model: CustomLinkModel) => { 27 | return {href: `${model.customLink}`}; 28 | }, 29 | 30 | TabHeader: () => { 31 | const i18n = useI18n(); 32 | 33 | return ( 34 | 35 | {i18n('Sitegeist.Archaeopteryx:LinkTypes.CustomLink:title')} 36 | 37 | ); 38 | }, 39 | 40 | Preview: ({model}: { model: CustomLinkModel }) => { 41 | return ( 42 | ...`} 45 | /> 46 | ) 47 | }, 48 | 49 | Editor: ({model}: { model: Nullable }) => { 50 | const i18n = useI18n(); 51 | 52 | return ( 53 |
54 | 57 |
58 | 59 | name="customLink" 60 | initialValue={model?.customLink} 61 | validate={ 62 | (value) => { 63 | if (!value) { 64 | return i18n('Sitegeist.Archaeopteryx:LinkTypes.CustomLink:validation.required'); 65 | } 66 | } 67 | } 68 | >{({input}) => ( 69 | 75 | )} 76 |
77 |
78 | ); 79 | } 80 | })); 81 | 82 | -------------------------------------------------------------------------------- /Resources/Private/Translations/en/LinkTypes/MailTo.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Mail to 8 | 9 | 10 | 11 | 12 | Recipient 13 | 14 | 15 | e.g.: recipient@example.com 16 | 17 | 18 | Recipient is required 19 | 20 | 21 | Recipient should be a valid E-Mail Address 22 | 23 | 24 | 25 | 26 | Subject (optional) 27 | 28 | 29 | 30 | 31 | CC (optional) 32 | 33 | 34 | e.g.: recipient1@example.com, recipient2@example.com, ... 35 | 36 | 37 | CC should be a comma-separated list of valid E-Mail Addresses 38 | 39 | 40 | 41 | 42 | BCC (optional) 43 | 44 | 45 | e.g.: recipient1@example.com, recipient2@example.com, ... 46 | 47 | 48 | BCC should be a comma-separated list of valid E-Mail Addresses 49 | 50 | 51 | 52 | 53 | Body (optional) 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/application/SelectNodeTypeFilter.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | import * as React from "react"; 9 | import { useAsync } from "react-use"; 10 | import { VError } from "@sitegeist/archaeopteryx-error-handling"; 11 | 12 | import { SelectBox } from "@neos-project/react-ui-components"; 13 | import { useI18n } from "@sitegeist/archaeopteryx-neos-bridge"; 14 | 15 | import { getNodeTypeFilterOptions } from "../infrastructure/http"; 16 | 17 | const searchNodeTypeFilterOptions = ( 18 | searchTerm: string, 19 | options: { 20 | value: string; 21 | label: any; 22 | icon?: string; 23 | }[] 24 | ) => 25 | options.filter( 26 | (option) => 27 | option.label.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1 28 | ); 29 | 30 | interface Props { 31 | baseNodeTypeFilter: string; 32 | value: string; 33 | onChange: (value: string) => void; 34 | } 35 | 36 | export const SelectNodeTypeFilter: React.FC = (props) => { 37 | const i18n = useI18n(); 38 | const [filterTerm, setFilterTerm] = React.useState(""); 39 | const fetch__options = useAsync(async () => { 40 | const result = await getNodeTypeFilterOptions({ 41 | baseNodeTypeFilter: props.baseNodeTypeFilter, 42 | }); 43 | 44 | if ("success" in result) { 45 | return result.success.options.map((option) => ({ 46 | ...option, 47 | label: i18n(option.label) 48 | })); 49 | } 50 | 51 | if ("error" in result) { 52 | throw new VError(result.error.message); 53 | } 54 | 55 | throw new VError("Unable to fetch node type filter options"); 56 | }, [props.baseNodeTypeFilter]); 57 | const options = React.useMemo(() => { 58 | return searchNodeTypeFilterOptions( 59 | filterTerm, 60 | fetch__options.value ?? [] 61 | ); 62 | }, [filterTerm, fetch__options.value]); 63 | 64 | return ( 65 | 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /Classes/Infrastructure/ContentRepository/NodeSearchSpecification.php: -------------------------------------------------------------------------------- 1 | baseNodeTypeFilter->isSatisfiedByNode($node)) { 35 | return false; 36 | } 37 | 38 | if ($this->narrowNodeTypeFilter && !$this->narrowNodeTypeFilter->isSatisfiedByNode($node)) { 39 | return false; 40 | } 41 | 42 | if (!$this->nodeContainsSearchTerm($node)) { 43 | return false; 44 | } 45 | 46 | return true; 47 | } 48 | 49 | private function nodeContainsSearchTerm(Node $node): bool 50 | { 51 | if ($this->searchTerm === null) { 52 | return true; 53 | } 54 | 55 | $term = json_encode(UnicodeFunctions::strtolower($this->searchTerm), JSON_UNESCAPED_UNICODE); 56 | $term = trim($term ? $term : '', '"'); 57 | 58 | /** @var int|false $positionOfTermInLabel */ 59 | $positionOfTermInLabel = UnicodeFunctions::strpos( 60 | UnicodeFunctions::strtolower($node->getLabel()), 61 | $term 62 | ); 63 | 64 | if ($positionOfTermInLabel !== false) { 65 | // Term matches label 66 | return true; 67 | } 68 | 69 | // 70 | // In case the term cannot be found in the node label, we need to 71 | // replicate how the term is matched against the node properties in the 72 | // node data repository. 73 | // 74 | // Yeah, I know :( 75 | // 76 | $nodeData = $node->getNodeData(); 77 | $reflectionNodeData = new \ReflectionObject($nodeData); 78 | $reflectionProperties = $reflectionNodeData->getProperty('properties'); 79 | $reflectionProperties->setAccessible(true); 80 | $properties = $reflectionProperties->getValue($nodeData); 81 | $properties = json_encode($properties, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE); 82 | 83 | return UnicodeFunctions::strpos($properties, $term) !== false; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Classes/Infrastructure/ContentRepository/NodeSearchService.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | public function search( 41 | string $searchTerm, 42 | NodeTypeFilter $nodeTypeFilter, 43 | Node $rootNode, 44 | ): \Traversable { 45 | /** @var NeosNodeSearchService $nodeSearchService */ 46 | $nodeSearchService = $this->nodeSearchService; 47 | $searchTerm = trim($searchTerm); 48 | $context = $rootNode->getContext(); 49 | 50 | if ($searchTerm) { 51 | /** @var Node[] $result */ 52 | $result = $nodeSearchService->findByProperties( 53 | $searchTerm, 54 | $nodeTypeFilter->allowedNodeTypeNames, 55 | $context, 56 | $rootNode 57 | ); 58 | 59 | yield from $result; 60 | } else { 61 | // 62 | // This algorithm has been copied from the UI core, which uses the 63 | // NodeDataRepository as well (which it visibly regrets doing). 64 | // 65 | // Everything is going to be better in Neos 9.0. 66 | // 67 | $nodeDataRecords = $this->nodeDataRepository 68 | ->findByParentAndNodeTypeRecursively( 69 | $rootNode->getPath(), 70 | implode(',', $nodeTypeFilter->allowedNodeTypeNames), 71 | $context->getWorkspace(), 72 | $context->getDimensions() 73 | ); 74 | foreach ($nodeDataRecords as $nodeData) { 75 | $matchedNode = $this->nodeFactory->createFromNodeData($nodeData, $context); 76 | if ($matchedNode !== null) { 77 | /** @var Node $matchedNode */ 78 | yield $matchedNode; 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Neos.Ui/neos-bridge/src/domain/ContentRepository/ContextPath.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Any} from 'ts-toolbelt'; 3 | 4 | import {useSelector} from '../Extensibility/Store'; 5 | 6 | export type Path = Any.Type; 7 | export type Context = Any.Type; 8 | 9 | export class ContextPath { 10 | private constructor( 11 | public readonly path: Path, 12 | public readonly context: Context 13 | ) { } 14 | 15 | public static fromString(string: string): null | ContextPath { 16 | const [path, context] = (string ?? '').split('@'); 17 | 18 | if (path && string) { 19 | return new ContextPath(path as Path, context as Context); 20 | } 21 | 22 | return null; 23 | } 24 | 25 | public adopt(pathLike: undefined | null | string): null | ContextPath { 26 | const [path] = (pathLike ?? '').split('@'); 27 | 28 | if (path) { 29 | return new ContextPath(path as Path, this.context); 30 | } 31 | 32 | return null; 33 | } 34 | 35 | public getIntermediateContextPaths(other: ContextPath): ContextPath[] { 36 | if (other.path.startsWith(this.path)) { 37 | const segments = other.path.split('/'); 38 | const result: ContextPath[] = []; 39 | 40 | for (const [index] of segments.entries()) { 41 | const path = segments.slice(0, -index).join('/'); 42 | if (path) { 43 | result.push(new ContextPath(path as Path, this.context)); 44 | } 45 | 46 | if (path === this.path) { 47 | break; 48 | } 49 | } 50 | 51 | return result; 52 | } 53 | 54 | return []; 55 | } 56 | 57 | public equals(other: ContextPath): boolean { 58 | return this.path === other.path && this.context === other.context; 59 | } 60 | 61 | public toString(): string { 62 | return `${this.path}@${this.context}`; 63 | } 64 | 65 | get depth(): number { 66 | return this.path.match(/\//g)?.length ?? 0; 67 | } 68 | } 69 | 70 | export function useSiteNodeContextPath(): null | ContextPath { 71 | const siteNodeContextPath = useSelector(state => state.cr?.nodes?.siteNode); 72 | const result = React.useMemo(() => { 73 | if (siteNodeContextPath) { 74 | return ContextPath.fromString(siteNodeContextPath); 75 | } 76 | 77 | return null; 78 | }, [siteNodeContextPath]); 79 | 80 | return result; 81 | } 82 | 83 | export function useDocumentNodeContextPath(): null | ContextPath { 84 | const documentNodeContextPath = useSelector(state => state.cr?.nodes?.documentNode); 85 | const result = React.useMemo(() => { 86 | if (documentNodeContextPath) { 87 | return ContextPath.fromString(documentNodeContextPath); 88 | } 89 | 90 | return null; 91 | }, [documentNodeContextPath]); 92 | 93 | return result; 94 | } -------------------------------------------------------------------------------- /Classes/Application/GetNodeSummary/GetNodeSummaryQueryHandler.php: -------------------------------------------------------------------------------- 1 | contentContextFactory->create([ 37 | 'workspaceName' => $query->workspaceName, 38 | 'dimensions' => $query->dimensionValues, 39 | 'targetDimensions' => $query->getTargetDimensionValues(), 40 | 'invisibleContentShown' => true, 41 | 'removedContentShown' => false, 42 | 'inaccessibleContentShown' => true 43 | ]); 44 | 45 | $node = $contentContext->getNodeByIdentifier((string) $query->nodeId); 46 | if (!$node instanceof Node) { 47 | throw NodeWasNotFound::becauseNodeWithGivenIdentifierDoesNotExistInContext( 48 | nodeAggregateIdentifier: $query->nodeId, 49 | contentContext: $contentContext, 50 | ); 51 | } 52 | 53 | return new GetNodeSummaryQueryResult( 54 | icon: $node->getNodeType()->getConfiguration('ui.icon'), 55 | label: $node->getLabel(), 56 | uri: new Uri('node://' . $node->getNodeAggregateIdentifier()), 57 | breadcrumbs: $this->createBreadcrumbsForNode($node) 58 | ); 59 | } 60 | 61 | private function createBreadcrumbsForNode(Node $node): Breadcrumbs 62 | { 63 | $items = []; 64 | 65 | while ($node) { 66 | /** @var Node $node */ 67 | $items[] = $this->createBreadcrumbForNode($node); 68 | $node = $node->getParent(); 69 | } 70 | 71 | $items = array_slice($items, 0, -2); 72 | $items = array_reverse($items); 73 | 74 | return new Breadcrumbs(...$items); 75 | } 76 | 77 | private function createBreadcrumbForNode(Node $node): Breadcrumb 78 | { 79 | return new Breadcrumb( 80 | icon: $node->getNodeType()->getConfiguration('ui.icon') ?? 'questionmark', 81 | label: $node->getLabel(), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Classes/Application/GetChildrenForTreeNode/GetChildrenForTreeNodeQuery.php: -------------------------------------------------------------------------------- 1 | > $dimensionValues 28 | */ 29 | public function __construct( 30 | public readonly string $workspaceName, 31 | public readonly array $dimensionValues, 32 | public readonly NodeAggregateIdentifier $treeNodeId, 33 | public readonly string $nodeTypeFilter, 34 | public readonly NodeTypeNames $linkableNodeTypes, 35 | ) { 36 | } 37 | 38 | /** 39 | * @param array $array 40 | */ 41 | public static function fromArray(array $array): self 42 | { 43 | isset($array['workspaceName']) 44 | or throw new \InvalidArgumentException('Workspace name must be set'); 45 | is_string($array['workspaceName']) 46 | or throw new \InvalidArgumentException('Workspace name must be a string'); 47 | 48 | isset($array['treeNodeId']) 49 | or throw new \InvalidArgumentException('Tree node id must be set'); 50 | is_string($array['treeNodeId']) 51 | or throw new \InvalidArgumentException('Tree node id must be a string'); 52 | 53 | !isset($array['nodeTypeFilter']) or is_string($array['nodeTypeFilter']) 54 | or throw new \InvalidArgumentException('Node type filter must be a string'); 55 | 56 | !isset($array['linkableNodeTypes']) or is_array($array['linkableNodeTypes']) 57 | or throw new \InvalidArgumentException('Linkable node types must be an array'); 58 | 59 | return new self( 60 | workspaceName: $array['workspaceName'], 61 | dimensionValues: $array['dimensionValues'] ?? [], 62 | treeNodeId: NodeAggregateIdentifier::fromString($array['treeNodeId']), 63 | nodeTypeFilter: $array['nodeTypeFilter'] ?? '', 64 | linkableNodeTypes: NodeTypeNames::fromArray($array['linkableNodeTypes'] ?? []), 65 | ); 66 | } 67 | 68 | /** 69 | * @return array 70 | */ 71 | public function getTargetDimensionValues(): array 72 | { 73 | $result = []; 74 | 75 | foreach ($this->dimensionValues as $dimensionName => $fallbackChain) { 76 | $result[$dimensionName] = $fallbackChain[0] ?? ''; 77 | } 78 | 79 | return $result; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Classes/Application/GetChildrenForTreeNode/GetChildrenForTreeNodeQueryHandler.php: -------------------------------------------------------------------------------- 1 | contentContextFactory->create([ 39 | 'workspaceName' => $query->workspaceName, 40 | 'dimensions' => $query->dimensionValues, 41 | 'targetDimensions' => $query->getTargetDimensionValues(), 42 | 'invisibleContentShown' => true, 43 | 'removedContentShown' => false, 44 | 'inaccessibleContentShown' => true 45 | ]); 46 | 47 | $node = $contentContext->getNodeByIdentifier((string) $query->treeNodeId); 48 | if (!$node instanceof Node) { 49 | throw NodeWasNotFound::becauseNodeWithGivenIdentifierDoesNotExistInContext( 50 | nodeAggregateIdentifier: $query->treeNodeId, 51 | contentContext: $contentContext, 52 | ); 53 | } 54 | 55 | return new GetChildrenForTreeNodeQueryResult( 56 | children: $this->createTreeNodesFromChildrenOfNode($node, $query), 57 | ); 58 | } 59 | 60 | private function createTreeNodesFromChildrenOfNode(Node $node, GetChildrenForTreeNodeQuery $query): TreeNodes 61 | { 62 | $linkableNodeTypesFilter = NodeTypeFilter::fromNodeTypeNames( 63 | nodeTypeNames: $query->linkableNodeTypes, 64 | nodeTypeManager: $this->nodeTypeManager 65 | ); 66 | 67 | $items = []; 68 | 69 | foreach ($node->getChildNodes($query->nodeTypeFilter) as $childNode) { 70 | /** @var Node $childNode */ 71 | $items[] = TreeNodeBuilder::forNode($childNode) 72 | ->setIsMatchedByFilter(true) 73 | ->setIsLinkable($linkableNodeTypesFilter->isSatisfiedByNode($childNode)) 74 | ->setHasUnloadedChildren($childNode->getNumberOfChildNodes($query->nodeTypeFilter) > 0) 75 | ->build(); 76 | } 77 | 78 | return new TreeNodes(...$items); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Neos.Ui/custom-node-tree/src/infrastructure/http/getTree.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This script belongs to the package "Sitegeist.Archaeopteryx". 3 | * 4 | * This package is Open Source Software. For the full copyright and license 5 | * information, please view the LICENSE file which was distributed with this 6 | * source code. 7 | */ 8 | import { fetchWithErrorHandling } from "@sitegeist/archaeopteryx-neos-bridge"; 9 | 10 | import { TreeNodeDTO } from "../../domain"; 11 | 12 | type GetTreeQuery = { 13 | workspaceName: string; 14 | dimensionValues: Record; 15 | startingPoint: string; 16 | loadingDepth: number; 17 | baseNodeTypeFilter: string; 18 | linkableNodeTypes?: string[]; 19 | narrowNodeTypeFilter: string; 20 | searchTerm: string; 21 | selectedNodeId?: string; 22 | signal?: AbortSignal; 23 | }; 24 | 25 | type GetTreeQueryResultEnvelope = 26 | | { 27 | success: { 28 | root: TreeNodeDTO; 29 | }; 30 | } 31 | | { 32 | error: { 33 | type: string; 34 | code: number; 35 | message: string; 36 | }; 37 | }; 38 | 39 | export async function getTree( 40 | query: GetTreeQuery 41 | ): Promise { 42 | const searchParams = new URLSearchParams(); 43 | 44 | searchParams.set("workspaceName", query.workspaceName); 45 | for (const [dimensionName, fallbackChain] of Object.entries( 46 | query.dimensionValues 47 | )) { 48 | for (const fallbackValue of fallbackChain) { 49 | searchParams.set( 50 | `dimensionValues[${dimensionName}][]`, 51 | fallbackValue 52 | ); 53 | } 54 | } 55 | searchParams.set("startingPoint", query.startingPoint); 56 | searchParams.set("loadingDepth", String(query.loadingDepth)); 57 | searchParams.set("baseNodeTypeFilter", query.baseNodeTypeFilter); 58 | 59 | for (const linkableNodeType of query.linkableNodeTypes ?? []) { 60 | searchParams.append(`linkableNodeTypes[]`, linkableNodeType); 61 | } 62 | 63 | searchParams.set("narrowNodeTypeFilter", query.narrowNodeTypeFilter); 64 | searchParams.set("searchTerm", query.searchTerm); 65 | 66 | if (query.selectedNodeId) { 67 | searchParams.set("selectedNodeId", query.selectedNodeId); 68 | } 69 | 70 | try { 71 | const response = await fetchWithErrorHandling.withCsrfToken( 72 | (csrfToken) => ({ 73 | url: 74 | "/neos/archaeopteryx/get-tree?" + 75 | searchParams.toString(), 76 | method: "GET", 77 | credentials: "include", 78 | headers: { 79 | "X-Flow-Csrftoken": csrfToken, 80 | "Content-Type": "application/json", 81 | }, 82 | signal: query.signal, 83 | }) 84 | ); 85 | 86 | return fetchWithErrorHandling.parseJson(response); 87 | } catch (error) { 88 | // Don't handle AbortError as a general error 89 | if (error instanceof Error && error.name === 'AbortError') { 90 | throw error; 91 | } 92 | fetchWithErrorHandling.generalErrorHandler(error as any); 93 | throw error; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Classes/Infrastructure/ContentRepository/NodeTypeFilter.php: -------------------------------------------------------------------------------- 1 | parseFilterString($filterString); 44 | $allowedNodeTypeNames = []; 45 | 46 | foreach ($nodeTypeManager->getNodeTypes(false) as $nodeType) { 47 | $nodeTypeName = $nodeType->getName(); 48 | if ($nodeTypeConstraints->matches(NodeTypeName::fromString($nodeTypeName))) { 49 | $allowedNodeTypeNames[] = $nodeTypeName; 50 | } 51 | } 52 | 53 | return new self($filterString, $allowedNodeTypeNames); 54 | } 55 | 56 | public static function fromNodeTypeNames( 57 | NodeTypeNames $nodeTypeNames, 58 | NodeTypeManager $nodeTypeManager, 59 | ): self { 60 | $allowedNodeTypeNames = []; 61 | 62 | foreach ($nodeTypeManager->getNodeTypes(false) as $nodeType) { 63 | $nodeTypeName = $nodeType->getName(); 64 | 65 | if ($nodeTypeNames->includesSuperTypeOf($nodeType)) { 66 | $allowedNodeTypeNames[] = $nodeTypeName; 67 | } 68 | } 69 | 70 | return new self(null, $allowedNodeTypeNames); 71 | } 72 | 73 | public function toFilterString(): string 74 | { 75 | return $this->filterString ?? implode(',', $this->allowedNodeTypeNames); 76 | } 77 | 78 | public function isSatisfiedByNode(Node $node): bool 79 | { 80 | return $this->isSatisfiedByNodeTypeName($node->getNodeTypeName()); 81 | } 82 | 83 | public function isSatisfiedByNodeType(NodeType $nodeType): bool 84 | { 85 | return $this->isSatisfiedByNodeTypeNameString($nodeType->getName()); 86 | } 87 | 88 | public function isSatisfiedByNodeTypeName(NodeTypeName $nodeTypeName): bool 89 | { 90 | return $this->isSatisfiedByNodeTypeNameString((string) $nodeTypeName); 91 | } 92 | 93 | private function isSatisfiedByNodeTypeNameString(string $nodeTypeNameString): bool 94 | { 95 | return in_array($nodeTypeNameString, $this->allowedNodeTypeNames); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Neos.Ui/link-button/src/LinkButtonPreview.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import styled from "styled-components"; 4 | import { parsePhoneNumber} from 'libphonenumber-js/max' 5 | 6 | import {IconButton} from '@neos-project/react-ui-components'; 7 | import { ErrorBoundary } from '@sitegeist/archaeopteryx-error-handling'; 8 | import { useLinkTypeForHref } from '@sitegeist/archaeopteryx-core/src/domain'; 9 | 10 | const PreviewContainer = styled.div` 11 | position: absolute; 12 | top: 105%; 13 | left: 0; 14 | z-index: 10; 15 | width: 420px; 16 | display: grid; 17 | grid-template-columns: 1fr 40px; 18 | justify-content: stretch; 19 | border: 1px solid #3f3f3f; 20 | background: #323232; 21 | `; 22 | 23 | const StyledIconButton = styled(IconButton)` 24 | height: 100%; 25 | `; 26 | 27 | const getModelByLinkType = (linkType: string, cursorLink: string) => { 28 | if(linkType === 'Sitegeist.Archaeopteryx:Node') { 29 | return {nodeId: cursorLink.startsWith('node://') 30 | ? cursorLink.slice('node://'.length) 31 | : undefined}; 32 | } 33 | 34 | if(linkType === 'Sitegeist.Archaeopteryx:PhoneNumber') { 35 | const {nationalNumber, countryCallingCode} = parsePhoneNumber(cursorLink.replace('tel:', '')); 36 | 37 | if(!nationalNumber || !countryCallingCode) return 38 | return {phoneNumber: nationalNumber, countryCallingCode: `+${countryCallingCode}`} 39 | } 40 | 41 | if(linkType === 'Sitegeist.Archaeopteryx:Asset') { 42 | return {identifier: cursorLink.startsWith('asset://') 43 | ? cursorLink.slice('asset://'.length) 44 | : undefined}; 45 | } 46 | 47 | if(linkType === 'Sitegeist.Archaeopteryx:Web') { 48 | const match = cursorLink.match(/^(https?):\/\/(.+)$/i); 49 | if (match) { 50 | const [, protocol, urlWithoutProtocol] = match; 51 | return {protocol, urlWithoutProtocol}; 52 | } 53 | } 54 | 55 | if(linkType === 'Sitegeist.Archaeopteryx:MailTo') { 56 | return {recipient: cursorLink.startsWith('mailto:') 57 | ? cursorLink.slice('mailto:'.length).replace('?', '') 58 | : undefined}; 59 | } 60 | 61 | if(linkType === 'Sitegeist.Archaeopteryx:CustomLink') { 62 | return {customLink: cursorLink}; 63 | } 64 | 65 | return 66 | } 67 | 68 | type LinkButtonPreviewProps = { 69 | cursorLink?: string; 70 | onClick: () => void; 71 | } 72 | 73 | export const LinkButtonPreview = ({ cursorLink, onClick }: LinkButtonPreviewProps) => { 74 | 75 | const [show, setShow] = React.useState(true) 76 | 77 | const linkType = useLinkTypeForHref(cursorLink ?? ''); 78 | 79 | const handleClick = () => { 80 | setShow(false) 81 | onClick() 82 | } 83 | 84 | React.useEffect(() => { 85 | setShow(true) 86 | }, [cursorLink]) 87 | 88 | if(!cursorLink || !linkType || !show) return null; 89 | const model = getModelByLinkType(linkType.id, cursorLink); 90 | 91 | 92 | return ( 93 | 94 | 95 | 100 | 101 | 102 | 103 | ); 104 | }; 105 | --------------------------------------------------------------------------------