insomnia/packages/insomnia-app/app/ui/components/modals/workspace-environments-edit-modal.js
2018-12-12 12:36:11 -05:00

515 lines
16 KiB
JavaScript

// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import classnames from 'classnames';
import { SortableContainer, SortableElement, arrayMove } from 'react-sortable-hoc/dist/es6';
import { Dropdown, DropdownButton, DropdownItem } from '../base/dropdown';
import PromptButton from '../base/prompt-button';
import Button from '../base/button';
import Link from '../base/link';
import EnvironmentEditor from '../editors/environment-editor';
import Editable from '../base/editable';
import Modal from '../base/modal';
import ModalBody from '../base/modal-body';
import ModalHeader from '../base/modal-header';
import ModalFooter from '../base/modal-footer';
import * as models from '../../../models';
import { DEBOUNCE_MILLIS } from '../../../common/constants';
import type { Workspace } from '../../../models/workspace';
import type { Environment } from '../../../models/environment';
import * as db from '../../../common/database';
import HelpTooltip from '../help-tooltip';
import Tooltip from '../tooltip';
const ROOT_ENVIRONMENT_NAME = 'Base Environment';
type Props = {
activeEnvironmentId: string | null,
editorFontSize: number,
editorIndentSize: number,
editorKeyMap: string,
lineWrapping: boolean,
render: Function,
getRenderContext: Function,
nunjucksPowerUserMode: boolean,
isVariableUncovered: boolean,
};
type State = {
workspace: Workspace | null,
isValid: boolean,
subEnvironments: Array<Environment>,
rootEnvironment: Environment | null,
selectedEnvironmentId: string | null,
};
const SidebarListItem = SortableElement(
({ environment, activeEnvironment, showEnvironment, changeEnvironmentName }) => {
const classes = classnames({
'env-modal__sidebar-item': true,
'env-modal__sidebar-item--active': activeEnvironment === environment,
});
return (
<li key={environment._id} className={classes}>
<Button onClick={showEnvironment} value={environment}>
<i className="fa fa-drag-handle drag-handle" />
{environment.color ? (
<i className="space-right fa fa-circle" style={{ color: environment.color }} />
) : (
<i className="space-right fa fa-empty" />
)}
{environment.isPrivate && (
<Tooltip position="top" message="Environment will not be exported or synced">
<i className="fa fa-eye-slash faint space-right" />
</Tooltip>
)}
<Editable
className="inline-block"
onSubmit={name => changeEnvironmentName(environment, name)}
value={environment.name}
/>
</Button>
</li>
);
},
);
const SidebarList = SortableContainer(
({ environments, activeEnvironment, showEnvironment, changeEnvironmentName }) => (
<ul>
{environments.map((e, i) => (
<SidebarListItem
key={e._id}
environment={e}
index={i}
activeEnvironment={activeEnvironment}
showEnvironment={showEnvironment}
changeEnvironmentName={changeEnvironmentName}
/>
))}
</ul>
),
);
@autobind
class WorkspaceEnvironmentsEditModal extends React.PureComponent<Props, State> {
environmentEditorRef: ?EnvironmentEditor;
colorChangeTimeout: any;
saveTimeout: any;
modal: Modal;
editorKey: number;
constructor(props: Props) {
super(props);
this.state = {
workspace: null,
isValid: true,
subEnvironments: [],
rootEnvironment: null,
selectedEnvironmentId: null,
};
this.colorChangeTimeout = null;
this.editorKey = 0;
}
hide() {
this.modal && this.modal.hide();
}
_setEditorRef(n: ?EnvironmentEditor) {
this.environmentEditorRef = n;
}
_setModalRef(n: Modal | null) {
this.modal = n;
}
async show(workspace: Workspace) {
const { activeEnvironmentId } = this.props;
// Default to showing the currently active environment
if (activeEnvironmentId) {
this.setState({ selectedEnvironmentId: activeEnvironmentId });
}
await this._load(workspace);
this.modal && this.modal.show();
}
async _load(workspace: Workspace | null, environmentToSelect: Environment | null = null) {
if (!workspace) {
console.warn('Failed to reload environment editor without Workspace');
return;
}
const rootEnvironment = await models.environment.getOrCreateForWorkspace(workspace);
const subEnvironments = await models.environment.findByParentId(rootEnvironment._id);
let selectedEnvironmentId;
if (environmentToSelect) {
selectedEnvironmentId = environmentToSelect._id;
} else if (this.state.workspace && workspace._id !== this.state.workspace._id) {
// We've changed workspaces, so load the root one
selectedEnvironmentId = rootEnvironment._id;
} else {
// We haven't changed workspaces, so try loading the last environment, and fall back
// to the root one
selectedEnvironmentId = this.state.selectedEnvironmentId || rootEnvironment._id;
}
this.setState({
workspace,
rootEnvironment,
subEnvironments,
selectedEnvironmentId,
});
}
async _handleAddEnvironment(isPrivate: boolean = false) {
const { rootEnvironment, workspace } = this.state;
if (!rootEnvironment) {
console.warn('Failed to add environment. Unknown root environment');
return;
}
const parentId = rootEnvironment._id;
const environment = await models.environment.create({
parentId,
isPrivate,
});
await this._load(workspace, environment);
}
async _handleShowEnvironment(environment: Environment) {
// Don't allow switching if the current one has errors
if (this.environmentEditorRef && !this.environmentEditorRef.isValid()) {
return;
}
if (environment === this._getActiveEnvironment()) {
return;
}
const { workspace } = this.state;
await this._load(workspace, environment);
}
async _handleDeleteEnvironment(environment: Environment) {
const { rootEnvironment, workspace } = this.state;
// Don't delete the root environment
if (environment === rootEnvironment) {
return;
}
// Delete the current one, then activate the root environment
await models.environment.remove(environment);
await this._load(workspace, rootEnvironment);
}
async _handleChangeEnvironmentName(environment: Environment, name: string) {
const { workspace } = this.state;
// NOTE: Fetch the environment first because it might not be up to date.
// For example, editing the body updates silently.
const realEnvironment = await models.environment.getById(environment._id);
if (!realEnvironment) {
return;
}
await models.environment.update(realEnvironment, { name });
await this._load(workspace);
}
_handleChangeEnvironmentColor(environment: Environment, color: string | null) {
clearTimeout(this.colorChangeTimeout);
this.colorChangeTimeout = setTimeout(async () => {
const { workspace } = this.state;
await models.environment.update(environment, { color });
await this._load(workspace);
}, DEBOUNCE_MILLIS);
}
_didChange() {
this._saveChanges();
// Call this last in case component unmounted
const isValid = this.environmentEditorRef ? this.environmentEditorRef.isValid() : false;
if (this.state.isValid !== isValid) {
this.setState({ isValid });
}
}
_getActiveEnvironment(): Environment | null {
const { selectedEnvironmentId, subEnvironments, rootEnvironment } = this.state;
if (rootEnvironment && rootEnvironment._id === selectedEnvironmentId) {
return rootEnvironment;
} else {
return subEnvironments.find(e => e._id === selectedEnvironmentId) || null;
}
}
_handleUnsetColor(environment: Environment) {
this._handleChangeEnvironmentColor(environment, null);
}
componentDidMount() {
db.onChange(async changes => {
const { selectedEnvironmentId } = this.state;
for (const change of changes) {
const [
_, // eslint-disable-line no-unused-vars
doc,
fromSync,
] = change;
// Force an editor refresh if any changes from sync come in
if (doc._id === selectedEnvironmentId && fromSync) {
this.editorKey = doc.modified;
await this._load(this.state.workspace);
}
}
});
}
async _handleSortEnd(results: {
oldIndex: number,
newIndex: number,
collection: Array<Environment>,
}) {
const { oldIndex, newIndex } = results;
if (newIndex === oldIndex) {
return;
}
const { subEnvironments } = this.state;
const newSubEnvironments = arrayMove(subEnvironments, oldIndex, newIndex);
this.setState({ subEnvironments: newSubEnvironments });
// Do this last so we don't block the sorting
db.bufferChanges();
for (let i = 0; i < newSubEnvironments.length; i++) {
const environment = newSubEnvironments[i];
await models.environment.update(environment, { metaSortKey: i });
}
db.flushChanges();
}
async _handleClickColorChange(environment: Environment) {
let el = document.querySelector('#env-color-picker');
// Remove existing child so we reset the event handlers. This
// was easier than trying to clean them up later.
if (el && el.parentNode) {
el.parentNode.removeChild(el);
}
el = document.createElement('input');
el.id = 'env-color-picker';
el.type = 'color';
document.body && document.body.appendChild(el);
let color = environment.color || '#7d69cb';
if (!environment.color) {
await this._handleChangeEnvironmentColor(environment, color);
}
el.setAttribute('value', color);
el.addEventListener('input', (e: Event) => {
if (e.target instanceof HTMLInputElement) {
this._handleChangeEnvironmentColor(environment, e.target && e.target.value);
}
});
el.click();
}
_saveChanges() {
// Only save if it's valid
if (!this.environmentEditorRef || !this.environmentEditorRef.isValid()) {
return;
}
let data;
try {
data = this.environmentEditorRef.getValue();
} catch (err) {
// Invalid JSON probably
return;
}
const activeEnvironment = this._getActiveEnvironment();
if (activeEnvironment) {
clearTimeout(this.saveTimeout);
this.saveTimeout = setTimeout(() => {
models.environment.update(activeEnvironment, { data });
}, DEBOUNCE_MILLIS * 4);
}
}
render() {
const {
editorFontSize,
editorIndentSize,
editorKeyMap,
lineWrapping,
render,
getRenderContext,
nunjucksPowerUserMode,
isVariableUncovered,
} = this.props;
const { subEnvironments, rootEnvironment, isValid } = this.state;
const activeEnvironment = this._getActiveEnvironment();
return (
<Modal ref={this._setModalRef} wide tall {...this.props}>
<ModalHeader>Manage Environments</ModalHeader>
<ModalBody noScroll className="env-modal">
<div className="env-modal__sidebar">
<li
className={classnames('env-modal__sidebar-root-item', {
'env-modal__sidebar-item--active': activeEnvironment === rootEnvironment,
})}>
<Button onClick={this._handleShowEnvironment} value={rootEnvironment}>
{ROOT_ENVIRONMENT_NAME}
<HelpTooltip className="space-left">
The variables in this environment are always available, regardless of which
sub-environment is active. Useful for storing default or fallback values.
</HelpTooltip>
</Button>
</li>
<div className="pad env-modal__sidebar-heading">
<h3 className="no-margin">Sub Environments</h3>
<Dropdown right>
<DropdownButton>
<i className="fa fa-plus-circle" />
<i className="fa fa-caret-down" />
</DropdownButton>
<DropdownItem onClick={this._handleAddEnvironment} value={false}>
<i className="fa fa-eye" /> Environment
</DropdownItem>
<DropdownItem
onClick={this._handleAddEnvironment}
value={true}
title="Environment will not be exported or synced">
<i className="fa fa-eye-slash" /> Private Environment
</DropdownItem>
</Dropdown>
</div>
<SidebarList
environments={subEnvironments}
activeEnvironment={activeEnvironment}
showEnvironment={this._handleShowEnvironment}
changeEnvironmentName={this._handleChangeEnvironmentName}
onSortEnd={this._handleSortEnd}
helperClass="env-modal__sidebar-item--dragging"
transitionDuration={0}
useWindowAsScrollContainer={false}
/>
</div>
<div className="env-modal__main">
<div className="env-modal__main__header">
<h1>
{rootEnvironment === activeEnvironment ? (
ROOT_ENVIRONMENT_NAME
) : (
<Editable
singleClick
className="wide"
onSubmit={name =>
activeEnvironment &&
this._handleChangeEnvironmentName(activeEnvironment, name)
}
value={activeEnvironment ? activeEnvironment.name : ''}
/>
)}
</h1>
{activeEnvironment && rootEnvironment !== activeEnvironment ? (
<Dropdown className="space-right" right>
<DropdownButton className="btn btn--clicky">
{activeEnvironment.color && (
<i
className="fa fa-circle space-right"
style={{ color: activeEnvironment.color }}
/>
)}
Color <i className="fa fa-caret-down" />
</DropdownButton>
<DropdownItem value={activeEnvironment} onClick={this._handleClickColorChange}>
<i className="fa fa-circle" style={{ color: activeEnvironment.color }} />
{activeEnvironment.color ? 'Change Color' : 'Assign Color'}
</DropdownItem>
<DropdownItem
value={activeEnvironment}
onClick={this._handleUnsetColor}
disabled={!activeEnvironment.color}>
<i className="fa fa-minus-circle" />
Unset Color
</DropdownItem>
</Dropdown>
) : null}
{activeEnvironment && rootEnvironment !== activeEnvironment ? (
<PromptButton
value={activeEnvironment}
onClick={this._handleDeleteEnvironment}
className="btn btn--clicky">
<i className="fa fa-trash-o" />
</PromptButton>
) : null}
</div>
<div className="env-modal__editor">
<EnvironmentEditor
editorFontSize={editorFontSize}
editorIndentSize={editorIndentSize}
editorKeyMap={editorKeyMap}
lineWrapping={lineWrapping}
ref={this._setEditorRef}
key={`${this.editorKey}::${activeEnvironment ? activeEnvironment._id : 'n/a'}`}
environment={activeEnvironment ? activeEnvironment.data : {}}
didChange={this._didChange}
render={render}
getRenderContext={getRenderContext}
nunjucksPowerUserMode={nunjucksPowerUserMode}
isVariableUncovered={isVariableUncovered}
/>
</div>
</div>
</ModalBody>
<ModalFooter>
<div className="margin-left italic txt-sm tall">
* Environment data can be used for&nbsp;
<Link href="https://support.insomnia.rest/article/40-template-tags">
Nunjucks Templating
</Link>{' '}
in your requests
</div>
<button className="btn" disabled={!isValid} onClick={this.hide}>
Done
</button>
</ModalFooter>
</Modal>
);
}
}
export default WorkspaceEnvironmentsEditModal;