200ok-ch/organice

View on GitHub
src/components/OrgFile/index.js

Summary

Maintainability
D
1 day
Test Coverage
import React, { PureComponent, Fragment } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import { Redirect } from 'react-router-dom';

import { GlobalHotKeys } from 'react-hotkeys';

import './stylesheet.css';

import HeaderList from './components/HeaderList';
import ActionDrawer from './components/ActionDrawer';
import CaptureModal from './components/CaptureModal';
import SyncConfirmationModal from './components/SyncConfirmationModal';
import TagsEditorModal from './components/TagsEditorModal';
import TimestampEditorModal from './components/TimestampEditorModal';
import PropertyListEditorModal from './components/PropertyListEditorModal';
import TitleEditorModal from './components/TitleEditorModal';
import DescriptionEditorModal from './components/DescriptionEditorModal';
import TableEditorModal from './components/TableEditorModal';
import NoteEditorModal from './components/NoteEditorModal';
import AgendaModal from './components/AgendaModal';
import SearchModal from './components/SearchModal';
import ExternalLink from '../UI/ExternalLink';
import Drawer from '../UI/Drawer/';
import DrawerActionBar from './components/DrawerActionBar';

import * as baseActions from '../../actions/base';
import * as syncBackendActions from '../../actions/sync_backend';
import * as orgActions from '../../actions/org';
import * as captureActions from '../../actions/capture';
import { ActionCreators as undoActions } from 'redux-undo';

import sampleCaptureTemplates from '../../lib/sample_capture_templates';
import { calculateActionedKeybindings } from '../../lib/keybindings';
import {
  timestampWithId,
  headerWithId,
  extractAllOrgTags,
  extractAllOrgProperties,
  changelogHash,
  STATIC_FILE_PREFIX,
} from '../../lib/org_utils';

import _ from 'lodash';
import { fromJS, List, Map, Set } from 'immutable';
import { createRawDescriptionText, generateTitleLine } from '../../lib/export_org';
import FinderModal from './components/FinderModal';

class OrgFile extends PureComponent {
  constructor(props) {
    super(props);

    _.bindAll(this, [
      'handleSelectNextVisibleHeaderHotKey',
      'handleSelectPreviousVisibleHeaderHotKey',
      'handleToggleHeaderOpenedHotKey',
      'handleAdvanceTodoHotKey',
      'handleEditTitleHotKey',
      'handleEditDescriptionHotKey',
      'handleExitEditModeHotKey',
      'handleAddHeaderHotKey',
      'handleCreateFirstHeader',
      'handleRemoveHeaderHotKey',
      'handleMoveHeaderUpHotKey',
      'handleMoveHeaderDownHotKey',
      'handleMoveHeaderLeftHotKey',
      'handleMoveHeaderRightHotKey',
      'handleUndoHotKey',
      'handleContainerRef',
      'handleCapture',
      'handlePopupClose',
      'handleSearchPopupClose',
      'handleRefilePopupClose',
      'handleTitlePopupClose',
      'saveTitle',
      'handleTodoChange',
      'handleDescriptionPopupClose',
      'handleTablePopupClose',
      'handleSyncConfirmationPull',
      'handleSyncConfirmationPush',
      'handleSyncConfirmationCancel',
      'handleTagsChange',
      'handlePropertyListItemsChange',
      'getPopupCloseAction',
      'getPopupSwitchAction',
      'checkPopupAndHeader',
      'checkPopup',
    ]);

    this.state = {
      hasUncaughtError: false,
      editRawValues: props.preferEditRawValues,
    };
  }

  componentDidMount() {
    const { staticFile, path } = this.props;

    if (!!staticFile) {
      this.props.org.setPath(STATIC_FILE_PREFIX + staticFile);
      if (staticFile === 'changelog') {
        this.props.base.setHasUnseenChangelog(false);
        changelogHash().then((hash) => {
          this.props.base.setLastSeenChangelogHeader(hash);
        });
      }

      setTimeout(() => (document.querySelector('.App').scrollTop = 0), 0);
    } else if (!_.isEmpty(path)) {
      if (this.props.fileIsLoaded(path)) {
        this.props.org.sync({ path, shouldSuppressMessages: true });
      } else {
        this.props.syncBackend.downloadFile(path);
      }
      this.props.org.setPath(path);
    }

    this.activatePopup();
  }

