Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command Palette #3491

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
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
6 changes: 6 additions & 0 deletions app/lib/menu/menu-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,12 @@ class MenuBuilder {

browserWindow.setFullScreen(!isFullScreen);
}
}, {
label: 'Show Command Palette',
accelerator: 'CommandOrControl+P',
click: function() {
app.emit('menu:action', 'show-command-palette');
}
});

return submenuTemplate;
Expand Down
147 changes: 144 additions & 3 deletions client/src/app/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
* except in compliance with the MIT License.
*/

import React, { PureComponent } from 'react';
import React, { PureComponent, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';

import debug from 'debug';

Expand Down Expand Up @@ -66,6 +67,7 @@ import css from './App.less';

import Notifications, { NOTIFICATION_TYPES } from './notifications';

import KeyboardInteractionTrap from '../shared/ui/modal/KeyboardInteractionTrap';

const log = debug('App');

Expand All @@ -92,7 +94,8 @@ const INITIAL_STATE = {
logEntries: [],
notifications: [],
currentModal: null,
endpoints: []
endpoints: [],
showCommandPalette: false
};


Expand Down Expand Up @@ -143,6 +146,22 @@ export class App extends PureComponent {
}

this.currentNotificationId = 0;

this.getGlobal('commands').registerCommand('tab.create.bpmn.plaform', () => this.triggerAction('create-bpmn-diagram'));
this.getGlobal('commands').registerCommand('tab.create.bpmn.cloud', () => this.triggerAction('create-cloud-bpmn-diagram'));
this.getGlobal('commands').registerCommand('tab.create.form.plaform', () => this.triggerAction('create-form'));
this.getGlobal('commands').registerCommand('tab.create.form.cloud', () => this.triggerAction('create-cloud-form'));
this.getGlobal('commands').registerCommand('tab.create.dmn', () => this.triggerAction('create-dmn-diagram'));
this.getGlobal('commands').registerCommand('tab.close', () => {
this.triggerAction('close-tab', { tabId: this.state.activeTab.id });
});

this.getGlobal('commandPalette').registerCommand('Create new Platform BPMN tab', 'tab.create.bpmn.plaform');
this.getGlobal('commandPalette').registerCommand('Create new Cloud BPMN tab' ,'tab.create.bpmn.cloud');
this.getGlobal('commandPalette').registerCommand('Create new Platform Form', 'tab.create.form.plaform');
this.getGlobal('commandPalette').registerCommand('Create new Cloud Form' ,'tab.create.form.cloud');
this.getGlobal('commandPalette').registerCommand('Create new DMN diagram' ,'tab.create.dmn');
this.getGlobal('commandPalette').registerCommand('Close tab' ,'tab.close');
}

createDiagram = async (type = 'bpmn') => {
Expand Down Expand Up @@ -1791,6 +1810,10 @@ export class App extends PureComponent {
return this.resizeTab();
}

if (action === 'show-command-palette') {
return this.showCommandPalette(true);
}

if (action === 'log') {
const {
action,
Expand Down Expand Up @@ -1824,6 +1847,12 @@ export class App extends PureComponent {
return tab.triggerAction(action, options);
}, this.handleError)

showCommandPalette = (show) => {
this.setState({
showCommandPalette: show
});
}

openExternalUrl(options) {
this.getGlobal('backend').send('external:open-url', options);
}
Expand Down Expand Up @@ -1924,7 +1953,8 @@ export class App extends PureComponent {
layout,
logEntries,
dirtyTabs,
unsavedTabs
unsavedTabs,
showCommandPalette
} = this.state;

const Tab = this.getTabComponent(activeTab);
Expand Down Expand Up @@ -2012,6 +2042,10 @@ export class App extends PureComponent {

<Notifications notifications={ this.state.notifications } />

{
showCommandPalette && <CommandPalette commandPalette={ this.getGlobal('commandPalette') } onClose={ () => this.showCommandPalette(false) } />
}

</DropZone>
);
}
Expand Down Expand Up @@ -2318,3 +2352,110 @@ function failSafe(fn, errorHandler) {
}
};
}

