Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 57 additions & 29 deletions Classes/Controller/PreferencesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@
*/

use Doctrine\ORM\EntityManagerInterface;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\ContentRepository\Core\Feature\Security\Exception\AccessDenied;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\Flow\Mvc\View\JsonView;
use Neos\Neos\Controller\CreateContentContextTrait;
use Neos\Neos\Domain\Model\UserPreferences;
use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface;
use Neos\Neos\Service\LinkingService;
use Neos\Neos\Service\UserService;
use Neos\Neos\Ui\ContentRepository\Service\NodeService;

class PreferencesController extends ActionController
{
use CreateContentContextTrait;

protected const FAVOURITES_PREFERENCE = 'commandBar.favourites';
protected const RECENT_COMMANDS_PREFERENCE = 'commandBar.recentCommands';
protected const RECENT_DOCUMENTS_PREFERENCE = 'commandBar.recentDocuments';
Expand All @@ -36,8 +36,9 @@ class PreferencesController extends ActionController
public function __construct(
protected UserService $userService,
protected EntityManagerInterface $entityManager,
protected NodeService $nodeService,
protected LinkingService $linkingService,
protected ContentRepositoryRegistry $contentRepositoryRegistry,
protected NodeLabelGeneratorInterface $nodeLabelGenerator,
) {
}

Expand All @@ -47,7 +48,9 @@ public function getPreferencesAction(): void
$this->view->assign('value', [
'favouriteCommands' => $preferences->get(self::FAVOURITES_PREFERENCE) ?? [],
'recentCommands' => $preferences->get(self::RECENT_COMMANDS_PREFERENCE) ?? [],
'recentDocuments' => $this->mapContextPathsToNodes($preferences->get(self::RECENT_DOCUMENTS_PREFERENCE) ?? []),
'recentDocuments' => $this->mapContextPathsToNodes(
$preferences->get(self::RECENT_DOCUMENTS_PREFERENCE) ?? []
),
'showBranding' => $this->settings['features']['showBranding'],
]);
}
Expand Down Expand Up @@ -94,23 +97,28 @@ public function addRecentCommandAction(string $commandId): void
/**
* Updates the list of recently used documents in the user preferences
*
* @Flow\SkipCsrfProtection
* @param string $nodeContextPath a context path to add to the recently visited documents
* @param string $nodeContextPath a node to add to the recently visited documents
*/
#[Flow\SkipCsrfProtection]
public function addRecentDocumentAction(string $nodeContextPath): void
{
$preferences = $this->getUserPreferences();

/** @var string[]|null $recentDocuments */
$recentDocuments = $preferences->get(self::RECENT_DOCUMENTS_PREFERENCE);
if ($recentDocuments === null) {
$recentDocuments = [];
}

// Remove the command from the list if it is already in there (to move it to the top)
$recentDocuments = array_filter($recentDocuments,
static fn($existingContextPath) => $existingContextPath !== $nodeContextPath);
$recentDocuments = array_filter(
$recentDocuments,
static fn($existingContextPath) => $existingContextPath !== $nodeContextPath
);

// Add the path to the top of the list
array_unshift($recentDocuments, $nodeContextPath);

// Limit the list to 5 items
$recentDocuments = array_slice($recentDocuments, 0, 5);

Expand All @@ -130,32 +138,52 @@ protected function getUserPreferences(): UserPreferences
}

