Skip to content

Commit

Permalink
Merge pull request #46 from Sebobo/feature/recent-documents
Browse files Browse the repository at this point in the history
FEATURE: Add recent documents command
  • Loading branch information
Sebobo committed Oct 11, 2023
2 parents 3c29518 + 457a539 commit aa37fb2
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 18 deletions.
70 changes: 63 additions & 7 deletions Classes/Controller/PreferencesController.php
Expand Up @@ -13,22 +13,32 @@
*/

use Doctrine\ORM\EntityManagerInterface;
use Neos\ContentRepository\Domain\Model\NodeInterface;
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\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';
protected $defaultViewObjectName = JsonView::class;
protected $supportedMediaTypes = ['application/json'];

public function __construct(protected UserService $userService, protected EntityManagerInterface $entityManager)
{
public function __construct(
protected UserService $userService,
protected EntityManagerInterface $entityManager,
protected NodeService $nodeService,
protected LinkingService $linkingService,
) {
}

public function getPreferencesAction(): void
Expand All @@ -37,7 +47,7 @@ public function getPreferencesAction(): void
$this->view->assign('value', [
'favouriteCommands' => $preferences->get(self::FAVOURITES_PREFERENCE) ?? [],
'recentCommands' => $preferences->get(self::RECENT_COMMANDS_PREFERENCE) ?? [],
'recentDocuments' => $preferences->get(self::RECENT_DOCUMENTS_PREFERENCE)?? [],
'recentDocuments' => $this->mapContextPathsToNodes($preferences->get(self::RECENT_DOCUMENTS_PREFERENCE) ?? []),
'showBranding' => $this->settings['features']['showBranding'],
]);
}
Expand Down Expand Up @@ -85,14 +95,29 @@ public function addRecentCommandAction(string $commandId): void
* 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
* @param string $nodeContextPath a context path to add to the recently visited documents
*/
public function setRecentDocumentsAction(array $nodeContextPaths): void
public function addRecentDocumentAction(string $nodeContextPath): void
{
$preferences = $this->getUserPreferences();
$preferences->set(self::RECENT_DOCUMENTS_PREFERENCE, $nodeContextPaths);

$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);
// 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);

// Save the list
$preferences->set(self::RECENT_DOCUMENTS_PREFERENCE, $recentDocuments);
$this->entityManager->persist($preferences);
$this->view->assign('value', $nodeContextPaths);
$this->view->assign('value', $this->mapContextPathsToNodes($recentDocuments));
}

protected function getUserPreferences(): UserPreferences
Expand All @@ -104,4 +129,35 @@ protected function getUserPreferences(): UserPreferences
return $user->getPreferences();
}

/**
* @var string[] $contextPaths
*/
protected function mapContextPathsToNodes(array $contextPaths): array
{
return array_reduce($contextPaths, function (array $carry, string $contextPath) {
$node = $this->nodeService->getNodeFromContextPath($contextPath);
if ($node instanceof NodeInterface) {
$uri = $this->getNodeUri($node);
if ($uri) {
$carry[]= [
'name' => $node->getLabel(),
'icon' => $node->getNodeType()->getConfiguration('ui.icon') ?? 'question',
'uri' => $this->getNodeUri($node),
'contextPath' => $contextPath,
];
}
}
return $carry;
}, []);
}

