Skip to content

Commit

Permalink
FEATURE: Introduce support for Shel.Neos.CommandBar
Browse files Browse the repository at this point in the history
With this change all terminal commands also show up in the commandbar.
  • Loading branch information
Sebobo committed Mar 14, 2023
1 parent 4cc4ae6 commit e38f101
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 31 deletions.
1 change: 1 addition & 0 deletions Readme.md
Expand Up @@ -21,6 +21,7 @@ It uses the great [terminal component](https://github.com/linuswillner/react-con
* Open Terminal via `t+t` shortcut
* Limit commands to backend roles
* Create your own commands and provide them in your own packages
* Full support for the [Shel.Neos.CommandBar](https://github.com/Sebobo/Shel.Neos.CommandBar)

## How it looks

Expand Down
18 changes: 15 additions & 3 deletions Resources/Private/JavaScript/Terminal/src/manifest.js
@@ -1,13 +1,15 @@
import manifest from '@neos-project/neos-ui-extensibility';

import { reducer, actions } from './actions';
import Terminal from './Terminal';
import getTerminalCommandRegistry from './registry/TerminalCommandRegistry';

window['NeosTerminal'] = window.NeosTerminal || {};

manifest('Shel.Neos.Terminal:Terminal', {}, (globalRegistry, { frontendConfiguration }) => {
const { enabled } = frontendConfiguration['Shel.Neos.Terminal:Terminal'];
manifest('Shel.Neos.Terminal:Terminal', {}, (globalRegistry, { store, frontendConfiguration }) => {
const config = frontendConfiguration['Shel.Neos.Terminal:Terminal'];

if (!enabled) return;
if (!config.enabled) return;

globalRegistry.get('reducers').set('Shel.Neos.Terminal', { reducer });
globalRegistry.get('containers').set('PrimaryToolbar/Middle/Terminal', Terminal);
Expand All @@ -18,4 +20,14 @@ manifest('Shel.Neos.Terminal:Terminal', {}, (globalRegistry, { frontendConfigura
action: actions.toggleNeosTerminal,
});
}

// Register test plugin command
const commandBarRegistry = globalRegistry.get('Shel.Neos.CommandBar');
if (commandBarRegistry) {
commandBarRegistry.set('plugins/terminal', async () => {
const i18nRegistry = globalRegistry.get('i18n');
const terminalCommandRegistry = getTerminalCommandRegistry(config, i18nRegistry, store);
return terminalCommandRegistry.getCommandsForCommandBar();
});
}
});
Expand Up @@ -46,6 +46,7 @@ const logToConsole = (type = 'log', text: string, ...args) => {
console[type](`%c[Neos.Terminal]%c ${text}:`, finalStyle, ConsoleStyle.text.join(';'), ...args);
};

// TODO: Either provider or use TerminalCommandRegistry instead
export const CommandsProvider = ({
invokeCommandEndPoint,
getCommandsEndPoint,
Expand Down
@@ -0,0 +1,135 @@
import React from 'react';

// @ts-ignore
import { selectors } from '@neos-project/neos-ui-redux-store';

import fetchCommands from '../helpers/fetchCommands';
import { CommandList, I18nRegistry, NeosRootState } from '../interfaces';
import doInvokeCommand from '../helpers/doInvokeCommand';
import Command from '../interfaces/Command';

interface NeosStore {
getState: () => NeosRootState;
dispatch: () => void;
}

// noinspection JSPotentiallyInvalidUsageOfClassThis
class TerminalCommandRegistry {
constructor(readonly config: TerminalConfig, readonly i18nRegistry: I18nRegistry, readonly store: NeosStore) {
this.invokeCommand = this.invokeCommand.bind(this);
}

private commands: CommandList;

public getCommands = async () => {
if (this.commands) return this.commands;
return (this.commands = await fetchCommands(this.config.getCommandsEndPoint).then(({ result }) => result));
};

public translate = (
id: string,
fallback = '',
params: Record<string, unknown> | string[] = [],
packageKey = 'Shel.Neos.Terminal',
sourceName = 'Main'
): string => {
return this.i18nRegistry.translate(id, fallback, params, packageKey, sourceName);
};

public getCommandsForCommandBar = async () => {
const commands = await this.getCommands();
const invokeCommand = this.invokeCommand;
return {
'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 = '') {
const state = this.store.getState();
const siteNode = selectors.CR.Nodes.siteNodeSelector(state);
const documentNode = selectors.CR.Nodes.documentNodeSelector(state);
const focusedNodes = selectors.CR.Nodes.focusedNodePathsSelector(state);
const command = this.commands[commandName] as Command;

if (!arg) {
yield {
success: true,
message: this.translate(
'TerminalCommandRegistry.message.provideArguments',
`Please provide arguments for command "${commandName}"`,
{ commandName }
),
view: (
<div>
<p>{this.translate(command.description)}</p>
<code>{command.usage}</code>
</div>
),
};
} else {
const response = await doInvokeCommand(
this.config.invokeCommandEndPoint,
commandName,
[arg],
siteNode.contextPath,
focusedNodes[0]?.contextPath,
documentNode.contextPath
);

let result = response.result;

// Try to prettify json results
try {
const parsedResult = JSON.parse(result);
if (typeof parsedResult !== 'string') {
result = (
<pre>
<code>{JSON.stringify(parsedResult, null, 2)}</code>
</pre>
);
} else {
result = <p>{result.replace(/\\n/g, '\n')}</p>;
}
} catch (e) {
// Treat result as simple string
}

yield {
success: response.success,
message: this.translate(
'TerminalCommandRegistry.message.result',
`Result of command "${commandName}"`,
{ commandName }
),
view: result,
};
}
};
}

let singleton = null;

export default function getTerminalCommandRegistry(
config: TerminalConfig,
i18nRegistry: I18nRegistry,
store: NeosStore
): TerminalCommandRegistry {
return singleton ?? (singleton = new TerminalCommandRegistry(config, i18nRegistry, store));
}
2 changes: 1 addition & 1 deletion Resources/Public/Assets/Plugin.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Resources/Public/Assets/Plugin.js.map

Large diffs are not rendered by default.

55 changes: 29 additions & 26 deletions composer.json
@@ -1,30 +1,33 @@
{
"description": "Neos CMS Ui terminal for running Eel expressions and other commands",
"type": "neos-plugin",
"name": "shel/neos-terminal",
"license": "MIT",
"keywords": [
"flow",
"neoscms",
"terminal",
"console",
"eel"
],
"require": {
"php": ">=7.4",
"neos/neos": "^7.3 || ^8.0",
"neos/neos-ui": "^7.3 || ^8.0",
"symfony/console": "^4.2 || ^5.1"
},
"autoload": {
"psr-4": {
"Shel\\Neos\\Terminal\\": "Classes"
}
},
"extra": {
"neos": {
"package-key": "Shel.Neos.Terminal"
}
"description": "Neos CMS Ui terminal for running Eel expressions and other commands",
"type": "neos-plugin",
"name": "shel/neos-terminal",
"license": "MIT",
"keywords": [
"flow",
"neoscms",
"terminal",
"console",
"eel"
],
"require": {
"php": ">=7.4",
"neos/neos": "^7.3 || ^8.0",
"neos/neos-ui": "^7.3 || ^8.0",
"symfony/console": "^4.2 || ^5.1"
},
"suggest": {
"shel/neos-commandbar": "The terminal provides a plugin integration for the Neos command bar"
},
"autoload": {
"psr-4": {
"Shel\\Neos\\Terminal\\": "Classes"
}
},
"extra": {
"neos": {
"package-key": "Shel.Neos.Terminal"
}
}
}

0 comments on commit e38f101

Please sign in to comment.