| // 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 |
| }; |