blob: 542ba8e84d4f69e68ae9e775dc56d69514fa4966 [file] [log] [blame]
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
import FauxtonAPI from "../../../core/api";
import PropTypes from 'prop-types';
import React from "react";
import { Dropdown, MenuItem } from "react-bootstrap";
import ReactDOM from "react-dom";
import Actions from "./actions";
import Stores from "./stores";
import FauxtonComponents from "../../fauxton/components";
import GeneralComponents from "../../components/react-components";
import { Modal } from "react-bootstrap";
import Helpers from "../../../helpers";
var store = Stores.docEditorStore;
class DocEditorController extends React.Component {
static defaultProps = {
database: {},
isNewDoc: false
};
getStoreState = () => {
return {
isLoading: store.isLoading(),
doc: store.getDoc(),
cloneDocModalVisible: store.isCloneDocModalVisible(),
uploadModalVisible: store.isUploadModalVisible(),
deleteDocModalVisible: store.isDeleteDocModalVisible(),
numFilesUploaded: store.getNumFilesUploaded(),
conflictCount: store.getDocConflictCount()
};
};
getCodeEditor = () => {
if (this.state.isLoading) {
return (<GeneralComponents.LoadLines />);
}
var code = JSON.stringify(this.state.doc.attributes, null, ' ');
var editorCommands = [{
name: 'save',
bindKey: { win: 'Ctrl-S', mac: 'Ctrl-S' },
exec: this.saveDoc
}];
return (
<GeneralComponents.CodeEditor
id="doc-editor"
ref={node => this.docEditor = node}
defaultCode={code}
mode="json"
autoFocus={true}
editorCommands={editorCommands}
notifyUnsavedChanges={true}
stringEditModalEnabled={true} />
);
};
componentDidMount() {
store.on('change', this.onChange, this);
}
componentWillUnmount() {
store.off('change', this.onChange);
}
UNSAFE_componentWillUpdate(nextProps, nextState) {
// Update the editor whenever a file is uploaded, a doc is cloned, or a new doc is loaded
if (this.state.numFilesUploaded !== nextState.numFilesUploaded ||
this.state.doc && this.state.doc.hasChanged() ||
(this.state.doc && nextState.doc && this.state.doc.id !== nextState.doc.id)) {
this.getEditor().setValue(JSON.stringify(nextState.doc.attributes, null, ' '));
this.onSaveComplete();
}
}
onChange = () => {
this.setState(this.getStoreState());
};
saveDoc = () => {
Actions.saveDoc(this.state.doc, this.checkDocIsValid(), this.onSaveComplete);
};
onSaveComplete = () => {
this.getEditor().clearChanges();
};
hideDeleteDocModal = () => {
Actions.hideDeleteDocModal();
};
deleteDoc = () => {
Actions.hideDeleteDocModal();
Actions.deleteDoc(this.state.doc);
};
getEditor = () => {
return (this.docEditor) ? this.docEditor.getEditor() : null;
};
checkDocIsValid = () => {
if (this.getEditor().hasErrors()) {
return false;
}
var json = JSON.parse(this.getEditor().getValue());
this.state.doc.clear().set(json, { validate: true });
return !this.state.doc.validationError;
};
clearChanges = () => {
this.docEditor.clearChanges();
};
getExtensionIcons = () => {
var extensions = FauxtonAPI.getExtensions('DocEditor:icons');
return _.map(extensions, (Extension, i) => {
return (<Extension doc={this.state.doc} key={i} database={this.props.database} />);
});
};
getButtonRow = () => {
if (this.props.isNewDoc) {
return false;
}
return (
<div>
<AttachmentsPanelButton doc={this.state.doc} isLoading={this.state.isLoading} />
<div className="doc-editor-extension-icons">{this.getExtensionIcons()}</div>
{this.state.conflictCount ? <PanelButton
title={`Conflicts (${this.state.conflictCount})`}
iconClass="icon-columns"
className="conflicts"
onClick={() => { FauxtonAPI.navigate(FauxtonAPI.urls('revision-browser', 'app', this.props.database.safeID(), this.state.doc.id));}}/> : null}
<PanelButton className="upload" title="Upload Attachment" iconClass="icon-circle-arrow-up" onClick={Actions.showUploadModal} />
<PanelButton title="Clone Document" iconClass="icon-repeat" onClick={Actions.showCloneDocModal} />
<PanelButton title="Delete" iconClass="icon-trash" onClick={Actions.showDeleteDocModal} />
</div>
);
};
state = this.getStoreState();
render() {
var saveButtonLabel = (this.props.isNewDoc) ? 'Create Document' : 'Save Changes';
let endpoint = FauxtonAPI.urls('allDocs', 'app', FauxtonAPI.url.encode(this.props.database.id));
return (
<div>
<div id="doc-editor-actions-panel">
<div className="doc-actions-left">
<button className="save-doc btn btn-primary save" type="button" onClick={this.saveDoc}>
<i className="icon fonticon-ok-circled"></i> {saveButtonLabel}
</button>
<div>
<a href={`#/${endpoint}`} className="js-back cancel-button">Cancel</a>
</div>
</div>
<div className="alignRight">
{this.getButtonRow()}
</div>
</div>
<div className="code-region">
<div className="bgEditorGutter"></div>
<div id="editor-container" className="doc-code">{this.getCodeEditor()}</div>
</div>
<UploadModal
ref={node => this.uploadModal = node}
visible={this.state.uploadModalVisible}
doc={this.state.doc} />
<CloneDocModal
doc={this.state.doc}
database={this.props.database}
visible={this.state.cloneDocModalVisible}
onSubmit={this.clearChanges} />
<FauxtonComponents.ConfirmationModal
title="Confirm Deletion"
visible={this.state.deleteDocModalVisible}
text="Are you sure you want to delete this document?"
onClose={this.hideDeleteDocModal}
onSubmit={this.deleteDoc}
successButtonLabel="Delete Document" />
</div>
);
}
}
class AttachmentsPanelButton extends React.Component {
static propTypes = {
isLoading: PropTypes.bool.isRequired,
doc: PropTypes.object
};
static defaultProps = {
isLoading: true,
doc: {}
};
getAttachmentList = () => {
const db = encodeURIComponent(this.props.doc.database.get('id'));
const doc = encodeURIComponent(this.props.doc.get('_id'));
return _.map(this.props.doc.get('_attachments'), (item, filename) => {
const url = FauxtonAPI.urls('document', 'attachment', db, doc, encodeURIComponent(filename));
return (
<MenuItem key={filename} href={url} target="_blank" data-bypass="true">
<strong>{filename}</strong>
<span className="attachment-delimiter">-</span>
<span>{item.content_type}{item.content_type ? ', ' : ''}{Helpers.formatSize(item.length)}</span>
</MenuItem>
);
});
};
render() {
if (this.props.isLoading || !this.props.doc.get('_attachments')) {
return false;
}
return (
<div className="panel-section view-attachments-section btn-group">
<Dropdown id="view-attachments-menu">
<Dropdown.Toggle noCaret className="panel-button dropdown-toggle btn" data-bypass="true">
<i className="icon icon-paper-clip"></i>
<span className="button-text">View Attachments</span>
<span className="caret"></span>
</Dropdown.Toggle>
<Dropdown.Menu>
{this.getAttachmentList()}
</Dropdown.Menu>
</Dropdown>
</div>
);
}
}
class PanelButton extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
className: PropTypes.string
};
static defaultProps = {
title: '',
iconClass: '',
onClick: function () { },
className: ''
};
render() {
var iconClasses = 'icon ' + this.props.iconClass;
return (
<div className="panel-section">
<button className={`panel-button ${this.props.className}`} title={this.props.title} onClick={this.props.onClick}>
<i className={iconClasses}></i>
<span>{this.props.title}</span>
</button>
</div>
);
}
}
class UploadModal extends React.Component {
static propTypes = {
visible: PropTypes.bool.isRequired,
doc: PropTypes.object
};
getStoreState = () => {
return {
inProgress: store.isUploadInProgress(),
loadPercentage: store.getUploadLoadPercentage(),
errorMessage: store.getFileUploadErrorMsg()
};
};
componentDidMount() {
store.on('change', this.onChange, this);
}
componentWillUnmount() {
store.off('change', this.onChange);
}
onChange = () => {
this.setState(this.getStoreState());
};
closeModal = (e) => {
if (e) {
e.preventDefault();
}
if (this.state.inProgress) {
Actions.cancelUpload();
}
Actions.hideUploadModal();
Actions.resetUploadModal();
};
upload = () => {
Actions.uploadAttachment({
doc: this.props.doc,
rev: this.props.doc.get('_rev'),
files: this.attachments.files
});
};
state = this.getStoreState();
render() {
let errorClasses = 'alert alert-error';
if (this.state.errorMessage === '') {
errorClasses += ' hide';
}
let loadIndicatorClasses = 'progress progress-info';
let disabledAttribute = {disabled: 'disabled'};
if (!this.state.inProgress) {
loadIndicatorClasses += ' hide';
disabledAttribute = {};
}
return (
<Modal dialogClassName="upload-file-modal" show={this.props.visible} onHide={this.closeModal}>
<Modal.Header closeButton={true}>
<Modal.Title>Upload Attachment</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className={errorClasses}>{this.state.errorMessage}</div>
<div>
<form ref={node => this.uploadForm = node} className="form">
<p>
Select a file to upload as an attachment to this document. Uploading a file saves the document as a new
revision.
</p>
<input ref={el => this.attachments = el} type="file" name="_attachments" {...disabledAttribute}/>
<br />
</form>
<div className={loadIndicatorClasses}>
<div className="bar" style={{ width: this.state.loadPercentage + '%'}}></div>
</div>
</div>
</Modal.Body>
<Modal.Footer>
<a href="#" data-bypass="true" className="cancel-link" onClick={this.closeModal}>Cancel</a>
<button href="#" id="upload-btn" data-bypass="true" className="btn btn-primary save" onClick={this.upload} {...disabledAttribute}>
<i className="icon icon-upload" /> Upload Attachment
</button>
</Modal.Footer>
</Modal>
);
}
}
class CloneDocModal extends React.Component {
static propTypes = {
visible: PropTypes.bool.isRequired,
doc: PropTypes.object,
database: PropTypes.object.isRequired,
onSubmit: PropTypes.func.isRequired
};
state = {
uuid: null
};
cloneDoc = () => {
if (this.props.onSubmit) {
this.props.onSubmit();
}
Actions.cloneDoc(this.props.database, this.props.doc, this.state.uuid);
};
componentDidUpdate() {
if (this.state.uuid === null) {
Helpers.getUUID().then((res) => {
if (res.uuids) {
this.setState({ uuid: res.uuids[0] });
}
}).catch(() => {});
}
}
closeModal = (e) => {
if (e) {
e.preventDefault();
}
Actions.hideCloneDocModal();
};
docIDChange = (e) => {
this.setState({ uuid: e.target.value });
};
render() {
if (this.state.uuid === null) {
return false;
}
return (
<Modal dialogClassName="clone-doc-modal" show={this.props.visible} onHide={this.closeModal}>
<Modal.Header closeButton={true}>
<Modal.Title>Clone Document</Modal.Title>
</Modal.Header>
<Modal.Body>
<form className="form" onSubmit={(e) => { e.preventDefault(); this.cloneDoc(); }}>
<p>
Document cloning copies the saved version of the document. Unsaved document changes will be discarded.
</p>
<p>
You can modify the following generated ID for your new document.
</p>
<input ref={node => this.newDocId = node} type="text" autoFocus={true} className="input-block-level"
onChange={this.docIDChange} value={this.state.uuid} />
</form>
</Modal.Body>
<Modal.Footer>
<a href="#" data-bypass="true" className="cancel-link" onClick={this.closeModal}>Cancel</a>
<button className="btn btn-primary save" onClick={this.cloneDoc}>
<i className="icon-repeat"></i> Clone Document
</button>
</Modal.Footer>
</Modal>
);
}
}
export default {
DocEditorController: DocEditorController,
AttachmentsPanelButton: AttachmentsPanelButton,
UploadModal: UploadModal,
CloneDocModal: CloneDocModal
};