  // If a fragment is set in the URL (by the activatePopup base
  // action), activate the appropriate pop-up
  activatePopup() {
    const urlFragment = window.location.hash.substr(1);
    if (!_.isEmpty(urlFragment)) {
      this.props.base.activatePopup(urlFragment);
    }
  }

  componentWillUnmount() {
    const { staticFile } = this.props;

    if (!!staticFile) {
      this.props.base.unloadStaticFile();
    } else {
      this.props.org.resetFileDisplay();
    }
  }

  componentDidUpdate(prevProps) {
    const { headers, pendingCapture } = this.props;
    if (!!pendingCapture && !!headers && headers.size > 0) {
      this.props.org.insertPendingCapture();
    }

    const { path } = this.props;
    if (!_.isEmpty(path) && path !== prevProps.path) {
      this.props.syncBackend.downloadFile(path);
      this.props.org.setPath(path);
    }
  }

  componentDidCatch() {
    // TODO: Track the `error` into a bug tracker
    this.setState({ hasUncaughtError: true });
  }

  handleSelectNextVisibleHeaderHotKey() {
    this.props.org.selectNextVisibleHeader();
  }

  handleSelectPreviousVisibleHeaderHotKey() {
    this.props.org.selectPreviousVisibleHeader();
  }

  handleToggleHeaderOpenedHotKey() {
    const { selectedHeaderId, closeSubheadersRecursively } = this.props;

    if (selectedHeaderId) {
      this.props.org.toggleHeaderOpened(selectedHeaderId, closeSubheadersRecursively);
    }
  }

  handleAdvanceTodoHotKey() {
    this.props.org.advanceTodoState(null, this.props.shouldLogIntoDrawer);
  }

  handleEditTitleHotKey() {
    this.props.base.activatePopup('title-editor');
  }

  handleEditDescriptionHotKey() {
    this.props.base.activatePopup('description-editor');
  }

  handleExitEditModeHotKey() {
    const onClose = this.getPopupSwitchAction(this.props.activePopupType);
    onClose(
      ...(this.state.popupCloseActionValuesAccessor
        ? this.state.popupCloseActionValuesAccessor()
        : [])
    );
    this.props.base.closePopup();
    this.container.focus();
  }

  handleAddHeaderHotKey() {
    this.props.org.addHeaderAndEdit(this.props.selectedHeaderId);
  }

  handleCreateFirstHeader() {
    this.props.org.createFirstHeader();
  }

  handleRemoveHeaderHotKey() {
    const { selectedHeaderId } = this.props;

    this.props.org.selectNextSiblingHeader(selectedHeaderId);
    this.props.org.removeHeader(selectedHeaderId);
  }

  handleMoveHeaderUpHotKey() {
    this.props.org.moveHeaderUp(this.props.selectedHeaderId);
  }

  handleMoveHeaderDownHotKey() {
    this.props.org.moveHeaderDown(this.props.selectedHeaderId);
  }

  handleMoveHeaderLeftHotKey() {
    this.props.org.moveHeaderLeft(this.props.selectedHeaderId);
  }

  handleMoveHeaderRightHotKey() {
    this.props.org.moveHeaderRight(this.props.selectedHeaderId);
  }

  handleUndoHotKey() {
    this.props.undo.undo();
  }

  handleContainerRef(container) {
    this.container = container;
    if (this.container) {
      this.container.focus();
    }
  }

  handleCapture(templateId, content, shouldPrepend) {
    this.props.org.insertCapture(templateId, content, shouldPrepend);
  }

  handlePopupClose() {
    this.props.base.closePopup();
  }

  handleSearchPopupClose(path, headerId) {
    this.props.base.closePopup();
    if (path && headerId) {
      this.props.org.selectHeaderAndOpenParents(path, headerId);
    }
  }

  handleRefilePopupClose(targetPath, targetHeaderId) {
    this.props.base.closePopup();
    // When the user closes the drawer without selecting a header, do
    // not trigger refiling.
    if (targetHeaderId) {
      const { loadedPath, selectedHeaderId } = this.props;
      this.props.org.refileSubtree(loadedPath, selectedHeaderId, targetPath, targetHeaderId);
    }
  }

  saveTitle(titleValue) {
    const { selectedHeader } = this.props;
    if (this.state.editRawValues) {
      if (generateTitleLine(selectedHeader.toJS(), false) !== titleValue) {
        this.props.org.updateHeaderTitle(selectedHeader.get('id'), titleValue);
      }
    } else {
      if (selectedHeader.getIn(['titleLine', 'rawTitle']) !== titleValue) {
        this.props.org.updateHeaderTitle(
          selectedHeader.get('id'),
          generateTitleLine(
            selectedHeader.setIn(['titleLine', 'rawTitle'], titleValue).toJS(),
            false
          )
        );
      }
    }
  }

