From 5caa604dd5a2f388b1cb89239c01d3ed159e1f87 Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Wed, 18 Sep 2024 11:09:07 +0200 Subject: [PATCH] TASK: Use command registry as a unified way to invocate commands This simplfies the ReplWrapper and CommandsProvider --- .../Command/EvaluateEelExpressionCommand.php | 2 + .../JavaScript/Terminal/src/Terminal.tsx | 22 +-- .../Terminal/src/components/ReplWrapper.tsx | 44 ++---- .../Terminal/src/helpers/doInvokeCommand.ts | 4 +- .../Terminal/src/helpers/fetchCommands.ts | 2 +- .../src/provider/CommandsProvider.tsx | 75 +++------- .../src/registry/TerminalCommandRegistry.tsx | 137 ++++++++++-------- .../Terminal/src/typings/global.d.ts | 15 +- 8 files changed, 145 insertions(+), 156 deletions(-) diff --git a/Classes/Command/EvaluateEelExpressionCommand.php b/Classes/Command/EvaluateEelExpressionCommand.php index 283e02a..ec14b9e 100644 --- a/Classes/Command/EvaluateEelExpressionCommand.php +++ b/Classes/Command/EvaluateEelExpressionCommand.php @@ -78,6 +78,8 @@ public function invokeCommand(string $argument, CommandContext $commandContext): $result = $e->getMessage(); } + // TODO: Convert NodeInterfaces to a variant of NodeResults with all properties and technical details + return new CommandInvocationResult($success, $result); } } diff --git a/Resources/Private/JavaScript/Terminal/src/Terminal.tsx b/Resources/Private/JavaScript/Terminal/src/Terminal.tsx index e5d9ec3..92af5b9 100755 --- a/Resources/Private/JavaScript/Terminal/src/Terminal.tsx +++ b/Resources/Private/JavaScript/Terminal/src/Terminal.tsx @@ -15,11 +15,13 @@ import { actions as terminalActions, selectors as terminalSelectors } from './ac interface TerminalProps { config: TerminalConfig; + user: TerminalUser; siteNode: Node; documentNode: Node; focusedNodes: string[]; i18nRegistry: I18nRegistry; terminalOpen: boolean; + toggleNeosTerminal: (visible?: boolean) => void; handleServerFeedback: (feedback: FeedbackEnvelope) => void; } @@ -37,18 +39,16 @@ class Terminal extends React.PureComponent { }; render() { - const { config } = this.props as TerminalProps; - return ( - 0 ? this.props.focusedNodes[0] : null} - i18nRegistry={this.props.i18nRegistry} - handleServerFeedback={this.props.handleServerFeedback} - config={config} - > - + + ); } diff --git a/Resources/Private/JavaScript/Terminal/src/components/ReplWrapper.tsx b/Resources/Private/JavaScript/Terminal/src/components/ReplWrapper.tsx index dd4cb87..87a3716 100644 --- a/Resources/Private/JavaScript/Terminal/src/components/ReplWrapper.tsx +++ b/Resources/Private/JavaScript/Terminal/src/components/ReplWrapper.tsx @@ -20,14 +20,9 @@ interface ReplProps { welcomeMessage?: string; registrationKey?: RegistrationKey; }; - user: { - firstName: string; - lastName: string; - fullName: string; - }; + user: TerminalUser; siteNode: Node; documentNode: Node; - className?: string; theme?: Record; terminalOpen?: boolean; toggleNeosTerminal: (visible?: boolean) => void; @@ -56,35 +51,26 @@ const ReplWrapper: React.FC = ({ const command = commands[commandName]; // Register command globally - window.NeosTerminal[commandName] = (...args) => invokeCommand(commandName, args); + window.NeosTerminal[commandName] = (...args: any[]) => invokeCommand(commandName, args); carry[commandName] = { ...command, description: translate(command.description ?? ''), - fn: (...args) => { + fn: async (...args: any[]) => { const currentTerminal = terminal.current; - invokeCommand(commandName, args) - .then((result) => { - currentTerminal.state.stdout.pop(); - let output = result; - if (!result) { - output = translate('command.empty'); - } - currentTerminal.pushToStdout(output); - }) - .catch((error) => { - console.error( - error, - translate( - 'command.invocationError', - 'An error occurred during invocation of the "{commandName}" command', - { commandName } - ) - ); + currentTerminal.pushToStdout(translate('command.evaluating')); + let evaluatingMessageRemoved = false; + for await (const result of invokeCommand(commandName, args)) { + if (!evaluatingMessageRemoved) { currentTerminal.state.stdout.pop(); - currentTerminal.pushToStdout(translate('command.error')); - }); - return translate('command.evaluating'); + evaluatingMessageRemoved = true; + } + let output = result; + if (!result) { + output = translate('command.empty'); + } + currentTerminal.pushToStdout(output); + } }, }; return carry; diff --git a/Resources/Private/JavaScript/Terminal/src/helpers/doInvokeCommand.ts b/Resources/Private/JavaScript/Terminal/src/helpers/doInvokeCommand.ts index 8d47350..94dd4a7 100644 --- a/Resources/Private/JavaScript/Terminal/src/helpers/doInvokeCommand.ts +++ b/Resources/Private/JavaScript/Terminal/src/helpers/doInvokeCommand.ts @@ -11,7 +11,7 @@ interface CommandInvocationResult { const doInvokeCommand = async ( endPoint: string, commandName: string, - args: string[], + argument: string, siteNode: string = null, focusedNode: string = null, documentNode: string = null @@ -27,7 +27,7 @@ const doInvokeCommand = async ( }, body: JSON.stringify({ commandName, - argument: args.join(' '), + argument, siteNode, focusedNode, documentNode, diff --git a/Resources/Private/JavaScript/Terminal/src/helpers/fetchCommands.ts b/Resources/Private/JavaScript/Terminal/src/helpers/fetchCommands.ts index e862c62..5f63dd1 100644 --- a/Resources/Private/JavaScript/Terminal/src/helpers/fetchCommands.ts +++ b/Resources/Private/JavaScript/Terminal/src/helpers/fetchCommands.ts @@ -29,7 +29,7 @@ const fetchCommands = async (endPoint: string): Promise<{ success: boolean; resu .then((data: CommandList) => { return data; }) - .catch((error: Error) => { + .catch(() => { return { success: false, result: {}, diff --git a/Resources/Private/JavaScript/Terminal/src/provider/CommandsProvider.tsx b/Resources/Private/JavaScript/Terminal/src/provider/CommandsProvider.tsx index d5ec251..6530393 100644 --- a/Resources/Private/JavaScript/Terminal/src/provider/CommandsProvider.tsx +++ b/Resources/Private/JavaScript/Terminal/src/provider/CommandsProvider.tsx @@ -1,24 +1,18 @@ -import * as React from 'react'; +import React from 'react'; import { createContext, useCallback, useContext, useEffect, useState } from 'react'; -import { FeedbackEnvelope, I18nRegistry, CommandList, NodeContextPath } from '../interfaces'; -import doInvokeCommand from '../helpers/doInvokeCommand'; +import { I18nRegistry, CommandList } from '../interfaces'; import logToConsole from '../helpers/logger'; import getTerminalCommandRegistry from '../registry/TerminalCommandRegistry'; interface CommandsContextProps { children: React.ReactElement; - config: TerminalConfig; - siteNode: NodeContextPath; - documentNode: NodeContextPath; - focusedNode?: NodeContextPath; i18nRegistry: I18nRegistry; - handleServerFeedback: (feedback: FeedbackEnvelope) => void; } interface CommandsContextValues { commands: CommandList; - invokeCommand: (endPoint: string, param: string[]) => Promise; + invokeCommand: (endPoint: string, param: string[]) => AsyncGenerator; translate: ( id: string, fallback?: string, @@ -31,15 +25,7 @@ interface CommandsContextValues { export const CommandsContext = createContext({} as CommandsContextValues); export const useCommands = (): CommandsContextValues => useContext(CommandsContext); -export const CommandsProvider = ({ - config, - children, - documentNode, - focusedNode, - siteNode, - i18nRegistry, - handleServerFeedback, -}: CommandsContextProps) => { +export const CommandsProvider = ({ children, i18nRegistry }: CommandsContextProps) => { const [commands, setCommands] = useState({}); useEffect(() => { @@ -60,7 +46,7 @@ export const CommandsProvider = ({ ); const invokeCommand = useCallback( - async (commandName: string, args: string[]): Promise => { + async function* (commandName: string, args: string[]): AsyncGenerator { const command = commands[commandName]; if (!command) @@ -68,43 +54,26 @@ export const CommandsProvider = ({ translate('command.doesNotExist', `The command {commandName} does not exist!`, { commandName }) ); - // TODO: Use TerminalCommandRegistry for invocation - needs some refactoring - const { success, result, uiFeedback } = await doInvokeCommand( - config.invokeCommandEndPoint, + for await (const { success, view, message, options } of getTerminalCommandRegistry().invokeCommand( commandName, - args, - siteNode, - focusedNode, - documentNode - ); - let parsedResult = result; - let textResult = result; - // Try to prettify json results - try { - parsedResult = JSON.parse(result); - if (typeof parsedResult !== 'string') { - textResult = JSON.stringify(parsedResult, null, 2); - } else { - textResult = parsedResult; - } - } catch (e) { - /* empty */ - } - logToConsole( - success ? 'log' : 'error', - translate('command.output', `"{commandName} {argument}":`, { - commandName, - argument: args.join(' '), - }), - parsedResult - ); - // Forward server feedback to the Neos UI - if (uiFeedback) { - handleServerFeedback(uiFeedback); + args.join(' ') + )) { + logToConsole( + success ? 'log' : 'error', + translate('command.output', `"{commandName} {argument}":`, { + commandName, + argument: args.join(' '), + }), + message, + view, + options + ); + yield view; } - return textResult; + + return; }, - [commands, siteNode, documentNode, focusedNode] + [commands] ); return ( diff --git a/Resources/Private/JavaScript/Terminal/src/registry/TerminalCommandRegistry.tsx b/Resources/Private/JavaScript/Terminal/src/registry/TerminalCommandRegistry.tsx index 3b6d028..dbd5f39 100644 --- a/Resources/Private/JavaScript/Terminal/src/registry/TerminalCommandRegistry.tsx +++ b/Resources/Private/JavaScript/Terminal/src/registry/TerminalCommandRegistry.tsx @@ -61,29 +61,32 @@ class TerminalCommandRegistry { const invokeCommand = this.invokeCommand; return Object.keys(commands).length > 0 ? { - 'shel.neos.terminal': { - name: 'Terminal', - description: 'Execute terminal commands', - icon: 'terminal', - subCommands: Object.values(commands).reduce((acc, { name, description }) => { - acc[name] = { - name, - icon: 'terminal', - description: this.translate(description), - action: async function* (arg) { - yield* invokeCommand(name, arg); - }, - canHandleQueries: true, - executeManually: true - }; - return acc; - }, {}) - } - } + 'shel.neos.terminal': { + name: 'Terminal', + description: 'Execute terminal commands', + icon: 'terminal', + subCommands: Object.values(commands).reduce((acc, { name, description }) => { + acc[name] = { + name, + icon: 'terminal', + description: this.translate(description), + action: async function* (arg) { + yield* invokeCommand(name, arg); + }, + canHandleQueries: true, + executeManually: true, + }; + return acc; + }, {}), + }, + } : {}; }; - public invokeCommand = async function* (commandName: string, arg = '') { + public invokeCommand = async function* ( + commandName: string, + argument = '' + ): AsyncGenerator { const state = this.store.getState(); const siteNode = selectors.CR.Nodes.siteNodeSelector(state); const documentNode = selectors.CR.Nodes.documentNodeSelector(state); @@ -91,7 +94,7 @@ class TerminalCommandRegistry { const setActiveContentCanvasSrc = actions.UI.ContentCanvas.setSrc; const command = this.commands[commandName] as Command; - if (!arg) { + if (!argument) { yield { success: true, message: this.translate( @@ -104,19 +107,39 @@ class TerminalCommandRegistry {

