Add ability for plugins to add folder actions (#774)

This commit is contained in:
Gregory Schier 2019-05-04 16:34:52 -04:00
parent ec1310000d
commit d8066bc558
11 changed files with 144 additions and 29 deletions

View File

@ -192,7 +192,7 @@ export async function render<T>(
export async function getRenderContext( export async function getRenderContext(
request: Request, request: Request,
environmentId: string, environmentId: string | null,
ancestors: Array<BaseModel> | null = null, ancestors: Array<BaseModel> | null = null,
purpose: RenderPurpose | null = null, purpose: RenderPurpose | null = null,
): Promise<Object> { ): Promise<Object> {
@ -212,7 +212,7 @@ export async function getRenderContext(
const rootEnvironment = await models.environment.getOrCreateForWorkspaceId( const rootEnvironment = await models.environment.getOrCreateForWorkspaceId(
workspace ? workspace._id : 'n/a', workspace ? workspace._id : 'n/a',
); );
const subEnvironment = await models.environment.getById(environmentId); const subEnvironment = await models.environment.getById(environmentId || 'n/a');
let keySource = {}; let keySource = {};
for (let key in (rootEnvironment || {}).data) { for (let key in (rootEnvironment || {}).data) {
@ -261,7 +261,7 @@ export async function getRenderContext(
export async function getRenderedRequestAndContext( export async function getRenderedRequestAndContext(
request: Request, request: Request,
environmentId: string, environmentId: string | null,
purpose?: RenderPurpose, purpose?: RenderPurpose,
): Promise<{ request: RenderedRequest, context: Object }> { ): Promise<{ request: RenderedRequest, context: Object }> {
const ancestors = await db.withAncestors(request, [ const ancestors = await db.withAncestors(request, [

View File

@ -805,7 +805,10 @@ export async function sendWithSettings(
return _actuallySend(renderResult.request, renderResult.context, workspace, settings); return _actuallySend(renderResult.request, renderResult.context, workspace, settings);
} }
export async function send(requestId: string, environmentId: string): Promise<ResponsePatch> { export async function send(
requestId: string,
environmentId: string | null,
): Promise<ResponsePatch> {
// HACK: wait for all debounces to finish // HACK: wait for all debounces to finish
/* /*
* TODO: Do this in a more robust way * TODO: Do this in a more robust way

View File

@ -12,6 +12,7 @@ describe('init()', () => {
'alert', 'alert',
'getPath', 'getPath',
'prompt', 'prompt',
'showGenericModalDialog',
'showSaveDialog', 'showSaveDialog',
]); ]);
}); });

View File

@ -1,12 +1,13 @@
// @flow // @flow
import * as electron from 'electron'; import * as electron from 'electron';
import { showAlert, showPrompt } from '../../ui/components/modals/index'; import { showAlert, showModal, showPrompt } from '../../ui/components/modals';
import type { RenderPurpose } from '../../common/render'; import type { RenderPurpose } from '../../common/render';
import { import {
RENDER_PURPOSE_GENERAL, RENDER_PURPOSE_GENERAL,
RENDER_PURPOSE_NO_RENDER, RENDER_PURPOSE_NO_RENDER,
RENDER_PURPOSE_SEND, RENDER_PURPOSE_SEND,
} from '../../common/render'; } from '../../common/render';
import WrapperModal from '../../ui/components/modals/wrapper-modal';
export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): { app: Object } { export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): { app: Object } {
return { return {
@ -18,6 +19,9 @@ export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): { a
return showAlert({ title, message }); return showAlert({ title, message });
}, },
showGenericModalDialog(title: string, options?: { html: string } = {}): Promise<void> {
return showModal(WrapperModal, { title, bodyHTML: options.html });
},
prompt( prompt(
title: string, title: string,
options?: { options?: {

View File

@ -4,7 +4,7 @@ import { send } from '../../network/network';
import type { Request } from '../../models/request'; import type { Request } from '../../models/request';
import * as models from '../../models'; import * as models from '../../models';
export function init(activeEnvironmentId: string): { network: Object } { export function init(activeEnvironmentId: string | null): { network: Object } {
const network = { const network = {
async sendRequest(request: Request): Promise<Response> { async sendRequest(request: Request): Promise<Response> {
const responsePatch = await send(request._id, activeEnvironmentId); const responsePatch = await send(request._id, activeEnvironmentId);

View File

@ -8,6 +8,8 @@ import { resolveHomePath } from '../common/misc';
import { showError } from '../ui/components/modals/index'; import { showError } from '../ui/components/modals/index';
import type { PluginTemplateTag } from '../templating/extensions/index'; import type { PluginTemplateTag } from '../templating/extensions/index';
import type { PluginTheme } from './misc'; import type { PluginTheme } from './misc';
import type { RequestGroup } from '../models/request-group';
import type { Request } from '../models/request';
export type Plugin = { export type Plugin = {
name: string, name: string,
@ -22,6 +24,19 @@ export type TemplateTag = {
templateTag: PluginTemplateTag, templateTag: PluginTemplateTag,
}; };
export type RequestGroupAction = {
plugin: Plugin,
action: (
context: Object,
models: {
requestGroup: RequestGroup,
requests: Array<Request>,
},
) => void | Promise<void>,
label: string,
icon?: string,
};
export type RequestHook = { export type RequestHook = {
plugin: Plugin, plugin: Plugin,
hook: Function, hook: Function,
@ -156,6 +171,16 @@ export async function getPlugins(force: boolean = false): Promise<Array<Plugin>>
return plugins; return plugins;
} }
export async function getRequestGroupActions(): Promise<Array<RequestGroupAction>> {
let extensions = [];
for (const plugin of await getPlugins()) {
const actions = plugin.module.requestGroupActions || [];
extensions = [...extensions, ...actions.map(p => ({ plugin, ...p }))];
}
return extensions;
}
export async function getTemplateTags(): Promise<Array<TemplateTag>> { export async function getTemplateTags(): Promise<Array<TemplateTag>> {
let extensions = []; let extensions = [];
for (const plugin of await getPlugins()) { for (const plugin of await getPlugins()) {

View File

@ -1,6 +1,7 @@
import React, { PureComponent } from 'react'; // @flow
import PropTypes from 'prop-types'; import * as React from 'react';
import autobind from 'autobind-decorator'; import autobind from 'autobind-decorator';
import classnames from 'classnames';
import PromptButton from '../base/prompt-button'; import PromptButton from '../base/prompt-button';
import { import {
Dropdown, Dropdown,
@ -11,12 +12,43 @@ import {
} from '../base/dropdown'; } from '../base/dropdown';
import EnvironmentEditModal from '../modals/environment-edit-modal'; import EnvironmentEditModal from '../modals/environment-edit-modal';
import * as models from '../../../models'; import * as models from '../../../models';
import { showPrompt, showModal } from '../modals/index'; import { showError, showModal, showPrompt } from '../modals';
import type { HotKeyRegistry } from '../../../common/hotkeys';
import { hotKeyRefs } from '../../../common/hotkeys'; import { hotKeyRefs } from '../../../common/hotkeys';
import type { RequestGroupAction } from '../../../plugins';
import { getRequestGroupActions } from '../../../plugins';
import type { RequestGroup } from '../../../models/request-group';
import type { Workspace } from '../../../models/workspace';
import * as pluginContexts from '../../../plugins/context/index';
import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render';
import type { Environment } from '../../../models/environment';
type Props = {
workspace: Workspace,
requestGroup: RequestGroup,
hotKeyRegistry: HotKeyRegistry,
activeEnvironment: Environment | null,
handleCreateRequest: (id: string) => any,
handleDuplicateRequestGroup: (rg: RequestGroup) => any,
handleMoveRequestGroup: (rg: RequestGroup) => any,
handleCreateRequestGroup: (id: string) => any,
};
type State = {
actionPlugins: Array<RequestGroupAction>,
loadingActions: { [string]: boolean },
};
@autobind @autobind
class RequestGroupActionsDropdown extends PureComponent { class RequestGroupActionsDropdown extends React.PureComponent<Props, State> {
_setDropdownRef(n) { _dropdown: ?Dropdown;
state = {
actionPlugins: [],
loadingActions: {},
};
_setDropdownRef(n: ?Dropdown) {
this._dropdown = n; this._dropdown = n;
} }
@ -56,19 +88,54 @@ class RequestGroupActionsDropdown extends PureComponent {
showModal(EnvironmentEditModal, this.props.requestGroup); showModal(EnvironmentEditModal, this.props.requestGroup);
} }
show() { async onOpen() {
this._dropdown.show(); const plugins = await getRequestGroupActions();
this.setState({ actionPlugins: plugins });
}
async show() {
this._dropdown && this._dropdown.show();
}
async _handlePluginClick(p: RequestGroupAction) {
this.setState(state => ({ loadingActions: { ...state.loadingActions, [p.label]: true } }));
try {
const { activeEnvironment, requestGroup } = this.props;
const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null;
const context = {
...pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER),
...pluginContexts.store.init(p.plugin),
...pluginContexts.network.init(activeEnvironmentId),
};
const requests = await models.request.findByParentId(requestGroup._id);
requests.sort((a, b) => a.metaSortKey - b.metaSortKey);
await p.action(context, { requestGroup, requests });
} catch (err) {
showError({
title: 'Plugin Action Failed',
error: err,
});
}
this.setState(state => ({ loadingActions: { ...state.loadingActions, [p.label]: false } }));
this._dropdown && this._dropdown.hide();
} }
render() { render() {
const { const {
workspace, // eslint-disable-line no-unused-vars
requestGroup, // eslint-disable-line no-unused-vars requestGroup, // eslint-disable-line no-unused-vars
hotKeyRegistry, hotKeyRegistry,
...other ...other
} = this.props; } = this.props;
const { actionPlugins, loadingActions } = this.state;
return ( return (
<Dropdown ref={this._setDropdownRef} {...other}> <Dropdown ref={this._setDropdownRef} onOpen={this.onOpen} {...other}>
<DropdownButton> <DropdownButton>
<i className="fa fa-caret-down" /> <i className="fa fa-caret-down" />
</DropdownButton> </DropdownButton>
@ -96,21 +163,20 @@ class RequestGroupActionsDropdown extends PureComponent {
<DropdownItem buttonClass={PromptButton} addIcon onClick={this._handleDeleteFolder}> <DropdownItem buttonClass={PromptButton} addIcon onClick={this._handleDeleteFolder}>
<i className="fa fa-trash-o" /> Delete <i className="fa fa-trash-o" /> Delete
</DropdownItem> </DropdownItem>
{actionPlugins.length > 0 && <DropdownDivider>Plugins</DropdownDivider>}
{actionPlugins.map((p: RequestGroupAction) => (
<DropdownItem key={p.label} onClick={() => this._handlePluginClick(p)} stayOpenAfterClick>
{loadingActions[p.label] ? (
<i className="fa fa-refresh fa-spin" />
) : (
<i className={classnames('fa', p.icon || 'fa-code')} />
)}
{p.label}
</DropdownItem>
))}
</Dropdown> </Dropdown>
); );
} }
} }
RequestGroupActionsDropdown.propTypes = {
workspace: PropTypes.object.isRequired,
hotKeyRegistry: PropTypes.object.isRequired,
handleCreateRequest: PropTypes.func.isRequired,
handleCreateRequestGroup: PropTypes.func.isRequired,
handleDuplicateRequestGroup: PropTypes.func.isRequired,
handleMoveRequestGroup: PropTypes.func.isRequired,
// Optional
requestGroup: PropTypes.object,
};
export default RequestGroupActionsDropdown; export default RequestGroupActionsDropdown;

View File

@ -9,6 +9,7 @@ type Props = {};
type State = { type State = {
title: string, title: string,
body: React.Node, body: React.Node,
bodyHTML: ?string,
tall: boolean, tall: boolean,
}; };
@ -22,6 +23,7 @@ class WrapperModal extends React.PureComponent<Props, State> {
this.state = { this.state = {
title: '', title: '',
body: null, body: null,
bodyHTML: null,
tall: false, tall: false,
}; };
} }
@ -31,10 +33,11 @@ class WrapperModal extends React.PureComponent<Props, State> {
} }
show(options: Object = {}) { show(options: Object = {}) {
const { title, body, tall } = options; const { title, body, bodyHTML, tall } = options;
this.setState({ this.setState({
title, title,
body, body,
bodyHTML,
tall: !!tall, tall: !!tall,
}); });
@ -42,12 +45,17 @@ class WrapperModal extends React.PureComponent<Props, State> {
} }
render() { render() {
const { title, body, tall } = this.state; const { title, body, bodyHTML, tall } = this.state;
let finalBody = body;
if (bodyHTML) {
finalBody = <div dangerouslySetInnerHTML={{ __html: bodyHTML }} className="tall wide pad" />;
}
return ( return (
<Modal ref={this._setModalRef} tall={tall}> <Modal ref={this._setModalRef} tall={tall}>
<ModalHeader>{title || 'Uh Oh!'}</ModalHeader> <ModalHeader>{title || 'Uh Oh!'}</ModalHeader>
<ModalBody>{body}</ModalBody> <ModalBody>{finalBody}</ModalBody>
</Modal> </Modal>
); );
} }

View File

@ -7,6 +7,7 @@ import type { RequestGroup } from '../../../models/request-group';
import type { Workspace } from '../../../models/workspace'; import type { Workspace } from '../../../models/workspace';
import type { Request } from '../../../models/request'; import type { Request } from '../../../models/request';
import type { HotKeyRegistry } from '../../../common/hotkeys'; import type { HotKeyRegistry } from '../../../common/hotkeys';
import type { Environment } from '../../../models/environment';
type Child = { type Child = {
doc: Request | RequestGroup, doc: Request | RequestGroup,
@ -31,6 +32,7 @@ type Props = {
workspace: Workspace, workspace: Workspace,
filter: string, filter: string,
hotKeyRegistry: HotKeyRegistry, hotKeyRegistry: HotKeyRegistry,
activeEnvironment: Environment | null,
// Optional // Optional
activeRequest?: Request, activeRequest?: Request,
@ -53,6 +55,7 @@ class SidebarChildren extends React.PureComponent<Props> {
activeRequest, activeRequest,
workspace, workspace,
hotKeyRegistry, hotKeyRegistry,
activeEnvironment,
} = this.props; } = this.props;
const activeRequestId = activeRequest ? activeRequest._id : 'n/a'; const activeRequestId = activeRequest ? activeRequest._id : 'n/a';
@ -119,6 +122,7 @@ class SidebarChildren extends React.PureComponent<Props> {
requestGroup={requestGroup} requestGroup={requestGroup}
hotKeyRegistry={hotKeyRegistry} hotKeyRegistry={hotKeyRegistry}
children={children} children={children}
activeEnvironment={activeEnvironment}
/> />
); );
}); });

View File

@ -64,6 +64,7 @@ class SidebarRequestGroupRow extends PureComponent {
isDraggingOver, isDraggingOver,
workspace, workspace,
hotKeyRegistry, hotKeyRegistry,
activeEnvironment,
} = this.props; } = this.props;
const { dragDirection } = this.state; const { dragDirection } = this.state;
@ -117,6 +118,7 @@ class SidebarRequestGroupRow extends PureComponent {
workspace={workspace} workspace={workspace}
requestGroup={requestGroup} requestGroup={requestGroup}
hotKeyRegistry={hotKeyRegistry} hotKeyRegistry={hotKeyRegistry}
activeEnvironment={activeEnvironment}
right right
/> />
</div> </div>
@ -176,6 +178,7 @@ SidebarRequestGroupRow.propTypes = {
// Optional // Optional
children: PropTypes.node, children: PropTypes.node,
activeEnvironment: PropTypes.object,
}; };
/** /**

View File

@ -129,6 +129,7 @@ class Sidebar extends PureComponent {
activeRequest={activeRequest} activeRequest={activeRequest}
filter={filter || ''} filter={filter || ''}
hotKeyRegistry={hotKeyRegistry} hotKeyRegistry={hotKeyRegistry}
activeEnvironment={activeEnvironment}
/> />
{enableSyncBeta && vcs && isLoggedIn() && ( {enableSyncBeta && vcs && isLoggedIn() && (