diff --git a/packages/compass-components/src/components/document-list/document.tsx b/packages/compass-components/src/components/document-list/document.tsx index d7bef7101be..2e2d9cb540c 100644 --- a/packages/compass-components/src/components/document-list/document.tsx +++ b/packages/compass-components/src/components/document-list/document.tsx @@ -85,7 +85,14 @@ const HadronDocument: React.FunctionComponent<{ editable?: boolean; editing?: boolean; onEditStart?: () => void; -}> = ({ value: document, editable = false, editing = false, onEditStart }) => { + extraGutterWidth?: number; +}> = ({ + value: document, + editable = false, + editing = false, + onEditStart, + extraGutterWidth, +}) => { const { elements, visibleElements } = useHadronDocument(document); const [autoFocus, setAutoFocus] = useState<{ id: string; @@ -113,8 +120,9 @@ const HadronDocument: React.FunctionComponent<{ editable, level: 0, alignWithNestedExpandIcon: false, + extraGutterWidth, }), - [editable] + [editable, extraGutterWidth] ); return ( @@ -147,6 +155,7 @@ const HadronDocument: React.FunctionComponent<{ type: el.parent?.currentType === 'Array' ? 'value' : 'key', }); }} + extraGutterWidth={extraGutterWidth} > ); })} diff --git a/packages/compass-components/src/components/document-list/element.tsx b/packages/compass-components/src/components/document-list/element.tsx index fe1a2def411..85197f9e709 100644 --- a/packages/compass-components/src/components/document-list/element.tsx +++ b/packages/compass-components/src/components/document-list/element.tsx @@ -363,36 +363,39 @@ const elementKeyDarkMode = css({ color: palette.gray.light2, }); -const calculateElementSpacerWidth = (editable: boolean, level: number) => { - return (editable ? spacing[100] : 0) + spacing[400] * level; +const calculateElementSpacerWidth = ( + editable: boolean, + level: number, + extra: number +) => { + return (editable ? spacing[100] : 0) + extra + spacing[400] * level; }; export const calculateShowMoreToggleOffset = ({ editable, level, alignWithNestedExpandIcon, + extraGutterWidth = 0, }: { editable: boolean; level: number; alignWithNestedExpandIcon: boolean; + extraGutterWidth: number | undefined; }) => { - // the base padding that we have on all elements rendered in the document - const BASE_PADDING_LEFT = spacing[50]; - const OFFSET_WHEN_EDITABLE = editable + const spacerWidth = calculateElementSpacerWidth( + editable, + level, + extraGutterWidth + ); + const editableOffset = editable ? // space taken by element actions spacing[300] + // space and margin taken by line number element spacing[400] + - spacing[100] + - // element spacer width that we render - calculateElementSpacerWidth(editable, level) + spacing[100] : 0; - const EXPAND_ICON_SIZE = spacing[400]; - return ( - BASE_PADDING_LEFT + - OFFSET_WHEN_EDITABLE + - (alignWithNestedExpandIcon ? EXPAND_ICON_SIZE : 0) - ); + const expandIconSize = alignWithNestedExpandIcon ? spacing[400] : 0; + return spacerWidth + editableOffset + expandIconSize; }; export const HadronElement: React.FunctionComponent<{ @@ -402,6 +405,7 @@ export const HadronElement: React.FunctionComponent<{ onEditStart?: (id: string, field: 'key' | 'value' | 'type') => void; lineNumberSize: number; onAddElement(el: HadronElementType): void; + extraGutterWidth?: number; }> = ({ value: element, editable, @@ -409,6 +413,7 @@ export const HadronElement: React.FunctionComponent<{ onEditStart, lineNumberSize, onAddElement, + extraGutterWidth = 0, }) => { const darkMode = useDarkMode(); const autoFocus = useAutoFocusContext(); @@ -445,8 +450,8 @@ export const HadronElement: React.FunctionComponent<{ }, [lineNumberSize, editingEnabled]); const elementSpacerWidth = useMemo( - () => calculateElementSpacerWidth(editable, level), - [editable, level] + () => calculateElementSpacerWidth(editable, level, extraGutterWidth), + [editable, level, extraGutterWidth] ); // To render the "Show more" toggle for the nested expandable elements we need @@ -457,8 +462,9 @@ export const HadronElement: React.FunctionComponent<{ editable, level, alignWithNestedExpandIcon: true, + extraGutterWidth, }), - [editable, level] + [editable, level, extraGutterWidth] ); const isValid = key.valid && value.valid; @@ -711,6 +717,7 @@ export const HadronElement: React.FunctionComponent<{ onEditStart={onEditStart} lineNumberSize={lineNumberSize} onAddElement={onAddElement} + extraGutterWidth={extraGutterWidth} > ); })} diff --git a/packages/compass-crud/src/components/readonly-document.tsx b/packages/compass-crud/src/components/readonly-document.tsx index e4815ad70ee..9397e959797 100644 --- a/packages/compass-crud/src/components/readonly-document.tsx +++ b/packages/compass-crud/src/components/readonly-document.tsx @@ -5,6 +5,7 @@ import type Document from 'hadron-document'; import type { TypeCastMap } from 'hadron-type-checker'; import { withPreferences } from 'compass-preferences-model/provider'; import { getInsightsForDocument } from '../utils'; +import { DocumentEvents } from 'hadron-document'; type BSONObject = TypeCastMap['Object']; export const documentStyles = css({ @@ -30,10 +31,73 @@ export type ReadonlyDocumentProps = { showInsights?: boolean; }; +type ReadonlyDocumentState = { + expanded: boolean; +}; + /** * Component for a single readonly document in a list of documents. */ -class ReadonlyDocument extends React.Component { +class ReadonlyDocument extends React.Component< + ReadonlyDocumentProps, + ReadonlyDocumentState +> { + constructor(props: ReadonlyDocumentProps) { + super(props); + this.state = { + 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. + */ + componentDidUpdate(prevProps: ReadonlyDocumentProps) { + if (prevProps.doc !== this.props.doc) { + this.unsubscribeFromDocumentEvents(prevProps.doc); + this.subscribeToDocumentEvents(this.props.doc); + } + } + + /** + * Unsubscribe from the update store on unmount. + */ + componentWillUnmount() { + this.unsubscribeFromDocumentEvents(this.props.doc); + } + + /** + * Subscribe to the document events. + */ + subscribeToDocumentEvents(doc: Document) { + doc.on(DocumentEvents.Expanded, this.handleExpanded); + doc.on(DocumentEvents.Collapsed, this.handleCollapsed); + } + + /** + * Unsubscribe from the document events. + */ + unsubscribeFromDocumentEvents(doc: Document) { + doc.on(DocumentEvents.Expanded, this.handleExpanded); + doc.on(DocumentEvents.Collapsed, this.handleCollapsed); + } + + handleExpanded = () => { + this.setState({ expanded: true }); + }; + + handleCollapsed = () => { + this.setState({ expanded: false }); + }; + handleClone = () => { const clonedDoc = this.props.doc.generateObject({ excludeInternalFields: true, @@ -48,13 +112,32 @@ class ReadonlyDocument extends React.Component { this.props.copyToClipboard?.(this.props.doc); }; + /** + * Handle clicking the expand all button. + */ + handleExpandAll = () => { + const { doc } = this.props; + // Update the doc directly - the components internal state will update via events + if (doc.expanded) { + doc.collapse(); + } else { + doc.expand(); + } + }; + /** * Get the elements for the document. * * @returns {Array} The elements. */ renderElements() { - return ; + return ( + + ); } renderActions() { @@ -64,6 +147,8 @@ class ReadonlyDocument extends React.Component { onClone={ this.props.openInsertDocumentDialog ? this.handleClone : undefined } + onExpand={this.handleExpandAll} + expanded={this.state.expanded} insights={ this.props.showInsights ? getInsightsForDocument(this.props.doc) @@ -94,7 +179,6 @@ class ReadonlyDocument extends React.Component { static propTypes = { copyToClipboard: PropTypes.func, doc: PropTypes.object.isRequired, - expandAll: PropTypes.bool, openInsertDocumentDialog: PropTypes.func, showInsights: PropTypes.bool, };