Skip to content

Commit

Permalink
Merge pull request #30 from Sebobo/feature/favourites-and-recents
Browse files Browse the repository at this point in the history
FEATURE: Mark commands as favourite and store recently used commands
  • Loading branch information
Sebobo authored Mar 2, 2023
2 parents 626640b + 6dbd6bd commit e8f35b3
Show file tree
Hide file tree
Showing 44 changed files with 1,135 additions and 408 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
node_modules
*.d.ts
!global.d.ts
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ node_modules

# Project specific ignores
packages/*/dist
/parcel-bundle-reports
*.d.ts
!.global.d.ts
9 changes: 0 additions & 9 deletions .testcaferc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const { waitForReact } = require('testcafe-react-selectors');

const BASE_URL = 'http://localhost:1234';

module.exports = {
Expand All @@ -18,11 +16,4 @@ module.exports = {
'retryTestPages': true,
'pageLoadTimeout': 10000,
'pageRequestTimeout': 60000,
hooks: {
test: {
before: async () => {
await waitForReact();
}
}
}
};
117 changes: 61 additions & 56 deletions Classes/Controller/PreferencesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,91 +12,96 @@
* source code.
*/

use Doctrine\ORM\EntityManagerInterface;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\Flow\Mvc\View\JsonView;
use Neos\Neos\Domain\Model\UserPreferences;
use Neos\Neos\Service\UserService;

