Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(compass-crud): allows nested elements to also expand in steps #6009

Merged
merged 14 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import React, { useState } from 'react';
import { expect } from 'chai';
import { render, cleanup, screen, within } from '@testing-library/react';
import {
render,
cleanup,
screen,
within,
waitFor,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import HadronDocument from 'hadron-document';
import Document from './document';
Expand Down Expand Up @@ -244,4 +250,52 @@ describe('Document', function () {
expect(() => screen.getByText('firstName')).to.throw;
expect(() => screen.getByText('lastName')).to.throw;
});

it('should render "Show more" toggle when number of fields are more than allowed visible fields', async function () {
const hadronDoc = new HadronDocument({
prop1: 'prop1',
prop2: 'prop2',
prop3: 'prop3',
prop4: 'prop4',
});
hadronDoc.setMaxVisibleElementsCount(2);
render(<Document value={hadronDoc}></Document>);
expect(screen.getByText('prop1')).to.exist;
expect(screen.getByText('prop2')).to.exist;
expect(() => screen.getByText('prop3')).to.throw;
expect(() => screen.getByText('prop4')).to.throw;
expect(screen.getByText('Show 2 more fields')).to.exist;

hadronDoc.setMaxVisibleElementsCount(25);
await waitFor(() => {
expect(screen.getByText('prop3')).to.exist;
expect(screen.getByText('prop4')).to.exist;
});
});

it('should render "Show more" toggle on element when its nested fields are more than allowed visible fields', async function () {
const hadronDoc = new HadronDocument({
nested: {
prop1: 'prop1',
prop2: 'prop2',
prop3: 'prop3',
prop4: 'prop4',
},
});
const [nestedElement] = [...hadronDoc.elements];
hadronDoc.expand();
render(<Document value={hadronDoc}></Document>);
expect(screen.getByText('nested')).to.exist;
expect(screen.getByText('prop1')).to.exist;
expect(screen.getByText('prop2')).to.exist;
expect(screen.getByText('prop3')).to.exist;
expect(screen.getByText('prop4')).to.exist;

nestedElement.setMaxVisibleElementsCount(2);
await waitFor(() => {
expect(() => screen.getByText('prop3')).to.throw;
expect(() => screen.getByText('prop4')).to.throw;
});
expect(screen.getByText('Show 2 more fields in nested')).to.exist;
});
});
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { css } from '@leafygreen-ui/emotion';
import type {
default as HadronDocumentType,
Element as HadronElementType,
} from 'hadron-document';
import { ElementEvents } from 'hadron-document';
import {
DEFAULT_VISIBLE_DOCUMENT_ELEMENTS,
DocumentEvents,
ElementEvents,
} from 'hadron-document';
import { AutoFocusContext } from './auto-focus-context';
import { useForceUpdate } from './use-force-update';
import { HadronElement } from './element';
import { usePrevious } from './use-previous';
import DocumentFieldsToggleGroup from './document-fields-toggle-group';
import VisibleFieldsToggle from './visible-field-toggle';
import { documentTypography } from './typography';

function useHadronDocument(doc: HadronDocumentType) {
const prevDoc = usePrevious(doc);
const forceUpdate = useForceUpdate();

const onVisibleElementsChanged = useCallback(
(document: HadronDocumentType) => {
if (document === doc) {
forceUpdate();
}
},
[doc, forceUpdate]
);

const onDocumentFieldsAddedOrRemoved = useCallback(
(
_el: HadronElementType,
Expand All @@ -39,17 +52,20 @@ function useHadronDocument(doc: HadronDocumentType) {
}, [prevDoc, doc, forceUpdate]);

useEffect(() => {
doc.on(DocumentEvents.VisibleElementsChanged, onVisibleElementsChanged);
doc.on(ElementEvents.Added, onDocumentFieldsAddedOrRemoved);
doc.on(ElementEvents.Removed, onDocumentFieldsAddedOrRemoved);

return () => {
doc.off(DocumentEvents.VisibleElementsChanged, onVisibleElementsChanged);
doc.off(ElementEvents.Added, onDocumentFieldsAddedOrRemoved);
doc.off(ElementEvents.Removed, onDocumentFieldsAddedOrRemoved);
};
}, [doc, onDocumentFieldsAddedOrRemoved]);
}, [doc, onDocumentFieldsAddedOrRemoved, onVisibleElementsChanged]);

return {
elements: [...doc.elements],
visibleElements: doc.getVisibleElements(),
};
}

Expand All @@ -61,8 +77,6 @@ const hadronDocument = css({
counterReset: 'line-number',
});

const INITIAL_FIELD_LIMIT = 25;