{this.translate(command.description)}

{command.usage} - ) + ), }; } else { const response = await doInvokeCommand( this.config.invokeCommandEndPoint, commandName, - [arg], + argument, siteNode.contextPath, focusedNodes[0]?.contextPath, documentNode.contextPath - ); - - let { success, result, uiFeedback } = response; + ).catch((error) => { + console.error( + error, + this.translate( + 'command.invocationError', + `An error occurred during invocation of the command "${commandName}"`, + { commandName } + ) + ); + return { + success: false, + message: this.translate( + 'TerminalCommandRegistry.message.error', + `An error occurred during invocation of the command "${commandName}"`, + { commandName } + ), + result: error.message, + uiFeedback: null, + }; + }); + + const { success, result, uiFeedback } = response; + let view = result; if (uiFeedback) { this.store.dispatch(actions.ServerFeedback.handleServerFeedback(uiFeedback)); @@ -126,6 +149,11 @@ class TerminalCommandRegistry { try { const parsedResult = JSON.parse(result); if (typeof parsedResult !== 'string') { + view = ( +
+                            {JSON.stringify(parsedResult, null, 2)}
+                        
+ ); if (Array.isArray(parsedResult)) { const resultType = parsedResult[0].__typename ?? ''; if (resultType === 'NodeResult') { @@ -136,44 +164,35 @@ class TerminalCommandRegistry { `${parsedResult.length} results`, { matches: parsedResult.length } ), - options: (parsedResult as NodeResult[]).reduce((carry, { - identifier, - label, - nodeType, - breadcrumb, - uri, - icon, - score - }) => { - if (!uri) { - // Skip nodes without uri + view, + options: (parsedResult as NodeResult[]).reduce( + (carry, { identifier, label, nodeType, breadcrumb, uri, icon, score }) => { + if (!uri) { + // Skip nodes without uri + return carry; + } + + carry[identifier] = { + id: identifier, + name: label + (score ? ` ${score}` : ''), + description: breadcrumb, + category: nodeType, + action: async () => { + this.store.dispatch(setActiveContentCanvasSrc(uri)); + }, + closeOnExecute: true, + icon, + }; return carry; - } - - carry[identifier] = { - id: identifier, - name: label + (score ? ` ${score}` : ''), - description: breadcrumb, - category: nodeType, - action: async () => { - this.store.dispatch(setActiveContentCanvasSrc(uri)); - }, - closeOnExecute: true, - icon - }; - return carry; - }, {}) + }, + {} + ), }; return; } } - result = ( -
-                            {JSON.stringify(parsedResult, null, 2)}
-                        
- ); } else { - result =

{result.replace(/\\n/g, '\n')}

; + view =

{result.replace(/\\n/g, '\n')}

; } } catch (e) { // Treat result as simple string @@ -186,7 +205,7 @@ class TerminalCommandRegistry { `Result of command "${commandName}"`, { commandName } ), - view: result + view, }; } }; diff --git a/Resources/Private/JavaScript/Terminal/src/typings/global.d.ts b/Resources/Private/JavaScript/Terminal/src/typings/global.d.ts index cdf3198..99c685b 100644 --- a/Resources/Private/JavaScript/Terminal/src/typings/global.d.ts +++ b/Resources/Private/JavaScript/Terminal/src/typings/global.d.ts @@ -1,6 +1,6 @@ interface Window { NeosTerminal: { - [key: string]: (...args) => Promise; + [key: string]: (...args) => AsyncGenerator; }; } @@ -28,3 +28,16 @@ interface NodeResult { uri: string; score: string; } + +type CommandInvocationResult = { + success: boolean; + message: string; + view?: string | JSX.Element; + options?: object; +}; + +type TerminalUser = { + firstName: string; + lastName: string; + fullName: string; +};