  handleTitlePopupClose(titleValue) {
    this.saveTitle(titleValue);
    this.props.base.closePopup();
  }

  handleDescriptionPopupClose(descriptionValue) {
    const { selectedHeader } = this.props;
    if (this.state.editRawValues) {
      if (
        createRawDescriptionText(selectedHeader, false, this.props.dontIndent) !== descriptionValue
      ) {
        this.props.org.updateHeaderDescription(selectedHeader.get('id'), descriptionValue);
      }
    } else {
      if (selectedHeader.get('rawDescription') !== descriptionValue) {
        this.props.org.updateHeaderDescription(
          selectedHeader.get('id'),
          createRawDescriptionText(
            selectedHeader.set('rawDescription', descriptionValue),
            false,
            this.props.dontIndent
          )
        );
      }
    }
    this.props.base.closePopup();
  }

  handleTablePopupClose() {
    this.props.base.closePopup();
    this.props.org.setSelectedTableCellId(null);
    this.props.org.setSelectedTableId(null);
  }

  handleSyncConfirmationPull(path) {
    this.props.org.sync({ path, forceAction: 'pull' });
    this.props.base.closePopup();
  }

  handleSyncConfirmationPush(path) {
    this.props.org.sync({ path, forceAction: 'push' });
    this.props.base.closePopup();
  }

  handleSyncConfirmationCancel() {
    this.props.base.closePopup();
  }

  handleTagsChange(newTags) {
    this.props.org.setHeaderTags(this.props.selectedHeaderId, newTags);
  }

  handlePropertyListItemsChange(newPropertyListItems) {
    this.props.org.updatePropertyListItems(this.props.selectedHeaderId, newPropertyListItems);
  }

  handleTimestampChange(popupData) {
    if (!!popupData.get('timestampId')) {
      return (newTimestamp) =>
        this.props.org.updateTimestampWithId(popupData.get('timestampId'), newTimestamp);
    } else if (popupData.get('logEntryIndex') !== undefined) {
      return (newTimestamp) =>
        this.props.org.updateLogEntryTime(
          popupData.get('headerId'),
          popupData.get('logEntryIndex'),
          popupData.get('entryType'),
          newTimestamp.get('firstTimestamp')
        );
    } else {
      return (newTimestamp) =>
        this.props.org.updatePlanningItemTimestamp(
          popupData.get('headerId'),
          popupData.get('planningItemIndex'),
          newTimestamp.get('firstTimestamp')
        );
    }
  }

  handleTodoChange(newTodoKeyword) {
    this.props.org.setTodoState(
      this.props.selectedHeaderId,
      newTodoKeyword,
      this.props.shouldLogIntoDrawer
    );
  }

  getPopupSwitchAction(activePopupType) {
    switch (activePopupType) {
      case 'title-editor':
        return (titleValue) => this.handleTitlePopupClose(titleValue);
      case 'description-editor':
        return (descriptionValue) => this.handleDescriptionPopupClose(descriptionValue);
      default:
        return () => {};
    }
  }

  getPopupCloseAction(activePopupType) {
    switch (activePopupType) {
      case 'search':
        return this.handleSearchPopupClose;
      case 'refile':
        return this.handleRefilePopupClose;
      case 'title-editor':
        return this.handleTitlePopupClose;
      case 'description-editor':
        return this.handleDescriptionPopupClose;
      case 'table-editor':
        return this.handleTablePopupClose;
      default:
        return this.handlePopupClose;
    }
  }

  getPopupMaxSize(activePopupType) {
    switch (activePopupType) {
      case 'agenda':
      case 'task-list':
      case 'search':
      case 'refile':
        return true;
      default:
        return false;
    }
  }