class PreferencesController extends ActionController
{
protected const FAVOURITES_PREFERENCE = 'commandBar.favourites';
protected const RECENT_PREFERENCE = 'commandBar.recent';
protected const MAX_RECENT_ITEMS = 5;
protected const RECENT_COMMANDS_PREFERENCE = 'commandBar.recentCommands';
protected const RECENT_DOCUMENTS_PREFERENCE = 'commandBar.recentDocuments';
protected $defaultViewObjectName = JsonView::class;
protected $supportedMediaTypes = ['application/json'];

/**
* @var UserService
*/
#[Flow\Inject]
protected $userService;

/**
* This action adds the given command to the favourites list stored in the users preferences.
*/
public function addFavouriteAction(string $commandId): void
public function __construct(protected UserService $userService, protected EntityManagerInterface $entityManager)
{
$user = $this->userService->getBackendUser();

if (!$user) {
$this->view->assign('success', false);
return;
}

$preferences = $user->getPreferences();
$favourites = $preferences->get(self::FAVOURITES_PREFERENCE) ?? [];
$favourites[] = $commandId;
}

$this->view->assignMultiple([
'success' => true,
'favourites' => $favourites,
public function getPreferencesAction(): void
{
$preferences = $this->getUserPreferences();
$this->view->assign('value', [
'favouriteCommands' => $preferences->get(self::FAVOURITES_PREFERENCE) ?? [],
'recentCommands' => $preferences->get(self::RECENT_COMMANDS_PREFERENCE) ?? [],
'recentDocuments' => $preferences->get(self::RECENT_DOCUMENTS_PREFERENCE)?? [],
'showBranding' => $this->settings['features']['showBranding'],
]);
}

/**
* This action removes the given command from the favourites list stored in the users preferences.
* Updates the list of favourite commands in the user preferences
*
* @Flow\SkipCsrfProtection
*/
public function removeFavouriteAction(string $commandId): void
public function setFavouritesAction(array $commandIds): void
{
$user = $this->userService->getBackendUser();
$preferences = $this->getUserPreferences();
$preferences->set(self::FAVOURITES_PREFERENCE, $commandIds);
$this->entityManager->persist($preferences);
$this->view->assign('value', $commandIds);
}

if (!$user) {
$this->view->assign('success', false);
return;
/**
* Updates the list of recently used commands in the user preferences
*
* @Flow\SkipCsrfProtection
*/
public function addRecentCommandAction(string $commandId): void
{
$preferences = $this->getUserPreferences();
$recentCommands = $preferences->get(self::RECENT_COMMANDS_PREFERENCE);
if ($recentCommands === null) {
$recentCommands = [];
}

$preferences = $user->getPreferences();
$favourites = $preferences->get(self::FAVOURITES_PREFERENCE) ?? [];
$favourites = array_diff($favourites, [$commandId]);

$this->view->assignMultiple([
'success' => true,
'favourites' => $favourites,
]);
// Remove the command from the list if it is already in there (to move it to the top)
$recentCommands = array_filter($recentCommands, static fn($id) => $id !== $commandId);
// Add the command to the top of the list
array_unshift($recentCommands, $commandId);
// Limit the list to 5 items
$recentCommands = array_slice($recentCommands, 0, 5);

// Save the list
$preferences->set(self::RECENT_COMMANDS_PREFERENCE, $recentCommands);
$this->entityManager->persist($preferences);
$this->view->assign('value', $recentCommands);
}

/**
* This action prepends the given command to the recent commands list stored
* in the users preferences. If the command is already in the list, it is moved to the top.
* The list is limited to a maximum of MAX_RECENT_ITEMS items.
* Updates the list of recently used documents in the user preferences
*
* @Flow\SkipCsrfProtection
* @param string[] $nodeContextPaths a list of context paths to uniquely define nodes
*/
public function addRecentAction(string $commandId): void
public function setRecentDocumentsAction(array $nodeContextPaths): void
{
$user = $this->userService->getBackendUser();
$preferences = $this->getUserPreferences();
$preferences->set(self::RECENT_DOCUMENTS_PREFERENCE, $nodeContextPaths);
$this->entityManager->persist($preferences);
$this->view->assign('value', $nodeContextPaths);
}

protected function getUserPreferences(): UserPreferences
{
$user = $this->userService->getBackendUser();
if (!$user) {
$this->view->assign('success', false);
return;
throw new \RuntimeException('No user found', 1676812156);
}

$preferences = $user->getPreferences();
$recent = $preferences->get(self::RECENT_PREFERENCE) ?? [];
array_unshift($recent, $commandId);
$recent = array_slice(array_unique($recent), 0, self::MAX_RECENT_ITEMS);

$this->view->assignMultiple([
'success' => true,
'recent' => $recent,
]);
return $user->getPreferences();
}

}
7 changes: 7 additions & 0 deletions Configuration/Routes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- name: 'Shel.Neos.CommandBar:Preferences'
uriPattern: 'neos/shel-neos-commandbar/preferences/{@action}'
defaults:
'@package': 'Shel.Neos.CommandBar'
'@controller': 'Preferences'
'@format': 'json'
allowedMethods: ['GET', 'POST']
3 changes: 3 additions & 0 deletions Configuration/Settings.Features.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ Shel:
Neos:
CommandBar:
features:
# Set this to false if you don't want to see the "Made with love" message and icon
# Please keep in mind that this plugin is free and open source. If you like it, please consider supporting its development.
showBranding: true
searchNeosDocs:
enabled: false
endpoint: 'https://docs.neos.io/search'
Expand Down
16 changes: 16 additions & 0 deletions Configuration/Settings.Flow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Neos:
Flow:
mvc:
routes:
'Shel.Neos.CommandBar':
position: 'before Neos.Neos'

security:
authentication:
providers:
'Neos.Neos:Backend':
requestPatterns:
'Shel.Neos.CommandBar:Preferences':
pattern: ControllerObjectName
patternOptions:
controllerObjectNamePattern: 'Shel\Neos\CommandBar\Controller\.*'
60 changes: 58 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ This package provides a command bar plugin for Neos CMS.
* 📰 Publishing
* Publish / discard changes on current page
* Publish / discard all changes
* ⭐️Mark commands as favourites (stored in Neos user preferences)
* 🗄️Store recent commands (stored in Neos user preferences)
* 🪛 Extensibility
* Add new commands via the provided ´Shel.Neos.CommandBar` registry in your plugin manifests
* 🧩 Backend module integration
Expand Down Expand Up @@ -52,8 +54,48 @@ Then you can install the package via composer:
composer require shel/neos-commandbar:@dev
```

## Enabling the command bar in additional backend modules

By default, only the core Neos modules have the command bar enabled as a global inclusion will only be possible with Neos 8.3.
If you want to enable the command bar in a backend module, you can do so by adding the following setting:

```yaml
Neos:
Neos:
modules:
<MODULE_PATH>:
submodules:
<MODULE_NAME>:
additionalResources:
javaScripts:
Shel.Neos.CommandBar: 'resource://Shel.Neos.CommandBar/Public/Module.js'
styleSheets:
Shel.Neos.CommandBar: 'resource://Shel.Neos.CommandBar/Public/Module.css'
```
## Disable branding
If you supported the development of this package or you don't want to show the branding, you can disable it via the following setting:
```yaml
Shel:
Neos:
CommandBar:
features:
showBranding: false
```
## Development
⚠️ This package offers 2 plugins. One is the Neos.UI plugin built with Neos extensibility React based API and the other
is the backend module plugin built with ParcelJS and Preact.
We use yarn workspaces to manage the code for the 2 plugins, the dev server and the shared components.
Most of the code is shared and only a small wrapper is needed to make the components work in the UI and module environments.
Each plugin has its own setup and build process. The following sections describe how to set up and build each plugin.
### Setup
First install all dependencies:
```console
Expand All @@ -69,15 +111,29 @@ yarn dev
To develop the Neos plugin, you can run the following command to watch for changes and rebuild the plugin:

```console
yarn start
yarn watch
```

To build the plugin for production, run the following command:
Or watch them individually

```console
yarn watch-ui
yarn watch-module
```

To build both plugins for production, run the following command:

```console
yarn build
```

Or run the build for each plugin individually

```console
yarn build-ui
yarn build-module
```

## License

This package is licensed under the MIT license. See [license](LICENSE.txt) for details.
13 changes: 7 additions & 6 deletions Tests/e2e/page_model/Page.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { ReactSelector } from 'testcafe-react-selectors';
import { Selector } from 'testcafe';

class Page {
public dialog: Selector;
public searchBox: Selector;
public commandList: Selector;
public commands: Selector;

constructor() {
this.dialog = ReactSelector('CommandBarDialog');
selectByTestId = (id: string, selector = '*') => Selector(selector).withAttribute('data-testid', id);

this.searchBox = this.dialog.findReact('SearchBox');
this.commandList = this.dialog.findReact('CommandList');
this.commands = this.commandList.findReact('CommandListItem');
constructor() {
this.dialog = this.selectByTestId('CommandBarDialog', 'dialog');
this.searchBox = this.selectByTestId('SearchBox', 'input');
this.commandList = this.selectByTestId('CommandList', 'nav');
this.commands = this.selectByTestId('CommandListItem', 'li');
}
}

Expand Down
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@neos-commandbar/ui-plugin": "workspace:*"
},
"devDependencies": {
"@testing-library/preact": "^3.2.3",
"@types/react": "^16.14.35",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
Expand All @@ -26,7 +27,6 @@
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^2.8.4",
"testcafe": "^2.3.1",
"testcafe-react-selectors": "^5.0.2",
"typescript": "^4.9.5",
"typescript-plugin-css-modules": "^4.1.1"
},
Expand All @@ -40,7 +40,8 @@
"dev": "yarn workspace @neos-commandbar/dev-server start",
"test": "testcafe",
"test:github-actions": "xvfb-run --server-args=\"-screen 0 1280x720x24\" yarn testcafe chrome",
"lint": "eslint 'packages/*/src/**/*.{js,jsx,ts,tsx}'"
"lint": "eslint 'packages/*/src/**/*.{js,jsx,ts,tsx}'",
"analyze-module": "yarn build-module --reporter @parcel/reporter-bundle-analyzer"
},
"prettier": {
"printWidth": 120,
Expand All @@ -56,5 +57,12 @@
"flat": true,
"resolutions": {
"@neos-project/build-essentials@^8.2.6": "patch:@neos-project/build-essentials@npm%3A8.2.6#./.yarn/patches/@neos-project-build-essentials-npm-8.2.6-787ecaf75f.patch"
},
"alias": {
"react": "preact/compat",
"react-dom/test-utils": "preact/test-utils",
"react-dom": "preact/compat",
"react/jsx-runtime": "preact/jsx-runtime",
"preact/jsx-dev-runtime": "preact/jsx-runtime"
}
}
7 changes: 5 additions & 2 deletions packages/commandbar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
"name": "@neos-commandbar/commandbar",
"version": "1.0.0",
"dependencies": {
"fuzzy-search": "^3.2.1",
"react": "^16.14.0"
"fuzzy-search": "^3.2.1"
},
"optionalDependencies": {
"preact": "*",
"react": "*"
},
"main": "src/index.ts",
"scripts": {
Expand Down
Loading

0 comments on commit e8f35b3

Please sign in to comment.