import React from 'react';
import PropTypes from 'prop-types';
import type { Document } from 'hadron-document';
import HadronDocument from 'hadron-document';
import { DocumentList, css } from '@mongodb-js/compass-components';
import { withPreferences } from 'compass-preferences-model';
import { documentStyles, documentContentStyles } from './readonly-document';
import { getInsightsForDocument } from '../utils';
import type { CrudActions } from '../stores/crud-store';
const documentElementsContainerStyles = css({
position: 'relative',
});
/**
* The initial field limit.
*/
const INITIAL_FIELD_LIMIT = 25;
export type EditableDocumentProps = {
doc: Document;
removeDocument?: CrudActions['removeDocument'];
replaceDocument?: CrudActions['replaceDocument'];
updateDocument?: CrudActions['updateDocument'];
openInsertDocumentDialog?: CrudActions['openInsertDocumentDialog'];
copyToClipboard?: CrudActions['copyToClipboard'];
showInsights?: boolean;
};
type EditableDocumentState = {
editing: boolean;
deleting: boolean;
expanded: boolean;
renderSize?: number;
};
/**
* Component for a single editable document in a list of documents.
*/
class EditableDocument extends React.Component<
EditableDocumentProps,
EditableDocumentState
> {
constructor(props: EditableDocumentProps) {
super(props);
this.state = {
editing: false,
deleting: false,
expanded: props.doc.expanded,
};
}
/**
* Subscribe to the update store on mount.
*/
componentDidMount() {
this.subscribeToDocumentEvents(this.props.doc);
}
/**
* Refreshing the list updates the doc in the props so we should update the
* document on the instance.
*
* @param {Object} prevProps - The previous props.
*/
componentDidUpdate(prevProps: EditableDocumentProps) {
if (prevProps.doc !== this.props.doc) {
this.unsubscribeFromDocumentEvents(prevProps.doc);
this.subscribeToDocumentEvents(this.props.doc);
if (this.state.editing || this.state.deleting) {
// If the underlying document changed, that means that the collection
// contents have been refreshed. In that case, stop editing/deleting.
setTimeout(() => {
this.setState({ editing: false, deleting: false });
});
}
}
}
/**
* Unsubscribe from the update store on unmount.
*/
componentWillUnmount() {
this.unsubscribeFromDocumentEvents(this.props.doc);
}
/**
* Subscribe to the hadron document events.
*
* @param {Document} doc - The hadron document.
*/
subscribeToDocumentEvents(doc: Document) {
doc.on(HadronDocument.Events.Cancel, this.handleCancel);
doc.on(HadronDocument.Events.Expanded, this.handleExpanded);
doc.on(HadronDocument.Events.Collapsed, this.handleCollapsed);
doc.on('remove-success', this.handleRemoveSuccess);
doc.on('update-success', this.handleUpdateSuccess);
}
/**
* Unsubscribe from the hadron document events.
*
* @param {Document} doc - The hadron document.
*/
unsubscribeFromDocumentEvents(doc: Document) {
doc.removeListener(HadronDocument.Events.Cancel, this.handleCancel);
doc.removeListener(HadronDocument.Events.Expanded, this.handleExpanded);
doc.removeListener(HadronDocument.Events.Collapsed, this.handleCollapsed);
doc.removeListener('remove-success', this.handleRemoveSuccess);
doc.removeListener('update-success', this.handleUpdateSuccess);
}
/**
* Fires when the document update was successful.
*/
handleUpdateSuccess = () => {
this.setState({ editing: false });
};
/**
* Handle the successful remove.
*/
handleRemoveSuccess = () => {
this.setState({ deleting: false });
};
/**
* Handles canceling edits to the document.
*/
handleCancel = () => {
this.setState({
editing: false,
deleting: false,
renderSize: INITIAL_FIELD_LIMIT,
});
};
handleExpanded = () => {
this.setState({ expanded: true });
};
handleCollapsed = () => {
this.setState({ expanded: false });
};
/**
* Handle copying JSON to clipboard of the document.
*/
handleCopy() {
this.props.copyToClipboard?.(this.props.doc);
}
/**
* Handle cloning of the document.
*/
handleClone() {
const clonedDoc = this.props.doc.generateObject({
excludeInternalFields: true,
});
this.props.openInsertDocumentDialog?.(clonedDoc, true);
}
/**
* Handles document deletion.
*/
handleDelete() {
this.setState({
deleting: true,
editing: false,
renderSize: INITIAL_FIELD_LIMIT,
});
}
/**
* Handle the edit click.
*/
handleEdit() {
this.setState({ editing: true });
}
/**
* Handle clicking the expand all button.
*/
handleExpandAll() {
const { doc } = this.props;
if (this.state.expanded) {
doc.collapse();
} else {
doc.expand();
}
}
/**
* Render the actions component.
*
* @returns {Component} The actions component.
*/
renderActions() {
if (!this.state.editing && !this.state.deleting) {
return (
);
}
}
/**
* Get the elements for the document. If we are editing, we get editable elements,
* otherwise the readonly elements are returned.
*
* @returns {Array} The elements.
*/
renderElements() {
return (
);
}
/**
* Render the footer component.
*
* @returns {Component} The footer component.
*/
renderFooter() {
return (
{
if (force) {
this.props.replaceDocument?.(this.props.doc);
} else {
this.props.updateDocument?.(this.props.doc);
}
}}
onDelete={() => {
this.props.removeDocument?.(this.props.doc);
}}
onCancel={() => {
this.handleCancel();
}}
/>
);
}
/**
* Render a single document list item.
*
* @returns {React.Component} The component.
*/
render() {
return (
{this.renderElements()}
{this.renderActions()}
{this.renderFooter()}
);
}
static displayName = 'EditableDocument';
static propTypes = {
doc: PropTypes.object.isRequired,
expandAll: PropTypes.bool,
removeDocument: PropTypes.func.isRequired,
replaceDocument: PropTypes.func.isRequired,
updateDocument: PropTypes.func.isRequired,
openInsertDocumentDialog: PropTypes.func.isRequired,
copyToClipboard: PropTypes.func.isRequired,
showInsights: PropTypes.bool,
};
}
export default withPreferences(EditableDocument, ['showInsights'], React);