  renderActivePopup(setPopupCloseActionValuesAccessor) {
    const {
      activePopupType,
      activePopupData,
      captureTemplates,
      files,
      headers,
      selectedHeader,
      shouldDisableActions,
      todoKeywordSets,
    } = this.props;

    switch (activePopupType) {
      case 'sync-confirmation':
        return (
          <SyncConfirmationModal
            lastServerModifiedAt={activePopupData.get('lastServerModifiedAt')}
            lastSyncAt={activePopupData.get('lastSyncAt')}
            path={activePopupData.get('path')}
            onPull={() => this.handleSyncConfirmationPull(activePopupData.get('path'))}
            onPush={() => this.handleSyncConfirmationPush(activePopupData.get('path'))}
            onCancel={this.handleSyncConfirmationCancel}
          />
        );
      case 'capture':
        const template = captureTemplates.find(
          (template) => template.get('id') === activePopupData.get('templateId')
        );
        const path = template.get('file');
        let headersOfCaptureTarget = headers;
        if (path) {
          const file = files.get(path);
          headersOfCaptureTarget = file ? file.get('headers') : List();
        }
        return (
          <CaptureModal
            template={template}
            headers={headersOfCaptureTarget}
            onCapture={this.handleCapture}
            onClose={this.getPopupCloseAction(activePopupType)}
          />
        );
      case 'tags-editor':
        const allTags = extractAllOrgTags(headers);
        return (
          <TagsEditorModal
            header={selectedHeader}
            allTags={allTags}
            onChange={this.handleTagsChange}
          />
        );
      case 'timestamp-editor':
      case 'scheduled-editor':
      case 'deadline-editor':
        let editingTimestamp = null;
        if (activePopupData.get('timestampId')) {
          editingTimestamp = timestampWithId(headers, activePopupData.get('timestampId'));
        } else if (activePopupData.get('logEntryIndex') !== undefined) {
          editingTimestamp = fromJS({
            firstTimestamp: headerWithId(headers, activePopupData.get('headerId')).getIn([
              'logBookEntries',
              activePopupData.get('logEntryIndex'),
              activePopupData.get('entryType'),
            ]),
          });
        } else if (
          // for scheduled timestamp and deadline the modal can be opened when no timestamp exists
          (activePopupType !== 'scheduled-editor' && activePopupType !== 'deadline-editor') ||
          activePopupData.get('planningItemIndex') !== -1
        ) {
          editingTimestamp = fromJS({
            firstTimestamp: headerWithId(headers, activePopupData.get('headerId')).getIn([
              'planningItems',
              activePopupData.get('planningItemIndex'),
              'timestamp',
            ]),
          });
        }

        return (
          <TimestampEditorModal
            headerId={activePopupData.get('headerId')}
            timestamp={editingTimestamp}
            timestampId={activePopupData.get('timestampId')}
            popupType={activePopupType}
            planningItemIndex={activePopupData.get('planningItemIndex')}
            singleTimestampOnly={!activePopupData.get('timestampId')}
            onClose={this.getPopupCloseAction(activePopupType)}
            onChange={this.handleTimestampChange(activePopupData)}
          />
        );

      case 'property-list-editor':
        const allOrgProperties = extractAllOrgProperties(headers);
        return selectedHeader ? (
          <PropertyListEditorModal
            onChange={this.handlePropertyListItemsChange}
            propertyListItems={selectedHeader.get('propertyListItems')}
            allOrgProperties={allOrgProperties}
          />
        ) : null;
      case 'agenda':
        return (
          <AgendaModal headers={headers} onClose={this.getPopupCloseAction(activePopupType)} />
        );
      case 'search':
        return (
          <FinderModal headers={headers} onClose={this.getPopupCloseAction(activePopupType)} />
        );
      case 'refile':
        return <SearchModal context="refile" onClose={this.getPopupCloseAction(activePopupType)} />;
      case 'title-editor':
        return (
          <TitleEditorModal
            editRawValues={this.state.editRawValues}
            todoKeywordSets={todoKeywordSets}
            onClose={this.getPopupCloseAction('title-editor')}
            saveTitle={this.saveTitle}
            onTodoClicked={this.handleTodoChange}
            header={selectedHeader}
            setPopupCloseActionValuesAccessor={setPopupCloseActionValuesAccessor}
          />
        );
      case 'description-editor':
        return (
          <DescriptionEditorModal
            editRawValues={this.state.editRawValues}
            header={selectedHeader}
            dontIndent={this.props.dontIndent}
            setPopupCloseActionValuesAccessor={setPopupCloseActionValuesAccessor}
          />
        );
      case 'table-editor':
        return <TableEditorModal shouldDisableActions={shouldDisableActions} />;
      case 'note-editor':
        return <NoteEditorModal shouldDisableActions={shouldDisableActions} />;
      default:
        return null;
    }
  }