// TODO: This element should implement treegrid aria role to be accessible
// https://www.w3.org/TR/wai-aria-practices/examples/treegrid/treegrid-1.html
// https://jira.mongodb.org/browse/COMPASS-5614
Expand All @@ -72,12 +86,7 @@ const HadronDocument: React.FunctionComponent<{
editing?: boolean;
onEditStart?: () => void;
}> = ({ value: document, editable = false, editing = false, onEditStart }) => {
const { elements } = useHadronDocument(document);
const [visibleFieldsCount, setVisibleFieldsCount] =
useState(INITIAL_FIELD_LIMIT);
const visibleElements = useMemo(() => {
return elements.filter(Boolean).slice(0, visibleFieldsCount);
}, [elements, visibleFieldsCount]);
const { elements, visibleElements } = useHadronDocument(document);
const [autoFocus, setAutoFocus] = useState<{
id: string;
type: 'key' | 'value' | 'type';
Expand All @@ -89,6 +98,13 @@ const HadronDocument: React.FunctionComponent<{
}
}, [editing]);

const handleVisibleFieldsChanged = useCallback(
(totalVisibleFields: number) => {
document.setMaxVisibleElementsCount(totalVisibleFields);
},
[document]
);

return (
<div>
<div
Expand Down Expand Up @@ -124,20 +140,20 @@ const HadronDocument: React.FunctionComponent<{
})}
</AutoFocusContext.Provider>
</div>
<DocumentFieldsToggleGroup
<VisibleFieldsToggle
// TODO: "Hide items" button will only be shown when document is not
// edited because it's not decided how to handle changes to the fields
// that are changed but then hidden
// https://jira.mongodb.org/browse/COMPASS-5587
showHideButton={!editing}
currentSize={visibleFieldsCount}
currentSize={document.maxVisibleElementsCount}
totalSize={elements.length}
minSize={INITIAL_FIELD_LIMIT}
minSize={DEFAULT_VISIBLE_DOCUMENT_ELEMENTS}
// In the editing mode we allow to show / hide less fields because
// historically Compass was doing this for "performance" reasons
step={editing ? 100 : 1000}
onSizeChange={setVisibleFieldsCount}
></DocumentFieldsToggleGroup>
onSizeChange={handleVisibleFieldsChanged}
></VisibleFieldsToggle>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is existing functionality, but I figured I'd bring it up here, no action needed. A small UX improvement here for folks with lots of fields in their documents could be to adjust the scroll position when they click to hide a lot of fields to the relative scroll height after the fields are hidden. Currently it's easy to lose a document if it's not the last document and you hide a bunch of fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I noticed this after I implemented virtual list. I will see what I can do there to modify this behaviour a little.

</div>
);
};
Expand Down
101 changes: 80 additions & 21 deletions packages/compass-components/src/components/document-list/element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import type {
Element as HadronElementType,
Editor as EditorType,
} from 'hadron-document';
import { ElementEvents, ElementEditor } from 'hadron-document';
import {
ElementEvents,
ElementEditor,
DEFAULT_VISIBLE_ELEMENTS,
} from 'hadron-document';
import BSONValue from '../bson-value';
import { spacing } from '@leafygreen-ui/tokens';
import { KeyEditor, ValueEditor, TypeEditor } from './element-editors';
Expand All @@ -23,6 +27,7 @@ import { css, cx } from '@leafygreen-ui/emotion';
import { palette } from '@leafygreen-ui/palette';
import { Icon } from '../leafygreen';
import { useDarkMode } from '../../hooks/use-theme';
import VisibleFieldsToggle from './visible-field-toggle';