/**
* @var string[] $contextPaths
* @param string[] $nodeContextPaths
*/
protected function mapContextPathsToNodes(array $contextPaths): array
{
return array_reduce($contextPaths, function (array $carry, string $contextPath) {
$node = $this->nodeService->getNodeFromContextPath($contextPath);
if ($node instanceof NodeInterface) {
protected function mapContextPathsToNodes(
array $nodeContextPaths,
): array {
return array_filter(
array_map(function (string $nodeContextPath) {
$nodeAddress = NodeAddress::fromJsonString($nodeContextPath);

$contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId);
try {
$subgraph = $contentRepository->getContentSubgraph(
$nodeAddress->workspaceName,
$nodeAddress->dimensionSpacePoint
);
} catch (AccessDenied) {
// If the user does not have access to the subgraph, we skip this node
return null;
}

$node = $subgraph->findNodeById($nodeAddress->aggregateId);
if (!$node) {
return null;
}

$uri = $this->getNodeUri($node);
if ($uri) {
$carry[]= [
'name' => $node->getLabel(),
'icon' => $node->getNodeType()->getConfiguration('ui.icon') ?? 'question',
'uri' => $this->getNodeUri($node),
'contextPath' => $contextPath,
];
if (!$uri) {
return null;
}
}
return $carry;
}, []);

$nodeType = $contentRepository->getNodeTypeManager()->getNodeType($node->nodeTypeName);
return [
'name' => $this->nodeLabelGenerator->getLabel($node),
'icon' => $nodeType?->getConfiguration('ui.icon') ?? 'question',
'uri' => $this->getNodeUri($node),
'contextPath' => $nodeContextPath,
];
}, $nodeContextPaths)
);
}

protected function getNodeUri(NodeInterface $node): string
protected function getNodeUri(Node $node): string
{
try {
return $this->linkingService->createNodeUri($this->controllerContext, $node, null, 'html', true);
} catch (\Exception $e) {
} catch (\Exception) {
return '';
}
}
Expand Down
14 changes: 7 additions & 7 deletions Classes/Domain/Dto/CommandDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@
use Neos\Flow\Annotations as Flow;

#[Flow\Proxy(false)]
class CommandDto implements \JsonSerializable
readonly class CommandDto implements \JsonSerializable
{

public function __construct(
public readonly string $id,
public readonly string $name,
public readonly string $description,
public readonly string $action,
public readonly string $icon,
public readonly string $category = '',
public string $id,
public string $name,
public string $description,
public string $action,
public string $icon,
public string $category = '',
) {
}

Expand Down
16 changes: 9 additions & 7 deletions Classes/Helper/TranslationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,22 @@
use Neos\Flow\I18n\EelHelper\TranslationParameterToken;

#[Flow\Proxy(false)]
class TranslationHelper
final class TranslationHelper
{

public static function translateByShortHandString(string $shortHandString): string
{
$shortHandStringParts = explode(':', $shortHandString);
if (count($shortHandStringParts) === 3) {
[$package, $source, $id] = $shortHandStringParts;
return (new TranslationParameterToken($id))
->package($package)
->source(str_replace('.', '/', $source))
->translate();
try {
return (new TranslationParameterToken($id))
->package($package)
->source(str_replace('.', '/', $source))
->translate();
} catch (\Exception) {
return $shortHandString; // Fallback to original string if translation fails
}
}

return $shortHandString;
}
}
114 changes: 74 additions & 40 deletions Classes/Service/DataSource/CommandsDataSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@
* source code.
*/

use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\Flow\Http\Exception;
use Neos\Flow\I18n\Translator;
use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException;
use Neos\Flow\Mvc\Routing\UriBuilder;
use Neos\Flow\Persistence\Exception\IllegalObjectTypeException;
use Neos\Neos\Controller\Backend\MenuHelper;
use Neos\Neos\Domain\Model\SiteNodeName;
use Neos\Neos\Service\BackendRedirectionService;
use Neos\Neos\Service\DataSource\AbstractDataSource;
use Shel\Neos\CommandBar\Domain\Dto\CommandDto;
Expand All @@ -34,57 +38,37 @@ public function __construct(
) {
}

public function getData(NodeInterface $node = null, array $arguments = []): array
{
/**
* @throws Exception
* @throws MissingActionNameException
* @throws IllegalObjectTypeException
*/
public function getData(
Node $node = null,
array $arguments = []
): array {
$this->uriBuilder->setRequest($this->controllerContext->getRequest()->getMainRequest());

$sitesForMenu = array_reduce($this->menuHelper->buildSiteList($this->controllerContext),
$sitesForMenu = array_reduce(
$this->menuHelper->buildSiteList($this->controllerContext),
/** @param array{name: string, nodeName: SiteNodeName, uri: string, active: bool} $site */
static function (array $carry, array $site) {
if (!$site['uri']) {
return $carry;
}
$carry[$site['nodeName']] = new CommandDto(
$carry[$site['nodeName']->value] = new CommandDto(
$site['name'],
$site['name'],
'',
$site['uri'],
'globe'
);
return $carry;
}, []);
},
[]
);

$modulesForMenu = array_reduce($this->menuHelper->buildModuleList($this->controllerContext),
function (array $carry, array $module) {
// Skip modules without submodules
if (!$module['submodules']) {
return $carry;
}
$carry[$module['group']] = [
'name' => TranslationHelper::translateByShortHandString($module['label']),
'description' => TranslationHelper::translateByShortHandString($module['description']),
'icon' => $module['icon'],
'subCommands' => array_reduce($module['submodules'],
function (array $carry, array $submodule) {
if ($submodule['hideInMenu']) {
return $carry;
}
$carry[$submodule['module']] = new CommandDto(
$submodule['modulePath'],
TranslationHelper::translateByShortHandString($submodule['label']),
TranslationHelper::translateByShortHandString($submodule['description']),
$this->uriBuilder->uriFor(
'index',
['module' => $submodule['modulePath']],
'Backend\Module',
'Neos.Neos'
),
$submodule['icon'],
);
return $carry;
}, []),
];
return $carry;
}, []);
$modulesForMenu = $this->getModuleCommands();

$commands = [
'preferred-start-module' => new CommandDto(
Expand All @@ -94,6 +78,7 @@ function (array $carry, array $submodule) {
$this->backendRedirectionService->getAfterLoginRedirectionUri($this->controllerContext),
'home'
),
// TODO: Introduce group DTO
'modules' => [
'name' => $this->translate('CommandDataSource.category.modules'),
'description' => $this->translate('CommandDataSource.category.modules.description'),
Expand All @@ -104,6 +89,7 @@ function (array $carry, array $submodule) {

// Only show site switch command if there is more than one site
if (count($sitesForMenu) > 1) {
// TODO: Introduce group DTO
$commands['sites'] = [
'name' => $this->translate('CommandDataSource.category.sites'),
'description' => $this->translate('CommandDataSource.category.sites.description'),
Expand All @@ -115,8 +101,56 @@ function (array $carry, array $submodule) {
return $commands;
}

protected function translate($id): string
protected function translate(string $id): string
{
try {
return $this->translator->translateById($id, [], null, null, 'Main', 'Shel.Neos.CommandBar') ?? $id;
} catch (\Exception) {
return $id;
}
}

protected function getModuleCommands(): mixed
{
return $this->translator->translateById($id, [], null, null, 'Main', 'Shel.Neos.CommandBar') ?? $id;
return array_reduce(
$this->menuHelper->buildModuleList($this->controllerContext),
function (array $carry, array $module) {
// Skip modules without submodules
if (!$module['submodules']) {
return $carry;
}

// TODO: Introduce group DTO
$carry[$module['group']] = [
'name' => TranslationHelper::translateByShortHandString($module['label']),
'description' => TranslationHelper::translateByShortHandString($module['description']),
'icon' => $module['icon'],
'subCommands' => array_reduce(
$module['submodules'],
function (array $carry, array $submodule) {
if ($submodule['hideInMenu']) {
return $carry;
}
$carry[$submodule['module']] = new CommandDto(
$submodule['modulePath'],
TranslationHelper::translateByShortHandString($submodule['label']),
TranslationHelper::translateByShortHandString($submodule['description']),
$this->uriBuilder->uriFor(
'index',
['module' => $submodule['modulePath']],
'Backend\Module',
'Neos.Neos'
),
$submodule['icon'],
);
return $carry;
},
[]
),
];
return $carry;
},
[]
);
}
}
Loading