protected function getNodeUri(NodeInterface $node): string
{
try {
return $this->linkingService->createNodeUri($this->controllerContext, $node, null, 'html', true);
} catch (\Exception $e) {
return '';
}
}

}
2 changes: 2 additions & 0 deletions packages/commandbar/src/components/SearchBox/SearchBox.tsx
Expand Up @@ -16,6 +16,7 @@ const SearchBox: React.FC = () => {
const { executeCommand } = useCommandExecutor();
const { translate } = useIntl();
const inputRef = useRef<HTMLInputElement>();
const activeCommand = state.commands.value[state.activeCommandId.value || state.resultCommandId.value];

const handleChange = useCallback((e) => {
if (state.status.value === STATUS.DISPLAYING_RESULT) {
Expand Down Expand Up @@ -67,6 +68,7 @@ const SearchBox: React.FC = () => {
? translate('SearchBox.commandQuery.placeholder', 'Enter the query for the command')
: translate('SearchBox.placeholder', 'What do you want to do today?')
}
disabled={state.status.value === STATUS.DISPLAYING_RESULT && !activeCommand?.canHandleQueries}
autoFocus
onChange={handleChange}
onKeyUp={handleKeyPress}
Expand Down
7 changes: 6 additions & 1 deletion packages/commandbar/src/state/CommandBarExecutor.tsx
Expand Up @@ -38,7 +38,12 @@ export const CommandBarExecutor: React.FC<CommandInputContextProps> = ({ childre
// Cancel search, or selection, or close command bar
e.stopPropagation();
e.preventDefault();
if (state.selectedCommandGroup.value || state.searchWord.value || state.commandQuery.value) {
if (
state.selectedCommandGroup.value ||
state.searchWord.value ||
state.commandQuery.value ||
state.result.value
) {
actions.CANCEL();
} else {
// Close command bar if cancel is noop
Expand Down
6 changes: 3 additions & 3 deletions packages/commandbar/src/state/CommandBarStateProvider.tsx
Expand Up @@ -120,7 +120,7 @@ export const CommandBarStateProvider: React.FC<CommandBarContextProps> = ({
}, []);

// Provide all actions as shorthand functions
const actions: Record<TRANSITION, (...any) => void | Promise<void>> = useMemo(() => {
const actions: Record<TRANSITION, (...any) => void | Promise<void | any>> = useMemo(() => {
return {
[TRANSITION.RESET_SEARCH]: () => dispatch({ type: TRANSITION.RESET_SEARCH }),
[TRANSITION.HIGHLIGHT_NEXT_ITEM]: () => dispatch({ type: TRANSITION.HIGHLIGHT_NEXT_ITEM }),
Expand Down Expand Up @@ -148,13 +148,13 @@ export const CommandBarStateProvider: React.FC<CommandBarContextProps> = ({
[TRANSITION.EXPAND]: () => dispatch({ type: TRANSITION.EXPAND }),
[TRANSITION.ADD_FAVOURITE]: (commandId: CommandId) => {
dispatch({ type: TRANSITION.ADD_FAVOURITE, commandId });
userPreferences
return userPreferences
.setFavouriteCommands(state.favouriteCommands.value)
.catch((e) => logger.error('Could not update favourite commands', e));
},
[TRANSITION.REMOVE_FAVOURITE]: (commandId: CommandId) => {
dispatch({ type: TRANSITION.REMOVE_FAVOURITE, commandId });
userPreferences
return userPreferences
.setFavouriteCommands(state.favouriteCommands.value)
.catch((e) => logger.error('Could not update favourite commands', e));
},
Expand Down
14 changes: 11 additions & 3 deletions packages/commandbar/src/typings/global.d.ts
Expand Up @@ -126,14 +126,22 @@ type EditPreviewModes = Record<string, EditPreviewMode>;

type NodeContextPath = string;

interface RecentDocument {
name: string;
uri: string;
icon: string;
contextPath: NodeContextPath;
}

interface UserPreferences {
favouriteCommands: CommandId[];
recentCommands: CommandId[];
recentDocuments: NodeContextPath[];
recentDocuments: RecentDocument[];
showBranding: boolean;
}

interface UserPreferencesService extends UserPreferences {
setFavouriteCommands: (commandIds: CommandId[]) => Promise;
addRecentCommand: (commandId: CommandId) => Promise;
setFavouriteCommands: (commandIds: CommandId[]) => Promise<string[]>;
addRecentCommand: (commandId: CommandId) => Promise<string[]>;
addRecentDocument: (nodeContextPath: CommandId) => Promise<RecentDocument[]>;
}
6 changes: 6 additions & 0 deletions packages/dev-server/src/index.tsx
Expand Up @@ -49,13 +49,19 @@ if (module.hot) module.hot.accept();

let favourites: CommandId[] = [];
let recentCommands: CommandId[] = [];
let recentDocuments: CommandId[] = [];

const userPreferencesService: UserPreferencesService = {
favouriteCommands: [...favourites],
setFavouriteCommands: async (commandIds: CommandId[]) => void (favourites = [...commandIds]),
recentCommands: [...recentCommands],
addRecentCommand: async (commandId: CommandId) =>
void (recentCommands = [commandId, ...recentCommands.filter((id) => id !== commandId).slice(0, 4)]),
addRecentDocument: async (nodeContextPath: CommandId) =>
void (recentDocuments = [
nodeContextPath,
...recentDocuments.filter((id) => id !== nodeContextPath).slice(0, 4),
]),
recentDocuments: [],
showBranding: true,
};
Expand Down
3 changes: 2 additions & 1 deletion packages/module-plugin/src/App.tsx
Expand Up @@ -24,7 +24,7 @@ export default class App extends Component<
preferences: {
favouriteCommands: CommandId[];
recentCommands: CommandId[];
recentDocuments: NodeContextPath[];
recentDocuments: RecentDocument[];
showBranding: boolean;
};
}
Expand Down Expand Up @@ -201,6 +201,7 @@ export default class App extends Component<
...preferences,
setFavouriteCommands: PreferencesApi.setFavouriteCommands,
addRecentCommand: PreferencesApi.addRecentCommand,
addRecentDocument: PreferencesApi.addRecentDocument,
}}
translate={App.translate}
/>
Expand Down
10 changes: 8 additions & 2 deletions packages/neos-api/src/preferences.ts
Expand Up @@ -3,6 +3,7 @@ import { fetchData } from './fetch';
const ENDPOINT_GET_PREFERENCES = '/neos/shel-neos-commandbar/preferences/getpreferences';
const ENDPOINT_SET_FAVOURITE_COMMANDS = '/neos/shel-neos-commandbar/preferences/setfavourites';
const ENDPOINT_ADD_RECENT_COMMAND = '/neos/shel-neos-commandbar/preferences/addrecentcommand';
const ENDPOINT_ADD_RECENT_DOCUMENT = '/neos/shel-neos-commandbar/preferences/addrecentdocument';

async function setPreference<T = any>(endpoint: string, data: any): Promise<T> {
return fetchData<T>(endpoint, data, 'POST');
Expand All @@ -13,10 +14,15 @@ export async function getPreferences() {
}

export async function setFavouriteCommands(commandIds: CommandId[]) {
return setPreference<CommandId[]>(ENDPOINT_SET_FAVOURITE_COMMANDS, { commandIds: commandIds });
return setPreference<CommandId[]>(ENDPOINT_SET_FAVOURITE_COMMANDS, { commandIds });
}

export async function addRecentCommand(commandId: CommandId) {
// TODO: Check if sendBeacon is a better option here to reduce the impact on the user
return setPreference<CommandId[]>(ENDPOINT_ADD_RECENT_COMMAND, { commandId: commandId });
return setPreference<CommandId[]>(ENDPOINT_ADD_RECENT_COMMAND, { commandId });
}

export async function addRecentDocument(nodeContextPath: NodeContextPath) {
// TODO: Check if sendBeacon is a better option here to reduce the impact on the user
return setPreference<RecentDocument[]>(ENDPOINT_ADD_RECENT_DOCUMENT, { nodeContextPath });
}
54 changes: 53 additions & 1 deletion packages/ui-plugin/src/CommandBarUiPlugin.tsx
Expand Up @@ -16,6 +16,7 @@ import { actions as commandBarActions, NeosRootState, selectors as commandBarSel

import * as styles from './CommandBarUiPlugin.module.css';
import * as theme from '@neos-commandbar/commandbar/src/Theme.module.css';
import { addRecentDocument } from '@neos-commandbar/neos-api/src/preferences';

type CommandBarUiPluginProps = {
addNode: (
Expand All @@ -42,6 +43,7 @@ type CommandBarUiPluginProps = {
publishableNodesInDocument: CRNode[];
previewUrl: string | null;
setActiveContentCanvasSrc: (uri: string) => void;
setActiveContentCanvasContextPath: (contextPath: string) => void;
setEditPreviewMode: (mode: string) => void;
siteNode: CRNode;
toggleCommandBar: () => void;
Expand All @@ -52,7 +54,7 @@ type CommandBarUiPluginState = {
dragging: boolean;
favouriteCommands: CommandId[];
recentCommands: CommandId[];
recentDocuments: NodeContextPath[];
recentDocuments: RecentDocument[];
showBranding: boolean;
commands: HierarchicalCommandList;
};
Expand All @@ -79,6 +81,7 @@ class CommandBarUiPlugin extends React.PureComponent<CommandBarUiPluginProps, Co
publishableNodesInDocument: PropTypes.array,
previewUrl: PropTypes.string,
setActiveContentCanvasSrc: PropTypes.func.isRequired,
setActiveContentCanvasContextPath: PropTypes.func.isRequired,
setEditPreviewMode: PropTypes.func.isRequired,
siteNode: PropTypes.object,
toggleCommandBar: PropTypes.func.isRequired,
Expand Down Expand Up @@ -193,6 +196,15 @@ class CommandBarUiPlugin extends React.PureComponent<CommandBarUiPluginProps, Co
},
closeOnExecute: true,
},
recentDocument: {
name: this.translate('CommandBarUiPlugin.command.recentDocuments', 'Recent documents'),
description: this.translate(
'CommandBarUiPlugin.command.recentDocuments.description',
'Open a recently visited documents'
),
icon: 'history',
action: this.showRecentDocuments.bind(this),
},
},
};

Expand Down Expand Up @@ -289,6 +301,14 @@ class CommandBarUiPlugin extends React.PureComponent<CommandBarUiPluginProps, Co
});
}

async componentDidUpdate(prevProps: CommandBarUiPluginProps) {
if (prevProps.documentNode?.contextPath != this.props.documentNode?.contextPath) {
addRecentDocument(this.props.documentNode.contextPath).then((recentDocuments) => {
this.setState({ recentDocuments });
});
}
}

buildCommandsFromHotkeys = (): HierarchicalCommandList => {
const { hotkeyRegistry, handleHotkeyAction, config } = this.props;
const hotkeys: NeosHotKey[] = hotkeyRegistry.getAllAsList();
Expand Down Expand Up @@ -330,6 +350,36 @@ class CommandBarUiPlugin extends React.PureComponent<CommandBarUiPluginProps, Co
addNode(focusedNodeContextPath || documentNode.contextPath, undefined, 'after');
};

showRecentDocuments = async function* (this: CommandBarUiPlugin): CommandGeneratorResult {
const { recentDocuments } = this.state;
const { setActiveContentCanvasContextPath, setActiveContentCanvasSrc } = this.props;

if (!recentDocuments.length) {
yield {
success: false,
message: this.translate('CommandBarUiPlugin.command.searchDocuments.searchFailed', 'Search failed'),
};
} else {
yield {
success: true,
message: this.translate('CommandBarUiPlugin.command.recentDocuments.options', 'Recent documents'),
options: recentDocuments.reduce((carry, { name, contextPath, uri, icon }) => {
carry[contextPath] = {
id: contextPath,
name,
icon,
action: async () => {
setActiveContentCanvasSrc(uri);
setActiveContentCanvasContextPath(contextPath);
},
closeOnExecute: true,
};
return carry;
}, {} as FlatCommandList),
};
}
};

handleSearchNode = async function* (this: CommandBarUiPlugin, query: string): CommandGeneratorResult {
const { siteNode, setActiveContentCanvasSrc } = this.props as CommandBarUiPluginProps;
yield {
Expand Down Expand Up @@ -543,6 +593,7 @@ class CommandBarUiPlugin extends React.PureComponent<CommandBarUiPluginProps, Co
recentDocuments,
showBranding,
addRecentCommand: PreferencesApi.addRecentCommand,
addRecentDocument: PreferencesApi.addRecentDocument,
setFavouriteCommands: PreferencesApi.setFavouriteCommands,
}}
translate={this.translate}
Expand Down Expand Up @@ -587,4 +638,5 @@ export default connect(() => ({}), {
publishAction: actions.CR.Workspaces.publish,
discardAction: actions.CR.Workspaces.commenceDiscard,
setActiveContentCanvasSrc: actions.UI.ContentCanvas.setSrc,
setActiveContentCanvasContextPath: actions.CR.Nodes.setDocumentNode,
})(connect(mapStateToProps, mapDispatchToProps)(mapGlobalRegistryToProps(CommandBarUiPlugin)));

0 comments on commit aa37fb2

Please sign in to comment.