function CommandPalette(props) {
const {
commandPalette,
onClose
} = props;

const [searchTerm, setSearchTerm] = useState('');

const [selectedCommandIndex, setSelectedCommandIndex] = useState(0);

const [matchingCommands, setMatchingCommands] = useState(
commandPalette.getRegistered()
);

const ref = useRef();

useEffect(() => {
ref.current && ref.current.focus();
}, []);

const onSearchTermInput = (event) => {
const newSearchTerm = event.target.value;

setSearchTerm(newSearchTerm);

setMatchingCommands(
commandPalette.getRegistered(({ title }) => {
return (
!searchTerm.length ||
title.toLowerCase().indexOf(newSearchTerm.toLowerCase()) !== -1
);
})
);

setSelectedCommandIndex(0);
};

const onKeyDown = (event) => {
const { key } = event;

if (key === 'Enter') {
if (!matchingCommands.length) {
return;
}

const command = matchingCommands[selectedCommandIndex];

commandPalette.executeCommand(command.command);

onClose();
}

if (key === 'Escape') {
onClose();
}

if (key === 'ArrowUp') {
setSelectedCommandIndex(Math.max(selectedCommandIndex - 1, 0));
}

if (key === 'ArrowDown') {
setSelectedCommandIndex(
Math.min(selectedCommandIndex + 1, matchingCommands.length - 1)
);
}
};

return ReactDOM.createPortal(
<KeyboardInteractionTrap>
<div className="command-palette" onKeyDown={ onKeyDown }>
<div className="command-palette__input">
<input
placeholder="Start typing..."
ref={ ref }
type="text"
onInput={ onSearchTermInput }
spellCheck="false"
/>
</div>
<div className="command-palette__results">
{matchingCommands.length ? null : (
<div className="command-palette__result command-palette__result--selected">
No matching commands
</div>
)}
{matchingCommands.map((command, index) => {
return (
<div
key={ command.command }
className={
index === selectedCommandIndex
? 'command-palette__result command-palette__result--selected'
: 'command-palette__result'
}
onClick={ () => commandPalette.executeCommand(command.command) }
>
{command.title}
</div>
);
})}
</div>
</div>
</KeyboardInteractionTrap>,
document.body
);
}
48 changes: 48 additions & 0 deletions client/src/app/App.less
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,51 @@
* {
user-select: none;
}

.command-palette {
width: 300px;
position: absolute;
top: 100px;
left: 50%;
transform: translate(-50%);
padding: 10px;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, .16), 0 2px 16px 0 rgba(0, 0, 0, .16);
background-color: var(--overlay-background-color);
color: var(--overlay-font-color);
border-radius: 2px;
}

.command-palette__input input[type="text"] {
border-radius: 3px;
border: solid 1px var(--color-grey-225-10-75);
background-color: var(--input-background-color);
width: 100%;
height: 30px;
font-size: 13px;
padding-left: 8px;
color: var(--color-grey-225-10-15);
font-family: inherit;
font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: normal;
letter-spacing: normal;
}

.command-palette__result {
margin-top: 1px;
padding: 10px;
color: var(--color-grey-225-10-15);
font-family: inherit;
font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: normal;
letter-spacing: normal;
font-size: var(--text-size-base);
}

.command-palette__result--selected {
background: var(--color-blue-205-100-45);
color: var(--color-white);
}
49 changes: 49 additions & 0 deletions client/src/app/CommandPalette.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership.
*
* Camunda licenses this file to you under the MIT; you may not use this file
* except in compliance with the MIT License.
*/

export default class CommandPalette {
constructor(commands) {
this._commands = commands;

this._registeredCommands = {};
}

/**
* e.g. registerCommand('Create new tab', 'tab.create');
* e.g. registerCommand('Deploy', 'tab.deploy', (tab) => tab.type === 'bpmn');
*
* @param {String} title
* @param {String} command
* @param {Function} [enabled]
*/
registerCommand(title, command, enabled) {
if (this._registeredCommands[command]) {
throw new Error(`command <${command}> already registered`);
}

this._registeredCommands[command] = {
command,
enabled,
title
};
}

executeCommand(command) {
this._commands.executeCommand(command);
}

getRegistered(filter) {
if (!filter) {
return Object.values(this._registeredCommands);
}

return Object.values(this._registeredCommands).filter(filter);
}
}
34 changes: 34 additions & 0 deletions client/src/app/Commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership.
*
* Camunda licenses this file to you under the MIT; you may not use this file
* except in compliance with the MIT License.
*/

export default class Commands {
constructor() {
this._registeredCommands = {};
}

registerCommand(command, commandHandler) {
if (this._registeredCommands[command]) {
throw new Error(`command <${command}> already registered`);
}

this._registeredCommands[command] = commandHandler;
}

/**
* e.g. executeCommand('tab.create');
*/
executeCommand(command) {
if (!this._registeredCommands[command]) {
throw new Error(`command <${command}> not registered`);
}

this._registeredCommands[command]();
}
}
4 changes: 3 additions & 1 deletion client/src/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@
export { default as App } from './App';
export { default as AppParent } from './AppParent';
export { default as KeyboardBindings } from './KeyboardBindings';
export { default as TabsProvider } from './TabsProvider';
export { default as TabsProvider } from './TabsProvider';
export { default as Commands } from './Commands';
export { default as CommandPalette } from './CommandPalette';
Loading