From a9b33d141c175e7f5bc8936ccb1892986495629d Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Thu, 30 May 2024 23:44:42 +0200 Subject: [PATCH 01/11] TASK: Convert FlashMessage component to typescript --- .../FlashMessage/FlashMessage.tsx | 80 +++++++++++++++++++ .../FlashMessages/FlashMessage/index.js | 64 --------------- .../FlashMessages/FlashMessage/index.ts | 10 +++ 3 files changed, 90 insertions(+), 64 deletions(-) create mode 100644 packages/neos-ui/src/Containers/FlashMessages/FlashMessage/FlashMessage.tsx delete mode 100644 packages/neos-ui/src/Containers/FlashMessages/FlashMessage/index.js create mode 100644 packages/neos-ui/src/Containers/FlashMessages/FlashMessage/index.ts diff --git a/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/FlashMessage.tsx b/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/FlashMessage.tsx new file mode 100644 index 0000000000..1bdb2e0a10 --- /dev/null +++ b/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/FlashMessage.tsx @@ -0,0 +1,80 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import React from 'react'; +import mergeClassNames from 'classnames'; + +import {IconButton, Icon} from '@neos-project/react-ui-components'; + +import style from './style.module.css'; + +export const FlashMessage: React.FC<{ + id: string; + message: string; + severity: 'success' | 'error' | 'info'; + timeout?: number; + + onClose: (id: string) => void; +}> = (props) => { + const {message, severity} = props; + const isSuccess = severity === 'success'; + const isError = severity === 'error'; + const isInfo = severity === 'info'; + const isClosing = React.useRef(false); + const handleClose = React.useCallback(() => { + const {onClose, id} = props; + + if (!isClosing.current) { + isClosing.current = true; + setTimeout(() => onClose(id), 100); + } + }, [props.id]); + + const flashMessageClasses = mergeClassNames({ + [style.flashMessage]: true, + [style['flashMessage--success']]: isSuccess, + [style['flashMessage--error']]: isError, + [style['flashMessage--info']]: isInfo + }); + + const iconName = mergeClassNames({ + check: isSuccess, + ban: isError, + info: isInfo + }) || 'info'; + + React.useEffect( + () => { + if (props.timeout) { + const timeout = setTimeout(handleClose, props.timeout); + return () => { + clearTimeout(timeout); + }; + } + + return () => {}; + }, + [handleClose, props.timeout] + ); + + return ( +
+ +
{message}
+ +
+ ); +} diff --git a/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/index.js b/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/index.js deleted file mode 100644 index 278acae8f4..0000000000 --- a/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/index.js +++ /dev/null @@ -1,64 +0,0 @@ -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import mergeClassNames from 'classnames'; -import {IconButton, Icon} from '@neos-project/react-ui-components'; - -import style from './style.module.css'; - -export default class FlashMessage extends PureComponent { - static propTypes = { - id: PropTypes.string.isRequired, - message: PropTypes.string.isRequired, - severity: PropTypes.string.isRequired, - timeout: PropTypes.number, - onClose: PropTypes.func.isRequired - }; - - handleClose = () => { - const {onClose, id} = this.props; - - setTimeout(() => onClose(id), 100); - } - - componentDidMount() { - const {timeout} = this.props; - - if (timeout) { - setTimeout(this.handleClose, timeout); - } - } - - render() { - const {message, severity} = this.props; - const isSuccess = severity === 'success'; - const isError = severity === 'error'; - const isInfo = severity === 'info'; - - const flashMessageClasses = mergeClassNames({ - [style.flashMessage]: true, - [style['flashMessage--success']]: isSuccess, - [style['flashMessage--error']]: isError, - [style['flashMessage--info']]: isInfo - }); - - const iconName = mergeClassNames({ - check: isSuccess, - ban: isError, - info: isInfo - }) || 'info'; - - return ( -
- -
{message}
- -
- ); - } -} diff --git a/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/index.ts b/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/index.ts new file mode 100644 index 0000000000..24619772a9 --- /dev/null +++ b/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/index.ts @@ -0,0 +1,10 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {FlashMessage as default} from './FlashMessage'; From 10068f1b82181d42aa18e850b850b95823c4c498 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Fri, 31 May 2024 11:00:04 +0200 Subject: [PATCH 02/11] TASK: Convert FlashMessages component to typescript --- .../FlashMessages/FlashMessages.tsx | 59 +++++++++++++++++++ .../src/Containers/FlashMessages/index.js | 48 --------------- .../src/Containers/FlashMessages/index.ts | 10 ++++ 3 files changed, 69 insertions(+), 48 deletions(-) create mode 100644 packages/neos-ui/src/Containers/FlashMessages/FlashMessages.tsx delete mode 100644 packages/neos-ui/src/Containers/FlashMessages/index.js create mode 100644 packages/neos-ui/src/Containers/FlashMessages/index.ts diff --git a/packages/neos-ui/src/Containers/FlashMessages/FlashMessages.tsx b/packages/neos-ui/src/Containers/FlashMessages/FlashMessages.tsx new file mode 100644 index 0000000000..120f322d02 --- /dev/null +++ b/packages/neos-ui/src/Containers/FlashMessages/FlashMessages.tsx @@ -0,0 +1,59 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import React from 'react'; +// @ts-ignore +import {connect} from 'react-redux'; + +import {actions} from '@neos-project/neos-ui-redux-store'; +import {GlobalState} from '@neos-project/neos-ui-redux-store/src/System'; + +import FlashMessage from './FlashMessage'; + +import style from './style.module.css'; + +const withReduxState = connect((state: GlobalState) => ({ + flashMessages: state?.ui?.flashMessages +}), { + removeMessage: actions.UI.FlashMessages.remove +}); + +const StatelessFlashMessages: React.FC<{ + flashMessages: Record; + removeMessage: (id: string) => void; +}> = (props) => { + const {flashMessages, removeMessage} = props; + + return ( +
+ {Object.keys(flashMessages).map(flashMessageId => { + const flashMessage = flashMessages[flashMessageId]; + const {id, message, severity, timeout} = flashMessage; + + return ( + + ); + })} +
+ ); +} + +export const FlashMessages = withReduxState(StatelessFlashMessages); diff --git a/packages/neos-ui/src/Containers/FlashMessages/index.js b/packages/neos-ui/src/Containers/FlashMessages/index.js deleted file mode 100644 index 077fd4657d..0000000000 --- a/packages/neos-ui/src/Containers/FlashMessages/index.js +++ /dev/null @@ -1,48 +0,0 @@ -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import {connect} from 'react-redux'; - -import {actions} from '@neos-project/neos-ui-redux-store'; -import FlashMessage from './FlashMessage/index'; - -import style from './style.module.css'; - -@connect(state => ({ - flashMessages: state?.ui?.flashMessages -}), { - removeMessage: actions.UI.FlashMessages.remove -}) -export default class FlashMessages extends PureComponent { - static propTypes = { - flashMessages: PropTypes.object, - removeMessage: PropTypes.func.isRequired - }; - - static defaultProps = { - flashMessages: {} - }; - - render() { - const {flashMessages, removeMessage} = this.props; - - return ( -
- {Object.keys(flashMessages).map(flashMessageId => { - const flashMessage = flashMessages[flashMessageId]; - const {id, message, severity, timeout} = flashMessage; - - return ( - - ); - })} -
- ); - } -} diff --git a/packages/neos-ui/src/Containers/FlashMessages/index.ts b/packages/neos-ui/src/Containers/FlashMessages/index.ts new file mode 100644 index 0000000000..d87c65f2db --- /dev/null +++ b/packages/neos-ui/src/Containers/FlashMessages/index.ts @@ -0,0 +1,10 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {FlashMessages as default} from './FlashMessages'; From 28f91a3b804e0e6064313218fec8df2aad764fd7 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Fri, 31 May 2024 11:26:30 +0200 Subject: [PATCH 03/11] TASK: Move ErrorView component into ./container/ directory --- .../src/{ => container/ErrorView}/ErrorView.tsx | 2 +- .../neos-ui-error/src/container/ErrorView/index.ts | 10 ++++++++++ .../src/{ => container/ErrorView}/style.module.css | 0 packages/neos-ui-error/src/container/index.ts | 10 ++++++++++ packages/neos-ui-error/src/index.ts | 2 +- 5 files changed, 22 insertions(+), 2 deletions(-) rename packages/neos-ui-error/src/{ => container/ErrorView}/ErrorView.tsx (99%) create mode 100644 packages/neos-ui-error/src/container/ErrorView/index.ts rename packages/neos-ui-error/src/{ => container/ErrorView}/style.module.css (100%) create mode 100644 packages/neos-ui-error/src/container/index.ts diff --git a/packages/neos-ui-error/src/ErrorView.tsx b/packages/neos-ui-error/src/container/ErrorView/ErrorView.tsx similarity index 99% rename from packages/neos-ui-error/src/ErrorView.tsx rename to packages/neos-ui-error/src/container/ErrorView/ErrorView.tsx index d6549dee8e..99243c3507 100644 --- a/packages/neos-ui-error/src/ErrorView.tsx +++ b/packages/neos-ui-error/src/container/ErrorView/ErrorView.tsx @@ -11,7 +11,7 @@ import React from 'react'; import I18n from '@neos-project/neos-ui-i18n'; -import {AnyError, isECMAScriptError, isServerSideError, isStringError} from './types'; +import {AnyError, isECMAScriptError, isServerSideError, isStringError} from '../../types'; import style from './style.module.css'; diff --git a/packages/neos-ui-error/src/container/ErrorView/index.ts b/packages/neos-ui-error/src/container/ErrorView/index.ts new file mode 100644 index 0000000000..e343e72ed9 --- /dev/null +++ b/packages/neos-ui-error/src/container/ErrorView/index.ts @@ -0,0 +1,10 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {ErrorView} from './ErrorView'; diff --git a/packages/neos-ui-error/src/style.module.css b/packages/neos-ui-error/src/container/ErrorView/style.module.css similarity index 100% rename from packages/neos-ui-error/src/style.module.css rename to packages/neos-ui-error/src/container/ErrorView/style.module.css diff --git a/packages/neos-ui-error/src/container/index.ts b/packages/neos-ui-error/src/container/index.ts new file mode 100644 index 0000000000..e343e72ed9 --- /dev/null +++ b/packages/neos-ui-error/src/container/index.ts @@ -0,0 +1,10 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {ErrorView} from './ErrorView'; diff --git a/packages/neos-ui-error/src/index.ts b/packages/neos-ui-error/src/index.ts index cbe1cc7ea1..a5950f1e2f 100644 --- a/packages/neos-ui-error/src/index.ts +++ b/packages/neos-ui-error/src/index.ts @@ -14,4 +14,4 @@ export type { AnyError } from './types'; -export {ErrorView} from './ErrorView'; +export {ErrorView} from './container'; From 57af79b60b7149c6502f5470e5415f5ba035ea20 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Fri, 31 May 2024 11:41:50 +0200 Subject: [PATCH 04/11] TASK: Move ErrorBoundary component to neos-ui-error package --- .../container/ErrorBoundary/ErrorBoundary.tsx} | 15 ++++++++++++--- .../src/container/ErrorBoundary/index.ts | 10 ++++++++++ .../src/container}/ErrorBoundary/style.module.css | 0 packages/neos-ui-error/src/container/index.ts | 1 + packages/neos-ui-error/src/index.ts | 2 +- packages/neos-ui/src/Containers/Root.js | 2 +- .../terminateDueToFatalInitializationError.js | 2 +- 7 files changed, 26 insertions(+), 6 deletions(-) rename packages/{neos-ui/src/Containers/ErrorBoundary/index.tsx => neos-ui-error/src/container/ErrorBoundary/ErrorBoundary.tsx} (91%) create mode 100644 packages/neos-ui-error/src/container/ErrorBoundary/index.ts rename packages/{neos-ui/src/Containers => neos-ui-error/src/container}/ErrorBoundary/style.module.css (100%) diff --git a/packages/neos-ui/src/Containers/ErrorBoundary/index.tsx b/packages/neos-ui-error/src/container/ErrorBoundary/ErrorBoundary.tsx similarity index 91% rename from packages/neos-ui/src/Containers/ErrorBoundary/index.tsx rename to packages/neos-ui-error/src/container/ErrorBoundary/ErrorBoundary.tsx index 3eda9448ba..c659169820 100644 --- a/packages/neos-ui/src/Containers/ErrorBoundary/index.tsx +++ b/packages/neos-ui-error/src/container/ErrorBoundary/ErrorBoundary.tsx @@ -1,5 +1,13 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ import React from 'react'; -import styles from './style.module.css'; // @ts-ignore import Logo from '@neos-project/react-ui-components/src/Logo'; @@ -7,7 +15,9 @@ import Button from '@neos-project/react-ui-components/src/Button'; import Icon from '@neos-project/react-ui-components/src/Icon'; import {I18nRegistry} from '@neos-project/neos-ts-interfaces'; -class ErrorBoundary extends React.Component< +import styles from './style.module.css'; + +export class ErrorBoundary extends React.Component< { children: React.ReactNode, i18nRegistry: I18nRegistry }, { error: any } > { @@ -97,4 +107,3 @@ const ErrorFallback = (props: { error: any, i18nRegistry: I18nRegistry }) => { ; }; -export default ErrorBoundary; diff --git a/packages/neos-ui-error/src/container/ErrorBoundary/index.ts b/packages/neos-ui-error/src/container/ErrorBoundary/index.ts new file mode 100644 index 0000000000..24330aee86 --- /dev/null +++ b/packages/neos-ui-error/src/container/ErrorBoundary/index.ts @@ -0,0 +1,10 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {ErrorBoundary} from './ErrorBoundary'; diff --git a/packages/neos-ui/src/Containers/ErrorBoundary/style.module.css b/packages/neos-ui-error/src/container/ErrorBoundary/style.module.css similarity index 100% rename from packages/neos-ui/src/Containers/ErrorBoundary/style.module.css rename to packages/neos-ui-error/src/container/ErrorBoundary/style.module.css diff --git a/packages/neos-ui-error/src/container/index.ts b/packages/neos-ui-error/src/container/index.ts index e343e72ed9..530f5fd087 100644 --- a/packages/neos-ui-error/src/container/index.ts +++ b/packages/neos-ui-error/src/container/index.ts @@ -7,4 +7,5 @@ * information, please view the LICENSE file which was distributed with this * source code. */ +export {ErrorBoundary} from './ErrorBoundary'; export {ErrorView} from './ErrorView'; diff --git a/packages/neos-ui-error/src/index.ts b/packages/neos-ui-error/src/index.ts index a5950f1e2f..66e6da717a 100644 --- a/packages/neos-ui-error/src/index.ts +++ b/packages/neos-ui-error/src/index.ts @@ -14,4 +14,4 @@ export type { AnyError } from './types'; -export {ErrorView} from './container'; +export {ErrorBoundary, ErrorView} from './container'; diff --git a/packages/neos-ui/src/Containers/Root.js b/packages/neos-ui/src/Containers/Root.js index 242583db13..3a0cb78000 100644 --- a/packages/neos-ui/src/Containers/Root.js +++ b/packages/neos-ui/src/Containers/Root.js @@ -1,7 +1,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {Provider} from 'react-redux'; -import ErrorBoundary from './ErrorBoundary'; +import {ErrorBoundary} from '@neos-project/neos-ui-error'; import {DndProvider} from 'react-dnd'; import HTML5Backend from 'react-dnd-html5-backend'; import Neos from './Neos/index'; diff --git a/packages/neos-ui/src/System/terminateDueToFatalInitializationError.js b/packages/neos-ui/src/System/terminateDueToFatalInitializationError.js index 5edaecc5ac..13faffbfa7 100644 --- a/packages/neos-ui/src/System/terminateDueToFatalInitializationError.js +++ b/packages/neos-ui/src/System/terminateDueToFatalInitializationError.js @@ -1,6 +1,6 @@ import logo from '@neos-project/react-ui-components/src/Logo/logo.svg'; -import styles from '../Containers/ErrorBoundary/style.module.css'; +import styles from '@neos-project/neos-ui-error/src/container/ErrorBoundary/style.module.css'; export function terminateDueToFatalInitializationError(reason) { if (!document.body) { From 492f0cd05d21d734031b246295ac66931c4143bf Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Fri, 31 May 2024 12:00:04 +0200 Subject: [PATCH 05/11] TASK: Move function `terminateDueToFatalInitializationError` to neos-ui-error package --- packages/neos-ui-error/package.json | 1 + .../src/container/ErrorBoundary/index.ts | 1 + .../terminateDueToFatalInitializationError.ts} | 13 +++++++++++-- packages/neos-ui-error/src/container/index.ts | 2 +- packages/neos-ui-error/src/index.ts | 6 +++++- packages/neos-ui/src/System/index.js | 3 +-- packages/react-ui-components/src/global.d.ts | 1 + yarn.lock | 1 + 8 files changed, 22 insertions(+), 6 deletions(-) rename packages/{neos-ui/src/System/terminateDueToFatalInitializationError.js => neos-ui-error/src/container/ErrorBoundary/terminateDueToFatalInitializationError.ts} (60%) diff --git a/packages/neos-ui-error/package.json b/packages/neos-ui-error/package.json index b7f210a44c..124b7c3e1f 100644 --- a/packages/neos-ui-error/package.json +++ b/packages/neos-ui-error/package.json @@ -6,6 +6,7 @@ "main": "./src/index.ts", "dependencies": { "@neos-project/neos-ui-i18n": "workspace:*", + "@neos-project/react-ui-components": "workspace:*", "classnames": "^2.2.3", "react": "^16.12.0" }, diff --git a/packages/neos-ui-error/src/container/ErrorBoundary/index.ts b/packages/neos-ui-error/src/container/ErrorBoundary/index.ts index 24330aee86..e8149df1a8 100644 --- a/packages/neos-ui-error/src/container/ErrorBoundary/index.ts +++ b/packages/neos-ui-error/src/container/ErrorBoundary/index.ts @@ -8,3 +8,4 @@ * source code. */ export {ErrorBoundary} from './ErrorBoundary'; +export {terminateDueToFatalInitializationError} from './terminateDueToFatalInitializationError'; diff --git a/packages/neos-ui/src/System/terminateDueToFatalInitializationError.js b/packages/neos-ui-error/src/container/ErrorBoundary/terminateDueToFatalInitializationError.ts similarity index 60% rename from packages/neos-ui/src/System/terminateDueToFatalInitializationError.js rename to packages/neos-ui-error/src/container/ErrorBoundary/terminateDueToFatalInitializationError.ts index 13faffbfa7..b0f074e92b 100644 --- a/packages/neos-ui/src/System/terminateDueToFatalInitializationError.js +++ b/packages/neos-ui-error/src/container/ErrorBoundary/terminateDueToFatalInitializationError.ts @@ -1,8 +1,17 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ import logo from '@neos-project/react-ui-components/src/Logo/logo.svg'; -import styles from '@neos-project/neos-ui-error/src/container/ErrorBoundary/style.module.css'; +import styles from './style.module.css'; -export function terminateDueToFatalInitializationError(reason) { +export function terminateDueToFatalInitializationError(reason: string): void { if (!document.body) { throw new Error(reason); } diff --git a/packages/neos-ui-error/src/container/index.ts b/packages/neos-ui-error/src/container/index.ts index 530f5fd087..954653a8b0 100644 --- a/packages/neos-ui-error/src/container/index.ts +++ b/packages/neos-ui-error/src/container/index.ts @@ -7,5 +7,5 @@ * information, please view the LICENSE file which was distributed with this * source code. */ -export {ErrorBoundary} from './ErrorBoundary'; +export {ErrorBoundary, terminateDueToFatalInitializationError} from './ErrorBoundary'; export {ErrorView} from './ErrorView'; diff --git a/packages/neos-ui-error/src/index.ts b/packages/neos-ui-error/src/index.ts index 66e6da717a..83227623b8 100644 --- a/packages/neos-ui-error/src/index.ts +++ b/packages/neos-ui-error/src/index.ts @@ -14,4 +14,8 @@ export type { AnyError } from './types'; -export {ErrorBoundary, ErrorView} from './container'; +export { + ErrorBoundary, + ErrorView, + terminateDueToFatalInitializationError +} from './container'; diff --git a/packages/neos-ui/src/System/index.js b/packages/neos-ui/src/System/index.js index fca0607032..d1ae9857e5 100644 --- a/packages/neos-ui/src/System/index.js +++ b/packages/neos-ui/src/System/index.js @@ -1,7 +1,6 @@ import {initializeJsAPI} from '@neos-project/neos-ui-backend-connector'; import fetchWithErrorHandling from '@neos-project/neos-ui-backend-connector/src/FetchWithErrorHandling/index'; - -import {terminateDueToFatalInitializationError} from './terminateDueToFatalInitializationError'; +import {terminateDueToFatalInitializationError} from '@neos-project/neos-ui-error'; let initialData = null; function parseInitialData() { diff --git a/packages/react-ui-components/src/global.d.ts b/packages/react-ui-components/src/global.d.ts index 35306c6fc9..31e44ff3e2 100644 --- a/packages/react-ui-components/src/global.d.ts +++ b/packages/react-ui-components/src/global.d.ts @@ -1 +1,2 @@ declare module '*.css'; +declare module '*.svg'; diff --git a/yarn.lock b/yarn.lock index ed4a228d78..e9441ba281 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2442,6 +2442,7 @@ __metadata: resolution: "@neos-project/neos-ui-error@workspace:packages/neos-ui-error" dependencies: "@neos-project/neos-ui-i18n": "workspace:*" + "@neos-project/react-ui-components": "workspace:*" classnames: ^2.2.3 react: ^16.12.0 languageName: unknown From 7cd6b2d0c39ce452496fa674d248e2ba4839a702 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Fri, 31 May 2024 12:56:44 +0200 Subject: [PATCH 06/11] TASK: Move component FlashMessages to neos-ui-error package --- packages/neos-ui-error/package.json | 4 +++- .../container/FlashMessages}/FlashMessage.tsx | 0 .../container}/FlashMessages/FlashMessages.tsx | 2 +- .../src/container/FlashMessages}/index.ts | 2 +- .../container/FlashMessages}/style.module.css | 18 +++++++++++++++++- packages/neos-ui-error/src/container/index.ts | 1 + packages/neos-ui-error/src/index.ts | 1 + packages/neos-ui/src/Containers/App.js | 2 +- .../src/Containers/FlashMessages/index.ts | 10 ---------- .../Containers/FlashMessages/style.module.css | 15 --------------- yarn.lock | 2 ++ 11 files changed, 27 insertions(+), 30 deletions(-) rename packages/{neos-ui/src/Containers/FlashMessages/FlashMessage => neos-ui-error/src/container/FlashMessages}/FlashMessage.tsx (100%) rename packages/{neos-ui/src/Containers => neos-ui-error/src/container}/FlashMessages/FlashMessages.tsx (97%) rename packages/{neos-ui/src/Containers/FlashMessages/FlashMessage => neos-ui-error/src/container/FlashMessages}/index.ts (83%) rename packages/{neos-ui/src/Containers/FlashMessages/FlashMessage => neos-ui-error/src/container/FlashMessages}/style.module.css (77%) delete mode 100644 packages/neos-ui/src/Containers/FlashMessages/index.ts delete mode 100644 packages/neos-ui/src/Containers/FlashMessages/style.module.css diff --git a/packages/neos-ui-error/package.json b/packages/neos-ui-error/package.json index 124b7c3e1f..38151c7642 100644 --- a/packages/neos-ui-error/package.json +++ b/packages/neos-ui-error/package.json @@ -6,9 +6,11 @@ "main": "./src/index.ts", "dependencies": { "@neos-project/neos-ui-i18n": "workspace:*", + "@neos-project/neos-ui-redux-store": "workspace:*", "@neos-project/react-ui-components": "workspace:*", "classnames": "^2.2.3", - "react": "^16.12.0" + "react": "^16.12.0", + "react-redux": "^7.1.3" }, "license": "GNU GPLv3", "stableVersion": "8.3.4" diff --git a/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/FlashMessage.tsx b/packages/neos-ui-error/src/container/FlashMessages/FlashMessage.tsx similarity index 100% rename from packages/neos-ui/src/Containers/FlashMessages/FlashMessage/FlashMessage.tsx rename to packages/neos-ui-error/src/container/FlashMessages/FlashMessage.tsx diff --git a/packages/neos-ui/src/Containers/FlashMessages/FlashMessages.tsx b/packages/neos-ui-error/src/container/FlashMessages/FlashMessages.tsx similarity index 97% rename from packages/neos-ui/src/Containers/FlashMessages/FlashMessages.tsx rename to packages/neos-ui-error/src/container/FlashMessages/FlashMessages.tsx index 120f322d02..d2ea544737 100644 --- a/packages/neos-ui/src/Containers/FlashMessages/FlashMessages.tsx +++ b/packages/neos-ui-error/src/container/FlashMessages/FlashMessages.tsx @@ -14,7 +14,7 @@ import {connect} from 'react-redux'; import {actions} from '@neos-project/neos-ui-redux-store'; import {GlobalState} from '@neos-project/neos-ui-redux-store/src/System'; -import FlashMessage from './FlashMessage'; +import {FlashMessage} from './FlashMessage'; import style from './style.module.css'; diff --git a/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/index.ts b/packages/neos-ui-error/src/container/FlashMessages/index.ts similarity index 83% rename from packages/neos-ui/src/Containers/FlashMessages/FlashMessage/index.ts rename to packages/neos-ui-error/src/container/FlashMessages/index.ts index 24619772a9..097ed78742 100644 --- a/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/index.ts +++ b/packages/neos-ui-error/src/container/FlashMessages/index.ts @@ -7,4 +7,4 @@ * information, please view the LICENSE file which was distributed with this * source code. */ -export {FlashMessage as default} from './FlashMessage'; +export {FlashMessages} from './FlashMessages'; diff --git a/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/style.module.css b/packages/neos-ui-error/src/container/FlashMessages/style.module.css similarity index 77% rename from packages/neos-ui/src/Containers/FlashMessages/FlashMessage/style.module.css rename to packages/neos-ui-error/src/container/FlashMessages/style.module.css index a5a6bc1f2e..2c3b0378a8 100644 --- a/packages/neos-ui/src/Containers/FlashMessages/FlashMessage/style.module.css +++ b/packages/neos-ui-error/src/container/FlashMessages/style.module.css @@ -1,3 +1,19 @@ +.flashMessageContainer { + position: fixed; + z-index: var(--zIndex-FlashMessageContainer); + top: 0; + left: 50%; + width: 516px; + margin-top: 8px; + transform: translate(-50%, 0); + max-height: calc(100% - 16px); + overflow: auto; + + &:empty { + display: none; + } +} + .flashMessage { color: white; font-size: 14px; @@ -30,7 +46,7 @@ position: absolute; left: 0; top: 0; - height: 100%; + height: 100% !important; line-height: var(--spacing-GoldenUnit); width: var(--spacing-GoldenUnit) !important; text-align: center; diff --git a/packages/neos-ui-error/src/container/index.ts b/packages/neos-ui-error/src/container/index.ts index 954653a8b0..e4b02df376 100644 --- a/packages/neos-ui-error/src/container/index.ts +++ b/packages/neos-ui-error/src/container/index.ts @@ -9,3 +9,4 @@ */ export {ErrorBoundary, terminateDueToFatalInitializationError} from './ErrorBoundary'; export {ErrorView} from './ErrorView'; +export {FlashMessages} from './FlashMessages'; diff --git a/packages/neos-ui-error/src/index.ts b/packages/neos-ui-error/src/index.ts index 83227623b8..08c63eaeb3 100644 --- a/packages/neos-ui-error/src/index.ts +++ b/packages/neos-ui-error/src/index.ts @@ -17,5 +17,6 @@ export type { export { ErrorBoundary, ErrorView, + FlashMessages, terminateDueToFatalInitializationError } from './container'; diff --git a/packages/neos-ui/src/Containers/App.js b/packages/neos-ui/src/Containers/App.js index 21abe453f8..08fe022306 100644 --- a/packages/neos-ui/src/Containers/App.js +++ b/packages/neos-ui/src/Containers/App.js @@ -4,7 +4,7 @@ import {connect} from 'react-redux'; import mergeClassNames from 'classnames'; import style from './style.module.css'; -import FlashMessages from './FlashMessages/index'; +import {FlashMessages} from '@neos-project/neos-ui-error'; const App = ({globalRegistry, menu, isFullScreen, leftSidebarIsHidden, rightSidebarIsHidden}) => { const containerRegistry = globalRegistry.get('containers'); diff --git a/packages/neos-ui/src/Containers/FlashMessages/index.ts b/packages/neos-ui/src/Containers/FlashMessages/index.ts deleted file mode 100644 index d87c65f2db..0000000000 --- a/packages/neos-ui/src/Containers/FlashMessages/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * This file is part of the Neos.Neos.Ui package. - * - * (c) Contributors of the Neos Project - www.neos.io - * - * This package is Open Source Software. For the full copyright and license - * information, please view the LICENSE file which was distributed with this - * source code. - */ -export {FlashMessages as default} from './FlashMessages'; diff --git a/packages/neos-ui/src/Containers/FlashMessages/style.module.css b/packages/neos-ui/src/Containers/FlashMessages/style.module.css deleted file mode 100644 index 853c2c4c4f..0000000000 --- a/packages/neos-ui/src/Containers/FlashMessages/style.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.flashMessageContainer { - position: fixed; - z-index: var(--zIndex-FlashMessageContainer); - top: 0; - left: 50%; - width: 516px; - margin-top: 8px; - transform: translate(-50%, 0); - max-height: calc(100% - 16px); - overflow: auto; - - &:empty { - display: none; - } -} diff --git a/yarn.lock b/yarn.lock index e9441ba281..01a2b3f39c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2442,9 +2442,11 @@ __metadata: resolution: "@neos-project/neos-ui-error@workspace:packages/neos-ui-error" dependencies: "@neos-project/neos-ui-i18n": "workspace:*" + "@neos-project/neos-ui-redux-store": "workspace:*" "@neos-project/react-ui-components": "workspace:*" classnames: ^2.2.3 react: ^16.12.0 + react-redux: ^7.1.3 languageName: unknown linkType: soft From 2538b85a6738d3a3f631500ec93af8c6c85139b9 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Fri, 31 May 2024 21:00:12 +0200 Subject: [PATCH 07/11] TASK: Add package `@neos-project/framework-observable` This introduces a very basic implementation of the observable pattern for the Neos UI. This will allow us to replace redux opportunistically in various places. --- packages/framework-observable/README.md | 152 ++++++++++++++++++ packages/framework-observable/package.json | 8 + .../src/Observable.spec.ts | 94 +++++++++++ .../framework-observable/src/Observable.ts | 51 ++++++ packages/framework-observable/src/Observer.ts | 19 +++ .../framework-observable/src/State.spec.ts | 67 ++++++++ packages/framework-observable/src/State.ts | 61 +++++++ .../framework-observable/src/Subscriber.ts | 19 +++ .../framework-observable/src/Subscription.ts | 19 +++ packages/framework-observable/src/index.ts | 14 ++ yarn.lock | 6 + 11 files changed, 510 insertions(+) create mode 100644 packages/framework-observable/README.md create mode 100644 packages/framework-observable/package.json create mode 100644 packages/framework-observable/src/Observable.spec.ts create mode 100644 packages/framework-observable/src/Observable.ts create mode 100644 packages/framework-observable/src/Observer.ts create mode 100644 packages/framework-observable/src/State.spec.ts create mode 100644 packages/framework-observable/src/State.ts create mode 100644 packages/framework-observable/src/Subscriber.ts create mode 100644 packages/framework-observable/src/Subscription.ts create mode 100644 packages/framework-observable/src/index.ts diff --git a/packages/framework-observable/README.md b/packages/framework-observable/README.md new file mode 100644 index 0000000000..c350482578 --- /dev/null +++ b/packages/framework-observable/README.md @@ -0,0 +1,152 @@ +# @neos-project/framework-observable + +> Observable pattern implementation for the Neos UI + +> [!NOTE] +> This package implements a pattern for which there is a WICG proposal: +> https://github.com/WICG/observable +> +> It is therefore likely that future versions of this package will use the web-native `Observable` primitive under the hood. + +## API + +### Observables + +An `Observable` represents a sequence of values that can be *observed* from the outside. This is a powerful abstraction that allows to encapsule all kinds of value streams like: + +- (DOM) Events +- Timeouts & Intervals +- Async operations & Promises +- Websockets +- etc. + +An `Observable` can be created using the `createObservable` function like this: + +```typescript +const numbers$ = createObservable((next) => { + next(1); + next(2); + next(3); +}); +``` + +> [!NOTE] +> Suffixing variable names with `$` is a common naming convention to signify that a variable represents an observable. + +Here, the `numbers$` observable represents the sequence of the numbers 1, 2 and 3. This observable can be subscribed to thusly: + +```typescript +numbers$.subscribe((value) => { + console.log(value); +}); +``` + +Because the `numbers$` observable emits its values immediately, the above subscription will immediately log: +``` +1 +2 +3 +``` + +An additional subscription would also immediately receive all 3 values. By default, oberservables are *lazy* and *single-cast*. This means, values are generated exclusively for each subscription, and the generation starts exactly when a subscriber is registered. + +The usefulness of observables becomes more apparent when we introduce some asynchrony: +```typescript +const timedNumbers$ = createObservable((next) => { + let i = 1; + const interval = setInterval(() => { + next(i++); + }, 2000); + + return () => clearInterval(interval); +}); +``` + +This `timedNumbers$` observable will emit a new value every two seconds. This time, the callback used to facilitate the observable returns a function: +```typescript +// .. +return () => clearInterval(interval); +// .. +``` + +This function will be called when a subscription is cancelled. This is a way for observables to clean up after themselves. + +If we now subscribe to `timedNumbers$` like this: +```typescript +const subscription = timedNumbers$.subscribe((value) => { + console.log(value); +}); +``` + +The following values will be logged to the console: +``` +1 (After 2 seconds) +2 (After 4 seconds) +3 (After 6 seconds) +4 (After 8 seconds) +... +``` + +This will go on forever, unless we call the `unsubscribe` on our `subscription` which has been the return value we've saved from `timedNumber$.subscribe(...)`. When we call `unsubscribe`, the cleanup function of the `timedNumbers$` observable will be called and so the interval will be cleared: +```typescript +subscription.unsubscribe(); +``` + +That's all there is to it. With this small set of tools, `Observable`s can be used to encapsule all kinds of synchronous or asynchronous value streams. + +They can be created from a Promise: +```typescript +async function someLongRunningOperation() { + // ... +} + +const fromPromise$ = createObservable((next) => { + someLongRunningOperation().then(next); +}); +``` + +Or DOM events: +```typescript +const clicks$ = createObservable((next) => { + const button = document.querySelector('button'); + button.addEventListener('click', next); + return () => button.removeEventListener('click', next); +}); +``` + +And there are many, many more possibilities. + +### State + +A `State` is a special `Observable` that can track a value over time. `State`s can be created using the `createState` function like this: + +```typescript +const count$ = createState(0); +``` + +The `count$` state is now set to `0`. Unlike regular observables, a `State` instance can be queried for its current value: +```typescript +console.log(count$.current); // output: 0 +``` + +Each `State` instance has an `update` method that can be used to push new values to the state observable. It takes a callback that receives the current value as its first paramater and returns the new value: + +```typescript +count$.update((value) => value + 1); + +console.log(count$.current); // output: 1 +``` + +When a new subscriber is registered to a `State` instance, that subscriber immediately receives the current value: +```typescript +const count$ = createState(0); +count$.update((value) => value + 1); // nothing is logged, nobody has subscribed yet +count$.update((value) => value + 1); // nothing is logged, nobody has subscribed yet +count$.update((value) => value + 1); // nothing is logged, nobody has subscribed yet + +count$.subscribe((value) => console.log(value)); // immediately logs: 3 + +count$.update((value) => value + 1); // logs: 4 +``` + +Unlike regular `Observable`s, `State`s are multi-cast. This means that all subscribers receive updates at the same time, and every subscriber only receives updates that are published after the subscription has been registered. diff --git a/packages/framework-observable/package.json b/packages/framework-observable/package.json new file mode 100644 index 0000000000..d79d8c5fd9 --- /dev/null +++ b/packages/framework-observable/package.json @@ -0,0 +1,8 @@ +{ + "name": "@neos-project/framework-observable", + "version": "", + "description": "Observable pattern implementation for the Neos UI", + "private": true, + "main": "./src/index.ts", + "license": "GNU GPLv3" +} diff --git a/packages/framework-observable/src/Observable.spec.ts b/packages/framework-observable/src/Observable.spec.ts new file mode 100644 index 0000000000..04dfff09cd --- /dev/null +++ b/packages/framework-observable/src/Observable.spec.ts @@ -0,0 +1,94 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import {createObservable} from './Observable'; + +describe('Observable', () => { + test('emit some values and subscribe', () => { + const observable$ = createObservable((next) => { + next(1); + next(2); + next(3); + }); + const subscriber = { + next: jest.fn() + }; + + observable$.subscribe(subscriber); + + expect(subscriber.next).toHaveBeenCalledTimes(3); + expect(subscriber.next).toHaveBeenNthCalledWith(1, 1); + expect(subscriber.next).toHaveBeenNthCalledWith(2, 2); + expect(subscriber.next).toHaveBeenNthCalledWith(3, 3); + }); + + test('emit some values and subscribe a couple of times', () => { + const observable$ = createObservable((next) => { + next(1); + next(2); + next(3); + }); + const subscriber1 = { + next: jest.fn() + }; + const subscriber2 = { + next: jest.fn() + }; + const subscriber3 = { + next: jest.fn() + }; + + observable$.subscribe(subscriber1); + observable$.subscribe(subscriber2); + observable$.subscribe(subscriber3); + + expect(subscriber1.next).toHaveBeenCalledTimes(3); + expect(subscriber1.next).toHaveBeenNthCalledWith(1, 1); + expect(subscriber1.next).toHaveBeenNthCalledWith(2, 2); + expect(subscriber1.next).toHaveBeenNthCalledWith(3, 3); + + expect(subscriber2.next).toHaveBeenCalledTimes(3); + expect(subscriber2.next).toHaveBeenNthCalledWith(1, 1); + expect(subscriber2.next).toHaveBeenNthCalledWith(2, 2); + expect(subscriber2.next).toHaveBeenNthCalledWith(3, 3); + + expect(subscriber3.next).toHaveBeenCalledTimes(3); + expect(subscriber3.next).toHaveBeenNthCalledWith(1, 1); + expect(subscriber3.next).toHaveBeenNthCalledWith(2, 2); + expect(subscriber3.next).toHaveBeenNthCalledWith(3, 3); + }); + + test('emit no values, subscribe and unsubscribe', () => { + const unsubscribe = jest.fn(); + const observable$ = createObservable(() => { + return unsubscribe; + }); + const subscriber = { + next: jest.fn() + }; + + const subscription = observable$.subscribe(subscriber); + subscription.unsubscribe(); + + expect(subscriber.next).toHaveBeenCalledTimes(0); + expect(unsubscribe).toHaveBeenCalledTimes(1); + }); + + test('emit no values, subscribe and unsubscribe with void observer', () => { + const observable$ = createObservable(() => {}); + const subscriber = { + next: jest.fn() + }; + + const subscription = observable$.subscribe(subscriber); + + expect(() => subscription.unsubscribe()).not.toThrow(); + expect(subscriber.next).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/framework-observable/src/Observable.ts b/packages/framework-observable/src/Observable.ts new file mode 100644 index 0000000000..482fb87a9d --- /dev/null +++ b/packages/framework-observable/src/Observable.ts @@ -0,0 +1,51 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import type {Subscriber} from './Subscriber'; +import type {Subscription} from './Subscription'; +import type {Observer} from './Observer'; + +/** + * An Observable emits values over time. You can attach a subscriber to it + * using the Observable's `subscribe` method, or you can perform operations + * producing new Observables via its `pipe` method. + */ +export interface Observable { + subscribe: (subscriber: Subscriber) => Subscription; +} + +/** + * An ObservablePipeOperation is a function that takes an observable and + * returns a new observable. It can be passed to any Observable's `pipe` + * method. + */ +export interface ObservablePipeOperation { + (observable: Observable): Observable; +} + +/** + * Creates an Observable from the given Observer. + */ +export function createObservable(observer: Observer): Observable { + const observable: Observable = { + subscribe(subscriber) { + return Object.freeze({ + unsubscribe: observer( + subscriber.next, + subscriber.error ?? noop + ) ?? noop + }); + } + }; + + return Object.freeze(observable); +} + +function noop() { +} diff --git a/packages/framework-observable/src/Observer.ts b/packages/framework-observable/src/Observer.ts new file mode 100644 index 0000000000..b9c145b0ac --- /dev/null +++ b/packages/framework-observable/src/Observer.ts @@ -0,0 +1,19 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +/** + * An Observer is a function that emits values via its `next` callback. It can + * return a function that handles all logic that must be performed when a + * Subscription is cancelled (e.g. clearTimeout or similar cancellation + * effects). + */ +export interface Observer { + (next: (value: V) => void, fail: (error: any) => void): void | (() => void); +} diff --git a/packages/framework-observable/src/State.spec.ts b/packages/framework-observable/src/State.spec.ts new file mode 100644 index 0000000000..b573a6c880 --- /dev/null +++ b/packages/framework-observable/src/State.spec.ts @@ -0,0 +1,67 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import {createState} from './State'; + +describe('State', () => { + test('get current value', () => { + const state$ = createState(0); + + expect(state$.current).toBe(0); + + state$.update((value) => value + 1); + expect(state$.current).toBe(1); + + state$.update((value) => value + 1); + expect(state$.current).toBe(2); + + state$.update((value) => value + 1); + expect(state$.current).toBe(3); + }); + + test('subscribe to state updates: subscriber receives current value immediately', () => { + const state$ = createState(0); + const subscriber1 = { + next: jest.fn() + }; + const subscriber2 = { + next: jest.fn() + }; + + state$.subscribe(subscriber1); + expect(subscriber1.next).toHaveBeenCalledTimes(1); + expect(subscriber1.next).toHaveBeenNthCalledWith(1, 0); + + state$.update((value) => value + 1); + state$.update((value) => value + 1); + state$.update((value) => value + 1); + + state$.subscribe(subscriber2); + expect(subscriber2.next).toHaveBeenCalledTimes(1); + expect(subscriber2.next).toHaveBeenNthCalledWith(1, 3); + }); + + test('subscribe to state updates: subscriber receives all updates', () => { + const state$ = createState(0); + const subscriber = { + next: jest.fn() + }; + + state$.subscribe(subscriber); + state$.update((value) => value + 1); + state$.update((value) => value + 1); + state$.update((value) => value + 1); + + expect(subscriber.next).toHaveBeenCalledTimes(4); + expect(subscriber.next).toHaveBeenNthCalledWith(1, 0); + expect(subscriber.next).toHaveBeenNthCalledWith(2, 1); + expect(subscriber.next).toHaveBeenNthCalledWith(3, 2); + expect(subscriber.next).toHaveBeenNthCalledWith(4, 3); + }); +}); diff --git a/packages/framework-observable/src/State.ts b/packages/framework-observable/src/State.ts new file mode 100644 index 0000000000..c0d63a9f2f --- /dev/null +++ b/packages/framework-observable/src/State.ts @@ -0,0 +1,61 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import {createObservable, Observable} from './Observable'; + +/** + * A State is a special kind of Observable that keeps track of a value over + * time. + * + * It has a public readonly `current` property that allows you to ask for + * its current value at any point in time. A new subscriber to the State + * Observable will also immediately receive the current value at the time of + * subscription. + * + * Via the `update` method, a State's value can be modified. When called, + * Subscribers to the state are immediately informed about the new value. + */ +export interface State extends Observable { + readonly current: V; + update: (updateFn: (current: V) => V) => void; +} + +/** + * Creates a new State with the given initial value. + */ +export function createState(initialValue: V): State { + let currentState = initialValue; + const listeners = new Set<(value: V) => void>(); + const state: State = { + ...createObservable((next) => { + listeners.add(next); + next(currentState); + + return () => listeners.delete(next); + }), + + get current() { + return currentState; + }, + + update(updateFn) { + const nextState = updateFn(currentState); + + if (currentState !== nextState) { + currentState = nextState; + + for (const next of listeners) { + next(currentState); + } + } + } + }; + + return Object.freeze(state); +} diff --git a/packages/framework-observable/src/Subscriber.ts b/packages/framework-observable/src/Subscriber.ts new file mode 100644 index 0000000000..ac8f5c963e --- /dev/null +++ b/packages/framework-observable/src/Subscriber.ts @@ -0,0 +1,19 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +/** + * A Subscriber can be attached to an Observable. It receives values from the + * Observable in its `next` callback function. It may also provide an optional + * `error` callback, that will only be called if the Observable emits an Error. + */ +export interface Subscriber { + next: (value: V) => void; + error?: (error: Error) => void; +} diff --git a/packages/framework-observable/src/Subscription.ts b/packages/framework-observable/src/Subscription.ts new file mode 100644 index 0000000000..3821d0d04a --- /dev/null +++ b/packages/framework-observable/src/Subscription.ts @@ -0,0 +1,19 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ + +/** + * When attaching a Subscriber to an Observable, a Subscription is returned. + * The `unsubscribe` method of the Subscription allows you to detach the + * Subscriber from the Observable again, after which the Subscriber no longer + * receives any values emitted from the Observable. + */ +export interface Subscription { + unsubscribe: () => void; +} diff --git a/packages/framework-observable/src/index.ts b/packages/framework-observable/src/index.ts new file mode 100644 index 0000000000..b84ad0927e --- /dev/null +++ b/packages/framework-observable/src/index.ts @@ -0,0 +1,14 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export type {Observable} from './Observable'; +export {createObservable} from './Observable'; + +export type {State} from './State'; +export {createState} from './State'; diff --git a/yarn.lock b/yarn.lock index 01a2b3f39c..4b2d185897 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2277,6 +2277,12 @@ __metadata: languageName: node linkType: hard +"@neos-project/framework-observable@workspace:*, @neos-project/framework-observable@workspace:packages/framework-observable": + version: 0.0.0-use.local + resolution: "@neos-project/framework-observable@workspace:packages/framework-observable" + languageName: unknown + linkType: soft + "@neos-project/jest-preset-neos-ui@workspace:*, @neos-project/jest-preset-neos-ui@workspace:packages/jest-preset-neos-ui": version: 0.0.0-use.local resolution: "@neos-project/jest-preset-neos-ui@workspace:packages/jest-preset-neos-ui" From 3a9090e80619a92f43ba48c9208ff3f6f5833f09 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Fri, 31 May 2024 21:01:47 +0200 Subject: [PATCH 08/11] TASK: Add package `@neos-project/framework-observable-react` This new package contains react bindings for the other recently introduced package `@neos-project/framework-observable`. --- packages/framework-observable-react/README.md | 120 ++++++++++++++++++ .../framework-observable-react/package.json | 12 ++ .../framework-observable-react/src/index.ts | 11 ++ .../src/useLatestState.ts | 16 +++ .../src/useLatestValueFrom.ts | 41 ++++++ yarn.lock | 9 ++ 6 files changed, 209 insertions(+) create mode 100644 packages/framework-observable-react/README.md create mode 100644 packages/framework-observable-react/package.json create mode 100644 packages/framework-observable-react/src/index.ts create mode 100644 packages/framework-observable-react/src/useLatestState.ts create mode 100644 packages/framework-observable-react/src/useLatestValueFrom.ts diff --git a/packages/framework-observable-react/README.md b/packages/framework-observable-react/README.md new file mode 100644 index 0000000000..82b3625e2c --- /dev/null +++ b/packages/framework-observable-react/README.md @@ -0,0 +1,120 @@ +# @neos-project/framework-observable-react + +> React bindings for @neos-project/framework-observable + +This package provides a set of React [hooks](https://react.dev/reference/react/hooks) to let components interact with `Observable`s. + +## API + +### `useLatestValueFrom` + +```typescript +// Without default value: +function useLatestValueFrom(observable$: Observable): null | V; + +// With default value: +function useLatestValueFrom( + observable$: Observable, + defaultValue: D +): D | V; +``` + +`useLatestValueFrom` is a way to bind a react component the latest value emitted from an `Observable`. + +#### Parameters + +| Name | Description | +| ------------------------- | ---------------------------------------------------------------------------------------------- | +| `observable$` | The `Observable` to subscribe to | +| `defaultValue` (optional) | The value to default for when `observable$` hasn't emitted any values yet (defaults to `null`) | + +#### Return Value + +This hook returns the latest value from the provided `observable$`. If no value has been emitted from the observable yet, it returns `defaultValue` which itself defaults to `null`. + +#### Example + +This component will display the amount of seconds that have passed since it was first mounted: + +```typescript +const clock$ = createObservable((next) => { + let i = 1; + const interval = setInterval(() => { + next(i++); + }, 1000); + + return () => clearInterval(interval); +}); + +const MyComponent = () => { + const seconds = useLatestValueFrom(clock$, 0); + + return
{seconds} seconds passed
; +}; +``` + +You can combine this with `React.useMemo`, if you wish to create an ad-hoc observable: + +```typescript +const MyComponent = (props) => { + const beats = useLatestValueFrom( + React.useMemo( + () => + createObservable((next) => { + let i = 1; + const interval = setInterval(() => { + next(i++); + }, props.millisecondsPerBeat); + + return () => clearInterval(interval); + }), + [props.millisecondsPerBeat] + ), + 0 + ); + + return
{beats} beats passed
; +}; +``` + +### `useLatestState` + +```typescript +function useLatestState(state$: State): V; +``` + +`useLatestState` subscribes to a given state observable and keeps track of its latest value. + +#### Parameters + +| Name | Description | +| -------- | --------------------------------------- | +| `state$` | The `State` observable to keep track of | + +#### Return Value + +This hook returns the latest value from the given `State` observable. Initially it contains the current value of the `State` at the moment the component was first mounted. + +#### Example + +```typescript +const count$ = createState(0); + +const MyComponent = () => { + const count = useLatestState(count$); + const handleInc = React.useCallback(() => { + count$.update((count) => count + 1); + }, []); + const handleDec = React.useCallback(() => { + count$.update((count) => count - 1); + }, []); + + return ( +
+
Count {count}
+ + +
+ ); +}; +``` diff --git a/packages/framework-observable-react/package.json b/packages/framework-observable-react/package.json new file mode 100644 index 0000000000..ae114ecebb --- /dev/null +++ b/packages/framework-observable-react/package.json @@ -0,0 +1,12 @@ +{ + "name": "@neos-project/framework-observable-react", + "version": "", + "description": "React bindings for @neos-project/framework-observable", + "private": true, + "main": "./src/index.ts", + "dependencies": { + "@neos-project/framework-observable": "workspace:*", + "react": "^16.12.0" + }, + "license": "GNU GPLv3" +} diff --git a/packages/framework-observable-react/src/index.ts b/packages/framework-observable-react/src/index.ts new file mode 100644 index 0000000000..1ddd85b034 --- /dev/null +++ b/packages/framework-observable-react/src/index.ts @@ -0,0 +1,11 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {useLatestState} from './useLatestState'; +export {useLatestValueFrom} from './useLatestValueFrom'; diff --git a/packages/framework-observable-react/src/useLatestState.ts b/packages/framework-observable-react/src/useLatestState.ts new file mode 100644 index 0000000000..d7b23bad5c --- /dev/null +++ b/packages/framework-observable-react/src/useLatestState.ts @@ -0,0 +1,16 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import type {State} from '@neos-project/framework-observable'; + +import {useLatestValueFrom} from './useLatestValueFrom'; + +export function useLatestState(state$: State) { + return useLatestValueFrom(state$, state$.current); +} diff --git a/packages/framework-observable-react/src/useLatestValueFrom.ts b/packages/framework-observable-react/src/useLatestValueFrom.ts new file mode 100644 index 0000000000..9158face7d --- /dev/null +++ b/packages/framework-observable-react/src/useLatestValueFrom.ts @@ -0,0 +1,41 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import React from 'react'; + +import type {Observable} from '@neos-project/framework-observable'; + +export function useLatestValueFrom(observable$: Observable): null | V; +export function useLatestValueFrom( + observable$: Observable, + defaultValue: D +): D | V; + +export function useLatestValueFrom( + observable$: Observable, + defaultValue?: D +) { + const [value, setValue] = React.useState( + defaultValue ?? null + ); + + React.useEffect(() => { + const subscription = observable$.subscribe({ + next: (incomingValue) => { + if (incomingValue !== value) { + setValue(incomingValue); + } + } + }); + + return () => subscription.unsubscribe(); + }, [observable$]); + + return value; +} diff --git a/yarn.lock b/yarn.lock index 4b2d185897..99561c9516 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2277,6 +2277,15 @@ __metadata: languageName: node linkType: hard +"@neos-project/framework-observable-react@workspace:packages/framework-observable-react": + version: 0.0.0-use.local + resolution: "@neos-project/framework-observable-react@workspace:packages/framework-observable-react" + dependencies: + "@neos-project/framework-observable": "workspace:*" + react: ^16.12.0 + languageName: unknown + linkType: soft + "@neos-project/framework-observable@workspace:*, @neos-project/framework-observable@workspace:packages/framework-observable": version: 0.0.0-use.local resolution: "@neos-project/framework-observable@workspace:packages/framework-observable" From ff458b6064bc563bb10083df5c5b6fb5a5334976 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Fri, 31 May 2024 21:10:22 +0200 Subject: [PATCH 09/11] TASK: Decouple FlashMessage from redux store ...by using the newly introduced observable primitives. The neos-ui-error package now exposes a method to show flash messages. A saga has been created to redirect redux actions (that may still be used in plugins) to that method. --- packages/neos-ui-error/package.json | 6 +- .../container/FlashMessages/FlashMessage.tsx | 4 +- .../container/FlashMessages/FlashMessages.tsx | 67 ++++++++++++------- .../src/container/FlashMessages/index.ts | 2 +- packages/neos-ui-error/src/container/index.ts | 2 +- packages/neos-ui-error/src/index.ts | 4 +- packages/neos-ui-error/src/types.ts | 1 + packages/neos-ui-sagas/package.json | 1 + .../src/UI/FlashMessages/index.ts | 29 ++++++++ packages/neos-ui-sagas/src/index.js | 1 + packages/neos-ui-sagas/src/manifest.js | 5 +- yarn.lock | 7 +- 12 files changed, 95 insertions(+), 34 deletions(-) create mode 100644 packages/neos-ui-sagas/src/UI/FlashMessages/index.ts diff --git a/packages/neos-ui-error/package.json b/packages/neos-ui-error/package.json index 38151c7642..20ce5842d7 100644 --- a/packages/neos-ui-error/package.json +++ b/packages/neos-ui-error/package.json @@ -5,12 +5,12 @@ "private": true, "main": "./src/index.ts", "dependencies": { + "@neos-project/framework-observable": "workspace:*", + "@neos-project/framework-observable-react": "workspace:*", "@neos-project/neos-ui-i18n": "workspace:*", - "@neos-project/neos-ui-redux-store": "workspace:*", "@neos-project/react-ui-components": "workspace:*", "classnames": "^2.2.3", - "react": "^16.12.0", - "react-redux": "^7.1.3" + "react": "^16.12.0" }, "license": "GNU GPLv3", "stableVersion": "8.3.4" diff --git a/packages/neos-ui-error/src/container/FlashMessages/FlashMessage.tsx b/packages/neos-ui-error/src/container/FlashMessages/FlashMessage.tsx index 1bdb2e0a10..cfaa8ce7f4 100644 --- a/packages/neos-ui-error/src/container/FlashMessages/FlashMessage.tsx +++ b/packages/neos-ui-error/src/container/FlashMessages/FlashMessage.tsx @@ -12,12 +12,14 @@ import mergeClassNames from 'classnames'; import {IconButton, Icon} from '@neos-project/react-ui-components'; +import {Severity} from '../../types'; + import style from './style.module.css'; export const FlashMessage: React.FC<{ id: string; message: string; - severity: 'success' | 'error' | 'info'; + severity: Severity; timeout?: number; onClose: (id: string) => void; diff --git a/packages/neos-ui-error/src/container/FlashMessages/FlashMessages.tsx b/packages/neos-ui-error/src/container/FlashMessages/FlashMessages.tsx index d2ea544737..85fdd3a9b2 100644 --- a/packages/neos-ui-error/src/container/FlashMessages/FlashMessages.tsx +++ b/packages/neos-ui-error/src/container/FlashMessages/FlashMessages.tsx @@ -8,32 +8,55 @@ * source code. */ import React from 'react'; -// @ts-ignore -import {connect} from 'react-redux'; -import {actions} from '@neos-project/neos-ui-redux-store'; -import {GlobalState} from '@neos-project/neos-ui-redux-store/src/System'; +import {createState} from '@neos-project/framework-observable'; +import {useLatestState} from '@neos-project/framework-observable-react'; import {FlashMessage} from './FlashMessage'; +import {Severity} from '../../types'; + import style from './style.module.css'; -const withReduxState = connect((state: GlobalState) => ({ - flashMessages: state?.ui?.flashMessages -}), { - removeMessage: actions.UI.FlashMessages.remove -}); - -const StatelessFlashMessages: React.FC<{ - flashMessages: Record; - removeMessage: (id: string) => void; -}> = (props) => { - const {flashMessages, removeMessage} = props; +const flashMessages$ = createState>({}); + +export function showFlashMessage(flashMessage: { + id: string; + message: string; + severity?: Severity; + timeout?: number; +}) { + const flashMessageWithDefaults = { + id: flashMessage.id, + message: flashMessage.message, + severity: flashMessage.severity ?? 'info', + timeout: flashMessage.timeout + }; + + flashMessages$.update((flashMessages) => ({ + ...flashMessages, + [flashMessageWithDefaults.id]: flashMessageWithDefaults + })); +} + +function removeFlashMessage(id: string) { + flashMessages$.update((flashMessages) => { + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [id]: _, + ...remainingFlashMessages + } = flashMessages; + return remainingFlashMessages; + }); +} + +export const FlashMessages: React.FC = () => { + const flashMessages = useLatestState(flashMessages$); return (
@@ -48,12 +71,10 @@ const StatelessFlashMessages: React.FC<{ message={message} severity={severity} timeout={timeout} - onClose={removeMessage} + onClose={removeFlashMessage} /> ); })}
); } - -export const FlashMessages = withReduxState(StatelessFlashMessages); diff --git a/packages/neos-ui-error/src/container/FlashMessages/index.ts b/packages/neos-ui-error/src/container/FlashMessages/index.ts index 097ed78742..f2279f27a1 100644 --- a/packages/neos-ui-error/src/container/FlashMessages/index.ts +++ b/packages/neos-ui-error/src/container/FlashMessages/index.ts @@ -7,4 +7,4 @@ * information, please view the LICENSE file which was distributed with this * source code. */ -export {FlashMessages} from './FlashMessages'; +export {FlashMessages, showFlashMessage} from './FlashMessages'; diff --git a/packages/neos-ui-error/src/container/index.ts b/packages/neos-ui-error/src/container/index.ts index e4b02df376..507ef19d3c 100644 --- a/packages/neos-ui-error/src/container/index.ts +++ b/packages/neos-ui-error/src/container/index.ts @@ -9,4 +9,4 @@ */ export {ErrorBoundary, terminateDueToFatalInitializationError} from './ErrorBoundary'; export {ErrorView} from './ErrorView'; -export {FlashMessages} from './FlashMessages'; +export {FlashMessages, showFlashMessage} from './FlashMessages'; diff --git a/packages/neos-ui-error/src/index.ts b/packages/neos-ui-error/src/index.ts index 08c63eaeb3..82fa0fedab 100644 --- a/packages/neos-ui-error/src/index.ts +++ b/packages/neos-ui-error/src/index.ts @@ -11,12 +11,14 @@ export type { ECMAScriptError as ClientSideError, ServerSideError, StringError, - AnyError + AnyError, + Severity } from './types'; export { ErrorBoundary, ErrorView, FlashMessages, + showFlashMessage, terminateDueToFatalInitializationError } from './container'; diff --git a/packages/neos-ui-error/src/types.ts b/packages/neos-ui-error/src/types.ts index dc529db4d5..6228781e68 100644 --- a/packages/neos-ui-error/src/types.ts +++ b/packages/neos-ui-error/src/types.ts @@ -17,6 +17,7 @@ export type ServerSideError = { }; export type StringError = string; export type AnyError = ECMAScriptError | ServerSideError | StringError; +export type Severity = 'success' | 'error' | 'info'; export function isECMAScriptError(candidate: unknown): candidate is ECMAScriptError { return candidate instanceof Error; diff --git a/packages/neos-ui-sagas/package.json b/packages/neos-ui-sagas/package.json index ae10867255..99edf11223 100644 --- a/packages/neos-ui-sagas/package.json +++ b/packages/neos-ui-sagas/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@neos-project/neos-ui-backend-connector": "workspace:*", + "@neos-project/neos-ui-error": "workspace:*", "@neos-project/neos-ui-extensibility": "workspace:*", "@neos-project/neos-ui-guest-frame": "workspace:*", "@neos-project/neos-ui-redux-store": "workspace:*", diff --git a/packages/neos-ui-sagas/src/UI/FlashMessages/index.ts b/packages/neos-ui-sagas/src/UI/FlashMessages/index.ts new file mode 100644 index 0000000000..a40562f094 --- /dev/null +++ b/packages/neos-ui-sagas/src/UI/FlashMessages/index.ts @@ -0,0 +1,29 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import {takeEvery} from 'redux-saga/effects'; + +import {actionTypes, actions} from '@neos-project/neos-ui-redux-store'; +import {Severity, showFlashMessage} from '@neos-project/neos-ui-error'; + +export function * legacy__redirectReduxFlashMessagesToHighLevelApiCall() { + yield takeEvery( + actionTypes.UI.FlashMessages.ADD, + function restore( + action: ReturnType + ) { + showFlashMessage({ + id: action.payload.id, + message: action.payload.message, + severity: action.payload.severity.toLowerCase() as Severity, + timeout: action.payload.timeout + }); + } + ); +} diff --git a/packages/neos-ui-sagas/src/index.js b/packages/neos-ui-sagas/src/index.js index c0a6392b6b..7a78469b3a 100644 --- a/packages/neos-ui-sagas/src/index.js +++ b/packages/neos-ui-sagas/src/index.js @@ -12,4 +12,5 @@ export * as uiInspector from './UI/Inspector/index'; export * as uiPageTree from './UI/PageTree/index'; export * as uiHotkeys from './UI/Hotkeys/index'; export * as impersonate from './UI/Impersonate/index'; +export * as flashMessages from './UI/FlashMessages/index'; export * as sync from './Sync/index'; diff --git a/packages/neos-ui-sagas/src/manifest.js b/packages/neos-ui-sagas/src/manifest.js index ac9db2832d..4e226a187d 100644 --- a/packages/neos-ui-sagas/src/manifest.js +++ b/packages/neos-ui-sagas/src/manifest.js @@ -16,7 +16,8 @@ import { uiInspector, uiPageTree, uiHotkeys, - impersonate + impersonate, + flashMessages } from './index'; manifest('main.sagas', {}, globalRegistry => { @@ -81,4 +82,6 @@ manifest('main.sagas', {}, globalRegistry => { sagasRegistry.set('neos-ui/UI/Hotkeys/handleHotkeys', {saga: uiHotkeys.handleHotkeys}); sagasRegistry.set('neos-ui/UI/Impersonate/impersonateRestore', {saga: impersonate.impersonateRestore}); + + sagasRegistry.set('neos-ui/UI/FlashMessages/legacy__redirectReduxFlashMessagesToHighLevelApiCall', {saga: flashMessages.legacy__redirectReduxFlashMessagesToHighLevelApiCall}); }); diff --git a/yarn.lock b/yarn.lock index 99561c9516..046245605f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2277,7 +2277,7 @@ __metadata: languageName: node linkType: hard -"@neos-project/framework-observable-react@workspace:packages/framework-observable-react": +"@neos-project/framework-observable-react@workspace:*, @neos-project/framework-observable-react@workspace:packages/framework-observable-react": version: 0.0.0-use.local resolution: "@neos-project/framework-observable-react@workspace:packages/framework-observable-react" dependencies: @@ -2456,12 +2456,12 @@ __metadata: version: 0.0.0-use.local resolution: "@neos-project/neos-ui-error@workspace:packages/neos-ui-error" dependencies: + "@neos-project/framework-observable": "workspace:*" + "@neos-project/framework-observable-react": "workspace:*" "@neos-project/neos-ui-i18n": "workspace:*" - "@neos-project/neos-ui-redux-store": "workspace:*" "@neos-project/react-ui-components": "workspace:*" classnames: ^2.2.3 react: ^16.12.0 - react-redux: ^7.1.3 languageName: unknown linkType: soft @@ -2569,6 +2569,7 @@ __metadata: dependencies: "@neos-project/jest-preset-neos-ui": "workspace:*" "@neos-project/neos-ui-backend-connector": "workspace:*" + "@neos-project/neos-ui-error": "workspace:*" "@neos-project/neos-ui-extensibility": "workspace:*" "@neos-project/neos-ui-guest-frame": "workspace:*" "@neos-project/neos-ui-redux-store": "workspace:*" From 2258a0ba565ec8dbba29e561553b6520da6ae477 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Thu, 6 Jun 2024 20:46:37 +0200 Subject: [PATCH 10/11] !!!TASK: Delete obsolete redux-store elements for flash messages --- .../src/UI/FlashMessages/index.spec.js | 108 +----------------- .../src/UI/FlashMessages/index.ts | 69 +---------- packages/neos-ui-redux-store/src/UI/index.ts | 3 - 3 files changed, 3 insertions(+), 177 deletions(-) diff --git a/packages/neos-ui-redux-store/src/UI/FlashMessages/index.spec.js b/packages/neos-ui-redux-store/src/UI/FlashMessages/index.spec.js index ec2f0d759a..76181d64f3 100644 --- a/packages/neos-ui-redux-store/src/UI/FlashMessages/index.spec.js +++ b/packages/neos-ui-redux-store/src/UI/FlashMessages/index.spec.js @@ -1,117 +1,11 @@ -import {actionTypes, actions, reducer} from './index'; - -import {actionTypes as system} from '../../System/index'; +import {actionTypes, actions} from './index'; test(`should export actionTypes`, () => { expect(actionTypes).not.toBe(undefined); expect(typeof (actionTypes.ADD)).toBe('string'); - expect(typeof (actionTypes.REMOVE)).toBe('string'); }); test(`should export action creators`, () => { expect(actions).not.toBe(undefined); expect(typeof (actions.add)).toBe('function'); - expect(typeof (actions.remove)).toBe('function'); -}); - -test(`should export a reducer`, () => { - expect(reducer).not.toBe(undefined); - expect(typeof (reducer)).toBe('function'); -}); - -test(`The reducer should return a plain JS object as the initial state.`, () => { - const nextState = reducer(undefined, { - type: system.INIT - }); - - expect(typeof nextState).toBe('object'); -}); - -test(`The "add" action should throw an error if no arguments where passed.`, () => { - const fn = () => reducer(undefined, actions.add()); - - expect(fn).toThrowError( - 'Empty or non existent "id" passed to the addFlashMessage reducer. Please specify a string containing a random id.' - ); -}); - -test(`The "add" action should throw an error no "message" was passed.`, () => { - const fn = () => reducer(undefined, actions.add('myMessageId', null)); - - expect(fn).toThrowError( - 'Empty or non existent "message" passed to the addFlashMessage reducer. Please specify a string containing your desired message.' - ); -}); - -test(`The "add" action should throw an error if an invalid "severity" was passed.`, () => { - const fn = () => reducer(undefined, actions.add('myMessageId', 'myMessage', null)); - - expect(fn).toThrowError( - 'Invalid "severity" specified while adding a new FlashMessage. Allowed severities are success error info.' - ); -}); - -test(` - The "add" action should be able to add the passed data as a new flashMessage - item.`, () => { - const state = {}; - const nextState = reducer(state, actions.add('myMessageId', 'myMessage', 'error', 300)); - - const addedMessage = nextState.myMessageId; - - expect(addedMessage).toEqual({ - severity: 'error', - id: 'myMessageId', - message: 'myMessage', - timeout: 300 - }); -}); - -test(` - The "add" action should normalize the severity to lowercase for the new - flashMessage item.`, () => { - const state = {}; - const nextState1 = reducer(state, actions.add('myMessageId', 'myMessage', 'Error', 300)); - const nextState2 = reducer(state, actions.add('myMessageId', 'myMessage', 'ERROR', 300)); - const nextState3 = reducer(state, actions.add('myMessageId', 'myMessage', 'eRrOr', 300)); - - const addedMessage1 = nextState1.myMessageId; - const addedMessage2 = nextState2.myMessageId; - const addedMessage3 = nextState3.myMessageId; - - expect(addedMessage1.severity).toBe('error'); - expect(addedMessage2.severity).toBe('error'); - expect(addedMessage3.severity).toBe('error'); -}); - -test(` - The "add" action should set a default timeout of "0" if none was passed for - the new flashMessage item.`, () => { - const state = {}; - const nextState = reducer(state, actions.add('myMessageId', 'myMessage', 'error')); - - const addedMessage = nextState.myMessageId; - - expect(addedMessage.timeout).toBe(0); -}); - -test(` - The "remove" action should be able to remove an added flashMessage item for - the passed key.`, () => { - const state = { - someMessage: 'someMessage', - anotherMessage: 'anotherMessage' - }; - - const nextState1 = reducer(state, actions.remove('someMessage')); - const nextState2 = reducer(state, actions.remove('anotherMessage')); - const nextState3 = reducer(nextState1, actions.remove('anotherMessage')); - - expect(nextState1).toEqual({ - anotherMessage: 'anotherMessage' - }); - expect(nextState2).toEqual({ - someMessage: 'someMessage' - }); - expect(nextState3).toEqual({}); }); diff --git a/packages/neos-ui-redux-store/src/UI/FlashMessages/index.ts b/packages/neos-ui-redux-store/src/UI/FlashMessages/index.ts index b3bfcf0c55..dd388dadb1 100644 --- a/packages/neos-ui-redux-store/src/UI/FlashMessages/index.ts +++ b/packages/neos-ui-redux-store/src/UI/FlashMessages/index.ts @@ -1,27 +1,10 @@ -import produce from 'immer'; import {action as createAction, ActionType} from 'typesafe-actions'; -import {InitAction} from '../../System'; - -export interface FlashMessage extends Readonly<{ - severity: string; - id: string; - message: string; - timeout: number; -}> {} - -export interface State extends Readonly<{ - [propName: string]: FlashMessage; -}> {} - -export const defaultState: State = {}; - // // Export the action types // export enum actionTypes { - ADD = '@neos/neos-ui/UI/FlashMessages/ADD', - REMOVE = '@neos/neos-ui/UI/FlashMessages/REMOVE' + ADD = '@neos/neos-ui/UI/FlashMessages/ADD' } /** @@ -39,59 +22,11 @@ const add = (id: string, message: string, severity: string, timeout: number = 0) timeout })); -/** - * Removes a flash message - * - * @param {String} id The flashMessage id to delete. - */ -const remove = (id: string) => createAction(actionTypes.REMOVE, ({ - id -})); - // // Export the actions // export const actions = { - add, - remove + add }; export type Action = ActionType; - -// -// Export the reducer -// -export const reducer = (state: State = defaultState, action: InitAction | Action) => produce(state, draft => { - switch (action.type) { - case actionTypes.ADD: { - const message = action.payload; - const allowedSeverities = ['success', 'error', 'info']; - const {id, severity} = message; - const messageContents = message.message; - - if (!id || id.length < 0) { - throw new Error('Empty or non existent "id" passed to the addFlashMessage reducer. Please specify a string containing a random id.'); - } - - if (!messageContents || messageContents.length < 0) { - throw new Error('Empty or non existent "message" passed to the addFlashMessage reducer. Please specify a string containing your desired message.'); - } - - if (!severity || allowedSeverities.indexOf(severity.toLowerCase()) < 0) { - throw new Error(`Invalid "severity" specified while adding a new FlashMessage. Allowed severities are ${allowedSeverities.join(' ')}.`); - } - message.severity = message.severity.toLowerCase(); - draft[message.id] = message; - break; - } - case actionTypes.REMOVE: { - delete draft[action.payload.id]; - break; - } - } -}); - -// -// Export the selectors -// -export const selectors = {}; diff --git a/packages/neos-ui-redux-store/src/UI/index.ts b/packages/neos-ui-redux-store/src/UI/index.ts index 507046bba8..9b868a36ee 100644 --- a/packages/neos-ui-redux-store/src/UI/index.ts +++ b/packages/neos-ui-redux-store/src/UI/index.ts @@ -24,7 +24,6 @@ import * as ContentTree from './ContentTree'; // Export the reducer state shape interface // export interface State { - flashMessages: FlashMessages.State; fullScreen: FullScreen.State; keyboardShortcutModal: KeyboardShortcutModal.State; leftSideBar: LeftSideBar.State; @@ -98,7 +97,6 @@ export const actions = { // Export the reducer // export const reducer = combineReducers({ - flashMessages: FlashMessages.reducer, fullScreen: FullScreen.reducer, keyboardShortcutModal: KeyboardShortcutModal.reducer, leftSideBar: LeftSideBar.reducer, @@ -122,7 +120,6 @@ export const reducer = combineReducers({ // Export the selectors // export const selectors = { - FlashMessages: FlashMessages.selectors, FullScreen: FullScreen.selectors, KeyboardShortcutModal: KeyboardShortcutModal.selectors, LeftSideBar: LeftSideBar.selectors, From 2caf38ac9800dc3d9f675a18af643e415af3b524 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Thu, 6 Jun 2024 21:51:25 +0200 Subject: [PATCH 11/11] TASK: Replace all calls to `action.UI.FlashMessages.add` with new `showFlashMessage` API --- .../src/UI/ContentCanvas/index.js | 8 ++++++- .../neos-ui-sagas/src/UI/ContentTree/index.js | 7 +++++- .../neos-ui-sagas/src/UI/Impersonate/index.js | 23 +++++++++++++++---- .../neos-ui-sagas/src/UI/PageTree/index.js | 13 +++++++++-- packages/neos-ui/src/index.js | 13 +++++++++-- packages/neos-ui/src/manifest.js | 11 +++++---- 6 files changed, 60 insertions(+), 15 deletions(-) diff --git a/packages/neos-ui-sagas/src/UI/ContentCanvas/index.js b/packages/neos-ui-sagas/src/UI/ContentCanvas/index.js index 6a24155440..09c62d9c95 100644 --- a/packages/neos-ui-sagas/src/UI/ContentCanvas/index.js +++ b/packages/neos-ui-sagas/src/UI/ContentCanvas/index.js @@ -4,6 +4,7 @@ import {takeLatest, put, select, take, race} from 'redux-saga/effects'; import {getGuestFrameDocument} from '@neos-project/neos-ui-guest-frame/src/dom'; import {actionTypes, actions} from '@neos-project/neos-ui-redux-store'; +import {showFlashMessage} from '@neos-project/neos-ui-error'; /** * Load newly created page into canvas @@ -80,7 +81,12 @@ export function * watchControlOverIFrame() { const nextAction = Object.keys(waitForNextAction).map(k => waitForNextAction[k])[0]; if (nextAction.type === actionTypes.UI.ContentCanvas.REQUEST_REGAIN_CONTROL) { - yield put(actions.UI.FlashMessages.add('iframe access', nextAction.payload.errorMessage, 'error', 5000)); + showFlashMessage({ + id: 'iframe access', + severity: 'error', + message: nextAction.payload.errorMessage, + timeout: 5000 + }); // // We need to delay, so that the iframe gets cleared before we load a new src diff --git a/packages/neos-ui-sagas/src/UI/ContentTree/index.js b/packages/neos-ui-sagas/src/UI/ContentTree/index.js index e597864db0..177e2d9bae 100644 --- a/packages/neos-ui-sagas/src/UI/ContentTree/index.js +++ b/packages/neos-ui-sagas/src/UI/ContentTree/index.js @@ -4,6 +4,7 @@ import {actionTypes, actions, selectors} from '@neos-project/neos-ui-redux-store import {isNodeCollapsed} from '@neos-project/neos-ui-redux-store/src/CR/Nodes/helpers'; import backend from '@neos-project/neos-ui-backend-connector'; +import {showFlashMessage} from '@neos-project/neos-ui-error'; export function * watchReloadTree({globalRegistry}) { const nodeTypesRegistry = globalRegistry.get('@neos-project/neos-ui-contentrepository'); @@ -122,7 +123,11 @@ export function * watchRequestChildrenForContextPath({globalRegistry}) { childNodes = yield query.neosUiFilteredChildren(nodeTypeFilter).get(); } catch (err) { yield put(actions.UI.ContentTree.invalidate(contextPath)); - yield put(actions.UI.FlashMessages.add('loadChildNodesError', err.message, 'error')); + showFlashMessage({ + id: 'loadChildNodesError', + severity: 'error', + message: err.message + }); } yield put(actions.UI.ContentTree.setAsLoaded(contextPath)); diff --git a/packages/neos-ui-sagas/src/UI/Impersonate/index.js b/packages/neos-ui-sagas/src/UI/Impersonate/index.js index a29646551a..354e85e9b2 100644 --- a/packages/neos-ui-sagas/src/UI/Impersonate/index.js +++ b/packages/neos-ui-sagas/src/UI/Impersonate/index.js @@ -1,7 +1,8 @@ -import {put, call, takeEvery} from 'redux-saga/effects'; +import {call, takeEvery} from 'redux-saga/effects'; -import {actionTypes, actions} from '@neos-project/neos-ui-redux-store'; +import {actionTypes} from '@neos-project/neos-ui-redux-store'; import backend from '@neos-project/neos-ui-backend-connector'; +import {showFlashMessage} from '@neos-project/neos-ui-error'; export function * impersonateRestore({globalRegistry, routes}) { const {impersonateRestore} = backend.get().endpoints; @@ -33,14 +34,26 @@ export function * impersonateRestore({globalRegistry, routes}) { ); if (status) { - yield put(actions.UI.FlashMessages.add('restoreUserImpersonateUser', restoreMessage, 'success')); + showFlashMessage({ + id: 'restoreUserImpersonateUser', + severity: 'success', + message: restoreMessage + }); } else { - yield put(actions.UI.FlashMessages.add('restoreUserImpersonateUser', errorMessage, 'error')); + showFlashMessage({ + id: 'restoreUserImpersonateUser', + severity: 'error', + message: errorMessage + }); } window.location.href = routes?.core?.modules?.defaultModule; } catch (error) { - yield put(actions.UI.FlashMessages.add('restoreUserImpersonateUser', errorMessage, 'error')); + showFlashMessage({ + id: 'restoreUserImpersonateUser', + severity: 'error', + message: errorMessage + }); } }); } diff --git a/packages/neos-ui-sagas/src/UI/PageTree/index.js b/packages/neos-ui-sagas/src/UI/PageTree/index.js index 8260df6bdb..d6733d5a20 100644 --- a/packages/neos-ui-sagas/src/UI/PageTree/index.js +++ b/packages/neos-ui-sagas/src/UI/PageTree/index.js @@ -2,6 +2,7 @@ import {takeLatest, takeEvery, put, select} from 'redux-saga/effects'; import {actionTypes, actions, selectors} from '@neos-project/neos-ui-redux-store'; import backend from '@neos-project/neos-ui-backend-connector'; +import {showFlashMessage} from '@neos-project/neos-ui-error'; import {isNodeCollapsed} from '@neos-project/neos-ui-redux-store/src/CR/Nodes/helpers'; @@ -42,7 +43,11 @@ export function * watchRequestChildrenForContextPath({configuration}) { childNodes = yield query.neosUiFilteredChildren(baseNodeType).getForTree(); } catch (err) { yield put(actions.UI.PageTree.invalidate(contextPath)); - yield put(actions.UI.FlashMessages.add('loadChildNodesError', err.message, 'error')); + showFlashMessage({ + id: 'loadChildNodesError', + severity: 'error', + message: err.message + }); } yield put(actions.UI.PageTree.setAsLoaded(contextPath)); @@ -183,7 +188,11 @@ export function * watchSearch({configuration}) { } catch (err) { console.error('Error while executing a tree search: ', err); yield put(actions.UI.PageTree.invalidate(contextPath)); - yield put(actions.UI.FlashMessages.add('searchError', 'There was an error searching in the node tree. Contact your administrator for fixing this issue.', 'error')); + showFlashMessage({ + id: 'searchError', + severity: 'error', + message: 'There was an error searching in the node tree. Contact your administrator for fixing this issue.' + }); return; } const siteNode = yield select(selectors.CR.Nodes.siteNodeSelector); diff --git a/packages/neos-ui/src/index.js b/packages/neos-ui/src/index.js index b54f1c2bf0..b545d10636 100644 --- a/packages/neos-ui/src/index.js +++ b/packages/neos-ui/src/index.js @@ -10,6 +10,7 @@ import fetchWithErrorHandling from '@neos-project/neos-ui-backend-connector/src/ import {SynchronousMetaRegistry} from '@neos-project/neos-ui-extensibility/src/registry'; import backend from '@neos-project/neos-ui-backend-connector'; import {handleActions} from '@neos-project/utils-redux'; +import {showFlashMessage} from '@neos-project/neos-ui-error'; import { appContainer, @@ -141,7 +142,11 @@ function initializeFetchWithErrorHandling() { message = exception.textContent; } - store.dispatch(actions.UI.FlashMessages.add('fetch error', message, 'error')); + showFlashMessage({ + id: 'fetch error', + severity: 'error', + message + }); }); } @@ -181,7 +186,11 @@ async function loadImpersonateStatus() { store.dispatch(actions.User.Impersonate.fetchStatus(impersonateState)); } } catch (error) { - store.dispatch(actions.UI.FlashMessages.add('impersonateStatusError', error.message, 'error')); + showFlashMessage({ + id: 'impersonateStatusError', + severity: 'error', + message: error.message + }); } } diff --git a/packages/neos-ui/src/manifest.js b/packages/neos-ui/src/manifest.js index 22cb23bb6e..c4d675a85b 100644 --- a/packages/neos-ui/src/manifest.js +++ b/packages/neos-ui/src/manifest.js @@ -21,6 +21,8 @@ import initializeContentDomNode from '@neos-project/neos-ui-guest-frame/src/init import style from '@neos-project/neos-ui-guest-frame/src/style.module.css'; import backend from '@neos-project/neos-ui-backend-connector'; +import {showFlashMessage} from '@neos-project/neos-ui-error'; + manifest('main', {}, (globalRegistry, {routes}) => { // // Create edit preview mode registry @@ -191,12 +193,13 @@ manifest('main', {}, (globalRegistry, {routes}) => { // // Take care of message feedback // - const flashMessageFeedbackHandler = (feedbackPayload, {store}) => { - const {message, severity} = feedbackPayload; - const timeout = severity.toLowerCase() === 'success' ? 5000 : 0; + const flashMessageFeedbackHandler = (feedbackPayload) => { + const {message} = feedbackPayload; + const severity = feedbackPayload.severity.toLowerCase(); + const timeout = severity === 'success' ? 5000 : 0; const id = uuid.v4(); - store.dispatch(actions.UI.FlashMessages.add(id, message, severity, timeout)); + showFlashMessage({id, message, severity, timeout}); }; serverFeedbackHandlers.set('Neos.Neos.Ui:Success/Main', flashMessageFeedbackHandler); serverFeedbackHandlers.set('Neos.Neos.Ui:Error/Main', flashMessageFeedbackHandler);