  // Some keyboard shortcuts only make sense when a header is selected and no popup is open
  checkPopupAndHeader(callback) {
    return (event) => {
      if (
        this.props.selectedHeader &&
        !this.props.activePopupType &&
        !this.props.shouldDisableActions
      ) {
        callback(event);
      }
    };
  }

  // Read only actions only need to be disabled when a popup is open
  checkPopup(callback) {
    return (event) => {
      if (!this.props.activePopupType) {
        callback(event);
      }
    };
  }

  render() {
    const {
      headers,
      linesBeforeHeadings,
      shouldDisableDirtyIndicator,
      shouldDisableSyncButtons,
      shouldDisableActions,
      isDirty,
      parsingErrorMessage,
      path,
      staticFile,
      customKeybindings,
      orgFileErrorMessage,
      activePopupType,
    } = this.props;

    if (!path && !staticFile) {
      return <Redirect to="/files" />;
    }

    if (this.state.hasUncaughtError) {
      return (
        <div className="error-message-container">
          Uh oh, you ran into a bug!
          <br />
          <br />
          This was probably the result of an error in attempting to parse your org file. It'd be
          super helpful if you could{' '}
          <ExternalLink href="https://github.com/200ok-ch/organice/issues/new">
            create an issue
          </ExternalLink>{' '}
          (and include the org file if possible!)
        </div>
      );
    }

    if (!!orgFileErrorMessage) {
      return <div className="error-message-container">{orgFileErrorMessage}</div>;
    }

    if (!headers) {
      return <div />;
    }

    const keyMap = _.fromPairs(calculateActionedKeybindings(customKeybindings));

    // Automatically call preventDefault on all the keyboard events that come through for
    // these hotkeys.
    const preventDefault = (callback) => (event) => {
      event.preventDefault();
      callback(event);
    };

    const handlers = {
      selectNextVisibleHeader: this.checkPopup(
        preventDefault(this.handleSelectNextVisibleHeaderHotKey)
      ),
      selectPreviousVisibleHeader: this.checkPopup(
        preventDefault(this.handleSelectPreviousVisibleHeaderHotKey)
      ),
      toggleHeaderOpened: this.checkPopup(
        preventDefault(this.handleToggleHeaderOpenedHotKey, true)
      ),
      advanceTodo: this.checkPopupAndHeader(preventDefault(this.handleAdvanceTodoHotKey)),
      editTitle: this.checkPopupAndHeader(preventDefault(this.handleEditTitleHotKey)),
      editDescription: this.checkPopupAndHeader(preventDefault(this.handleEditDescriptionHotKey)),
      exitEditMode: preventDefault(this.handleExitEditModeHotKey),
      addHeader: this.checkPopupAndHeader(preventDefault(this.handleAddHeaderHotKey)),
      removeHeader: this.checkPopupAndHeader(preventDefault(this.handleRemoveHeaderHotKey, true)),
      moveHeaderUp: this.checkPopupAndHeader(preventDefault(this.handleMoveHeaderUpHotKey)),
      moveHeaderDown: this.checkPopupAndHeader(preventDefault(this.handleMoveHeaderDownHotKey)),
      moveHeaderLeft: this.checkPopupAndHeader(preventDefault(this.handleMoveHeaderLeftHotKey)),
      moveHeaderRight: this.checkPopupAndHeader(preventDefault(this.handleMoveHeaderRightHotKey)),
      undo: this.checkPopupAndHeader(preventDefault(this.handleUndoHotKey)),
    };

    const setPopupCloseActionValuesAccessor = (v) => {
      this.setState({ popupCloseActionValuesAccessor: v });
    };

    // Check if this a legit Org file with content, just no headers
    const noHeadlineButContent = () => {
      return headers.size === 0 && linesBeforeHeadings.size > 0;
    };

    return (
      <GlobalHotKeys keyMap={keyMap} handlers={handlers}>
        <div className="org-file-container" tabIndex="-1" ref={this.handleContainerRef}>
          {headers.size === 0 ? (
            <div className="org-file__parsing-error-message">
              <h3>This file has no headlines</h3>

              {!!parsingErrorMessage ? (
                <Fragment>{parsingErrorMessage}</Fragment>
              ) : (
                <Fragment>
                  {noHeadlineButContent() ? (
                    <>
                      <p>Yes, your file has content. Do not worry, it is still there! </p>
                      <p>
                        However, interacting with Org files in organice happens on a per headline
                        basis. To use organice with this file, please create a new headline with the
                        button below. The existing content is then put into the description of this
                        new header.
                      </p>
                    </>
                  ) : (
                    <p></p>
                  )}
                  <p>Interact with your file by creating the first headline.</p>
                  <p>
                    <button className="btn" onClick={this.handleCreateFirstHeader}>
                      Create headline
                    </button>
                  </p>
                </Fragment>
              )}
            </div>
          ) : (
            <HeaderList shouldDisableActions={shouldDisableActions} />
          )}

          {isDirty && !shouldDisableDirtyIndicator && (
            <div className="dirty-indicator">Unpushed changes</div>
          )}

          {!shouldDisableActions && (
            <ActionDrawer
              shouldDisableSyncButtons={shouldDisableSyncButtons}
              staticFile={staticFile}
            />
          )}

          {activePopupType ? (
            <Drawer
              onClose={() => {
                this.getPopupCloseAction(activePopupType)(
                  ...(this.state.popupCloseActionValuesAccessor
                    ? this.state.popupCloseActionValuesAccessor()
                    : [])
                );
                this.setState({ editRawValues: this.props.preferEditRawValues });
                this.container.focus();
              }}
              maxSize={this.getPopupMaxSize(activePopupType)}
            >
              {this.renderActivePopup(setPopupCloseActionValuesAccessor)}
              {(activePopupType === 'title-editor' ||
                activePopupType === 'description-editor' ||
                activePopupType === 'tags-editor' ||
                activePopupType === 'property-list-editor' ||
                activePopupType === 'timestamp-editor' ||
                activePopupType === 'scheduled-editor' ||
                activePopupType === 'deadline-editor' ||
                activePopupType === 'note-editor') && (
                <DrawerActionBar
                  onSwitch={() => {
                    this.getPopupSwitchAction(activePopupType)(
                      ...(this.state.popupCloseActionValuesAccessor
                        ? this.state.popupCloseActionValuesAccessor()
                        : [])
                    );
                  }}
                  editRawValues={this.state.editRawValues}
                  setEditRawValues={(editRawValues) => this.setState({ editRawValues })}
                  restorePreferEditRawValues={() =>
                    this.setState({ editRawValues: this.props.preferEditRawValues })
                  }
                />
              )}
            </Drawer>
          ) : null}
        </div>
      </GlobalHotKeys>
    );
  }
}

