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

View File

@ -805,7 +805,10 @@ export async function sendWithSettings(
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
/*
* TODO: Do this in a more robust way

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { send } from '../../network/network';
import type { Request } from '../../models/request';
import * as models from '../../models';
export function init(activeEnvironmentId: string): { network: Object } {
export function init(activeEnvironmentId: string | null): { network: Object } {
const network = {
async sendRequest(request: Request): Promise<Response> {
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 type { PluginTemplateTag } from '../templating/extensions/index';
import type { PluginTheme } from './misc';
import type { RequestGroup } from '../models/request-group';
import type { Request } from '../models/request';
export type Plugin = {
name: string,
@ -22,6 +24,19 @@ export type TemplateTag = {
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 = {
plugin: Plugin,
hook: Function,
@ -156,6 +171,16 @@ export async function getPlugins(force: boolean = false): Promise<Array<Plugin>>
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>> {
let extensions = [];
for (const plugin of await getPlugins()) {

View File

@ -1,6 +1,7 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import classnames from 'classnames';
import PromptButton from '../base/prompt-button';
import {
Dropdown,
@ -11,12 +12,43 @@ import {
} from '../base/dropdown';
import EnvironmentEditModal from '../modals/environment-edit-modal';
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 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
class RequestGroupActionsDropdown extends PureComponent {
_setDropdownRef(n) {
class RequestGroupActionsDropdown extends React.PureComponent<Props, State> {
_dropdown: ?Dropdown;
state = {
actionPlugins: [],
loadingActions: {},
};
_setDropdownRef(n: ?Dropdown) {
this._dropdown = n;
}
@ -56,19 +88,54 @@ class RequestGroupActionsDropdown extends PureComponent {
showModal(EnvironmentEditModal, this.props.requestGroup);
}
show() {
this._dropdown.show();
async onOpen() {
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() {
const {
workspace, // eslint-disable-line no-unused-vars
requestGroup, // eslint-disable-line no-unused-vars
hotKeyRegistry,
...other
} = this.props;
const { actionPlugins, loadingActions } = this.state;
return (
<Dropdown ref={this._setDropdownRef} {...other}>
<Dropdown ref={this._setDropdownRef} onOpen={this.onOpen} {...other}>
<DropdownButton>
<i className="fa fa-caret-down" />
</DropdownButton>
@ -96,21 +163,20 @@ class RequestGroupActionsDropdown extends PureComponent {
<DropdownItem buttonClass={PromptButton} addIcon onClick={this._handleDeleteFolder}>
<i className="fa fa-trash-o" /> Delete
</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>
);
}
}
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;

View File

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

View File

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

View File

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

View File

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