function getEditorByType(type: HadronElementType['type']) {
switch (type) {
Expand Down Expand Up @@ -103,6 +108,7 @@ function useHadronElement(el: HadronElementType) {
el.on(ElementEvents.Removed, onElementAddedOrRemoved);
el.on(ElementEvents.Expanded, onElementChanged);
el.on(ElementEvents.Collapsed, onElementChanged);
el.on(ElementEvents.VisibleElementsChanged, onElementChanged);

return () => {
el.off(ElementEvents.Converted, onElementChanged);
Expand All @@ -113,6 +119,7 @@ function useHadronElement(el: HadronElementType) {
el.off(ElementEvents.Removed, onElementAddedOrRemoved);
el.off(ElementEvents.Expanded, onElementChanged);
el.off(ElementEvents.Collapsed, onElementChanged);
el.off(ElementEvents.VisibleElementsChanged, onElementChanged);
};
}, [el, onElementChanged, onElementAddedOrRemoved]);

Expand Down Expand Up @@ -162,6 +169,7 @@ function useHadronElement(el: HadronElementType) {
remove: el.isNotActionable() ? null : el.remove.bind(el),
expandable: Boolean(el.elements),
children: el.elements ? [...el.elements] : [],
visibleChildren: el.getVisibleElements(),
level: el.level,
parentType: el.parent?.currentType,
removed: el.isRemoved(),
Expand Down Expand Up @@ -381,6 +389,7 @@ export const HadronElement: React.FunctionComponent<{
remove,
expandable,
children,
visibleChildren,
level,
parentType,
removed,
Expand All @@ -400,9 +409,31 @@ export const HadronElement: React.FunctionComponent<{
const charCount = String(lineNumberSize).length;
return charCount > 2 ? `${charCount}.5ch` : spacing[3];
}
return spacing[3];
return spacing[400];
}, [lineNumberSize, editingEnabled]);

const elementSpacerWidth = useMemo(() => {
return (editable ? spacing[200] : 0) + spacing[400] * level;
}, [editable, level]);

// To render the "Show more" toggle for the nested expandable elements we need
// to calculate a proper offset so that it aligns with the nesting level
const nestedElementsVisibilityToggleOffset = useMemo(() => {
// the base padding that we have on all elements rendered in the document
const BASE_PADDING_LEFT = spacing[200];
const OFFSET_WHEN_EDITABLE = editable
? // space taken by element actions
spacing[400] +
// space and margin taken by line number element
spacing[400] +
spacing[100] +
// element spacer width that we render
elementSpacerWidth
: 0;
const EXPAND_ICON_SIZE = spacing[400];
return BASE_PADDING_LEFT + OFFSET_WHEN_EDITABLE + EXPAND_ICON_SIZE;
}, [editable, elementSpacerWidth]);

const isValid = key.valid && value.valid;
const shouldShowActions = editingEnabled;

Expand Down Expand Up @@ -436,6 +467,14 @@ export const HadronElement: React.FunctionComponent<{
const lineNumberInvalid = darkMode
? lineNumberInvalidDarkMode
: lineNumberInvalidLightMode;

const handleVisibleElementsChanged = useCallback(
(totalVisibleFields: number) => {
element.setMaxVisibleElementsCount(totalVisibleFields);
},
[element]
);

return (
<>
<div
Expand Down Expand Up @@ -501,10 +540,7 @@ export const HadronElement: React.FunctionComponent<{
</div>
</div>
)}
<div
className={elementSpacer}
style={{ width: (editable ? spacing[2] : 0) + spacing[3] * level }}
>
<div className={elementSpacer} style={{ width: elementSpacerWidth }}>
{/* spacer for nested documents */}
</div>
<div className={elementExpand}>
Expand Down Expand Up @@ -636,21 +672,44 @@ export const HadronElement: React.FunctionComponent<{
</div>
)}
</div>
{expandable &&
expanded &&
children.map((el, idx) => {
return (
<HadronElement
key={idx}
value={el}
editable={editable}
editingEnabled={editingEnabled}
onEditStart={onEditStart}
lineNumberSize={lineNumberSize}
onAddElement={onAddElement}
></HadronElement>
);
})}
{expandable && expanded && (
<>
{visibleChildren.map((el, idx) => {
return (
<HadronElement
key={idx}
value={el}
editable={editable}
editingEnabled={editingEnabled}
onEditStart={onEditStart}
lineNumberSize={lineNumberSize}
onAddElement={onAddElement}
></HadronElement>
);
})}
<VisibleFieldsToggle
parentFieldName={key.value}
// TODO: (Same as that for Document) "Hide items" button will only
// be shown when document is not edited because it's not decided how
// to handle changes to the fields that are changed but then hidden
// https://jira.mongodb.org/browse/COMPASS-5587
showHideButton={!editingEnabled}
currentSize={element.maxVisibleElementsCount}
totalSize={children.length}
minSize={DEFAULT_VISIBLE_ELEMENTS}
// Same as that for Document renderer, in the editing mode we allow
// to show / hide less fields because historically Compass was doing
// this for "performance" reasons
step={editingEnabled ? DEFAULT_VISIBLE_ELEMENTS : 1000}
onSizeChange={handleVisibleElementsChanged}
style={{
paddingLeft: nestedElementsVisibilityToggleOffset,
paddingTop: spacing[100],
paddingBottom: spacing[100],
}}
></VisibleFieldsToggle>
</>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { default as DocumentActionsGroup } from './document-actions-group';
export { default as DocumentFieldsToggleGroup } from './document-fields-toggle-group';
export { default as VisibleFieldsToggle } from './visible-field-toggle';
export { default as Document } from './document';
export { default as DocumentEditActionsFooter } from './document-edit-actions-footer';
Loading
Loading