const mapStateToProps = (state) => {
  const files = state.org.present.get('files');
  const path = state.org.present.get('path');
  const loadedFiles = Set.fromKeys(files);
  const fileIsLoaded = (path) => loadedFiles.includes(path);
  const file = state.org.present.getIn(['files', path], Map());
  const headers = file.get('headers');
  const linesBeforeHeadings = file.get('linesBeforeHeadings');
  const selectedHeaderId = file.get('selectedHeaderId', null);
  const activePopup = state.base.get('activePopup', Map());

  return {
    loadedPath: path,
    files,
    headers,
    linesBeforeHeadings,
    selectedHeaderId,
    isDirty: file.get('isDirty'),
    fileIsLoaded,
    selectedHeader: headers && headers.find((header) => header.get('id') === selectedHeaderId),
    customKeybindings: state.base.get('customKeybindings'),
    dontIndent: state.base.get('shouldNotIndentOnExport'),
    shouldLogIntoDrawer: state.base.get('shouldLogIntoDrawer'),
    shouldLiveSync: state.base.get('shouldLiveSync'),
    showDeadlineDisplay: state.base.get('showDeadlineDisplay'),
    activePopupType: !!activePopup ? activePopup.get('type') : null,
    activePopupData: !!activePopup ? activePopup.get('data') : null,
    captureTemplates: state.capture.get('captureTemplates').concat(sampleCaptureTemplates),
    pendingCapture: state.org.present.get('pendingCapture'),
    closeSubheadersRecursively: state.base.get('closeSubheadersRecursively'),
    orgFileErrorMessage: state.org.present.get('orgFileErrorMessage'),
    preferEditRawValues: state.base.get('preferEditRawValues'),
    todoKeywordSets: file.get('todoKeywordSets'),
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    base: bindActionCreators(baseActions, dispatch),
    syncBackend: bindActionCreators(syncBackendActions, dispatch),
    org: bindActionCreators(orgActions, dispatch),
    undo: bindActionCreators(undoActions, dispatch),
    capture: bindActionCreators(captureActions, dispatch),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(OrgFile);