insomnia/packages/insomnia-app/app/ui/components/sidebar/sidebar-request-row.tsx
2021-12-04 09:08:28 +13:00

349 lines
10 KiB
TypeScript

import { autoBindMethodsForReact } from 'class-autobind-decorator';
import classnames from 'classnames';
import { HotKeyRegistry } from 'insomnia-common';
import React, { FC, PureComponent } from 'react';
import { DragSource, DragSourceSpec, DropTarget, DropTargetSpec } from 'react-dnd';
import { useSelector } from 'react-redux';
import { AUTOBIND_CFG, CONTENT_TYPE_GRAPHQL } from '../../../common/constants';
import { getMethodOverrideHeader } from '../../../common/misc';
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
import * as requestOperations from '../../../models/helpers/request-operations';
import { Request } from '../../../models/request';
import { RequestGroup } from '../../../models/request-group';
import { useNunjucks } from '../../context/nunjucks/use-nunjucks';
import { selectActiveEnvironment, selectActiveProject } from '../../redux/selectors';
import { Editable } from '../base/editable';
import { Highlight } from '../base/highlight';
import { RequestActionsDropdown } from '../dropdowns/request-actions-dropdown';
import { GrpcSpinner } from '../grpc-spinner';
import { showModal } from '../modals/index';
import { RequestSettingsModal } from '../modals/request-settings-modal';
import { GrpcTag } from '../tags/grpc-tag';
import { MethodTag } from '../tags/method-tag';
import { DnDProps, DragObject, dropHandleCreator, hoverHandleCreator, sourceCollect, targetCollect } from './dnd';
type DerivedProps = ReturnType<typeof useDerivedProps>;
interface RawProps {
handleActivateRequest: Function;
handleSetRequestPinned: Function;
handleDuplicateRequest: Function;
handleGenerateCode: Function;
handleCopyAsCurl: Function;
requestCreate: Function;
filter: string;
isActive: boolean;
isPinned: boolean;
hotKeyRegistry: HotKeyRegistry;
requestGroup?: RequestGroup;
/** can be Request or GrpcRequest */
request?: Request | GrpcRequest;
disableDragAndDrop?: boolean;
}
type Props = RawProps & DnDProps & DerivedProps;
interface State {
dragDirection: number;
isEditing: boolean;
renderedUrl: string;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
class UnconnectedSidebarRequestRow extends PureComponent<Props, State> {
state: State = {
dragDirection: 0,
isEditing: false,
renderedUrl: '',
};
_urlUpdateInterval: NodeJS.Timeout | null = null;
_requestActionsDropdown: RequestActionsDropdown | null = null;
_setRequestActionsDropdownRef(n: RequestActionsDropdown) {
this._requestActionsDropdown = n;
}
_handleShowRequestActions(e) {
e.preventDefault();
this._requestActionsDropdown?.show();
}
_handleEditStart() {
this.setState({
isEditing: true,
});
}
async _handleRequestUpdateName(name) {
const { request } = this.props;
const patch = {
name,
};
// @ts-expect-error -- TSCONVERSION skip this if request is undefined
await requestOperations.update(request, patch);
this.setState({
isEditing: false,
});
}
_handleRequestCreateFromEmpty() {
const parentId = this.props.requestGroup?._id;
this.props.requestCreate(parentId);
}
_handleRequestActivate() {
const { isActive, request, handleActivateRequest } = this.props;
if (isActive) {
return;
}
// @ts-expect-error -- TSCONVERSION skip this if request is undefined
handleActivateRequest(request._id);
}
_handleShowRequestSettings() {
showModal(RequestSettingsModal, {
request: this.props.request,
});
}
_getMethodOverrideHeaderValue() {
const { request } = this.props;
// @ts-expect-error -- TSCONVERSION skip this if request is undefined or grpc
const header = getMethodOverrideHeader(request.headers);
if (header) {
return header.value;
}
// If no override, use GraphQL as override if it's a gql request
// @ts-expect-error -- TSCONVERSION skip this if request is undefined or grpc
if (request.body && request.body.mimeType === CONTENT_TYPE_GRAPHQL) {
return 'GQL';
}
return null;
}
async _debouncedUpdateRenderedUrl(props: Props) {
if (this._urlUpdateInterval !== null) {
clearTimeout(this._urlUpdateInterval);
}
this._urlUpdateInterval = setTimeout(() => {
this._updateRenderedUrl(props);
}, 300);
}
async _updateRenderedUrl(props: Props) {
let renderedUrl;
try {
// @ts-expect-error -- TSCONVERSION skip this if request is undefined or grpc
renderedUrl = await props.handleRender(props.request.url);
} catch (e) {
// Certain things, such as invalid variable tags and Prompts
// without titles will result in a failure to parse. Can't do
// much else, so let's just give them the unrendered URL
// @ts-expect-error -- TSCONVERSION skip this if request is undefined
renderedUrl = props.request.url;
}
this.setState({
renderedUrl,
});
}
setDragDirection(dragDirection) {
if (dragDirection !== this.state.dragDirection) {
this.setState({
dragDirection,
});
}
}
componentDidMount() {
const { request } = this.props;
if (request && !request.name) {
this._debouncedUpdateRenderedUrl(this.props);
}
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillUpdate(nextProps) {
if (!nextProps.request) {
return;
}
const requestUrl = this.props.request ? this.props.request.url : '';
if (nextProps.request.url !== requestUrl) {
this._debouncedUpdateRenderedUrl(nextProps);
}
}
componentWillUnmount() {
if (this._urlUpdateInterval !== null) {
clearTimeout(this._urlUpdateInterval);
}
}
render() {
const {
activeEnvironment,
connectDragSource,
connectDropTarget,
disableDragAndDrop,
filter,
handleCopyAsCurl,
handleDuplicateRequest,
handleGenerateCode,
handleSetRequestPinned,
hotKeyRegistry,
isActive,
isDragging,
isDraggingOver,
isPinned,
request,
requestGroup,
activeProject,
} = this.props;
const { dragDirection } = this.state;
let node;
const classes = classnames('sidebar__row', {
'sidebar__row--dragging': isDragging,
'sidebar__row--dragging-above': isDraggingOver && dragDirection > 0,
'sidebar__row--dragging-below': isDraggingOver && dragDirection < 0,
});
if (!request) {
node = (
<li className={classes}>
<div className="sidebar__item">
<button className="sidebar__clickable" onClick={this._handleRequestCreateFromEmpty}>
<em className="faded">click to add first request...</em>
</button>
</div>
</li>
);
} else {
const methodTag =
isGrpcRequest(request) ? (
<GrpcTag />
) : (
<MethodTag method={request.method} override={this._getMethodOverrideHeaderValue()} />
);
node = (
<li className={classes}>
<div
className={classnames('sidebar__item', 'sidebar__item--request', {
'sidebar__item--active': isActive,
})}
>
<button
className="wide"
onClick={this._handleRequestActivate}
onContextMenu={this._handleShowRequestActions}
>
<div className="sidebar__clickable">
{methodTag}
<Editable
value={request.name}
fallbackValue={this.state.renderedUrl}
blankValue="Empty"
className="inline-block"
onEditStart={this._handleEditStart}
onSubmit={this._handleRequestUpdateName}
renderReadView={(value, props) => (
<Highlight
search={filter}
text={value}
{...props}
title={`${request.name}\n${props.title}`}
/>
)}
/>
<GrpcSpinner requestId={request._id} className="margin-right-sm" />
</div>
</button>
<div className="sidebar__actions">
<RequestActionsDropdown
right
ref={this._setRequestActionsDropdownRef}
handleDuplicateRequest={handleDuplicateRequest}
handleSetRequestPinned={handleSetRequestPinned}
handleGenerateCode={handleGenerateCode}
handleCopyAsCurl={handleCopyAsCurl}
handleShowSettings={this._handleShowRequestSettings}
request={request}
isPinned={isPinned}
requestGroup={requestGroup}
hotKeyRegistry={hotKeyRegistry} // Necessary for plugin actions to have network capabilities
activeEnvironment={activeEnvironment}
activeProject={activeProject}
/>
</div>
{isPinned && (
<div className="sidebar__item__icon-pin">
<i className="fa fa-thumb-tack" />
</div>
)}
</div>
</li>
);
}
if (disableDragAndDrop) {
return node;
} else if (!this.state.isEditing) {
return connectDragSource(connectDropTarget(node));
} else {
return connectDropTarget(node);
}
}
}
const dragSource: DragSourceSpec<Props, DragObject> = {
beginDrag(props) {
return {
item: props.request,
};
},
};
const dropHandle = dropHandleCreator<Props>({
getParentId: props => props.requestGroup?._id || props.request?.parentId,
getTargetId: props => props.request?._id,
});
const hoverHandle = hoverHandleCreator<Props>();
const dragTarget: DropTargetSpec<Props> = {
drop: dropHandle,
hover: hoverHandle,
};
const source = DragSource('SIDEBAR_REQUEST_ROW', dragSource, sourceCollect)(UnconnectedSidebarRequestRow);
const Target = DropTarget('SIDEBAR_REQUEST_ROW', dragTarget, targetCollect)(source);
const useDerivedProps = () => {
const { handleRender } = useNunjucks();
const activeProject = useSelector(selectActiveProject);
const activeEnvironment = useSelector(selectActiveEnvironment);
return {
handleRender,
activeProject,
activeEnvironment,
};
};
export const SidebarRequestRow: FC<RawProps> = props => {
const derivedProps = useDerivedProps();
return <Target {...props} {...derivedProps} />;
};