Skip to content

Commit

Permalink
feat: delete bulk operation modal COMPASS-7326 (#5005)
Browse files Browse the repository at this point in the history
* chore: e2e flow

* chore: add bulk state in a new substate

* chore: add todo markers

* chore: styling

* chore: more styling 💅

* chore: linting

* chore: add dependency to query-parser

* chore: more styling

* chore: fix height of preview

* chore: linting

* chore: fix package-lock.json

* chore: fix package-lock

* chore: fix tests

* chore: test modal

* chore: oops i did it again, remove the .only

* chore: remove unused imports

* chore: test bulk dialog

* chore: last minute design changes

* chore: add ts-ignore and clean up
  • Loading branch information
kmruiz authored Oct 20, 2023
1 parent f8620f3 commit b296a11
Show file tree
Hide file tree
Showing 7 changed files with 405 additions and 9 deletions.
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/compass-crud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"mocha": "^10.2.0",
"mongodb-data-service": "^22.12.2",
"mongodb-instance-model": "^12.12.2",
"mongodb-query-parser": "^3.1.3",
"mongodb-ns": "^2.4.0",
"nyc": "^15.1.0",
"prop-types": "^15.7.2",
Expand Down
75 changes: 75 additions & 0 deletions packages/compass-crud/src/components/bulk-delete-modal.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import { expect } from 'chai';
import sinon from 'sinon';
import { render, screen, cleanup } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BulkDeleteModal from './bulk-delete-modal';

function renderBulkDeleteModal(
props?: Partial<React.ComponentProps<typeof BulkDeleteModal>>
) {
return render(
<BulkDeleteModal
open={true}
documentCount={0}
filterQuery="{ a: 1 }"
namespace="mydb.mycoll"
sampleDocuments={[]}
onCancel={() => {}}
onConfirmDeletion={() => {}}
{...props}
/>
);
}

describe('BulkDeleteModal Component', function () {
afterEach(function () {
cleanup();
});

it('does not render if closed', function () {
renderBulkDeleteModal({ open: false });
expect(screen.queryByText(/Delete/)).to.not.exist;
});

it('does render if open', function () {
renderBulkDeleteModal();
expect(screen.queryAllByText(/Delete/)).to.not.be.empty;
});

it('shows the number of documents that will be deleted', function () {
renderBulkDeleteModal({ documentCount: 42 });
expect(screen.queryAllByText('Delete 42 documents')[0]).to.be.visible;
});

it('shows the affected collection', function () {
renderBulkDeleteModal({ namespace: 'mydb.mycoll' });
expect(screen.queryByText('mydb.mycoll')).to.be.visible;
});

it('shows the provided query', function () {
renderBulkDeleteModal({ filterQuery: '{ a: 1 }' });
expect(screen.queryByDisplayValue('{ a: 1 }')).to.be.visible;
});

it('closes the modal when cancelled', function () {
const onCloseSpy = sinon.spy();
renderBulkDeleteModal({ onCancel: onCloseSpy });

userEvent.click(screen.getByText('Close').closest('button')!);
expect(onCloseSpy).to.have.been.calledOnce;
});

it('confirms deletion when clicked on the Delete documents button', function () {
const onConfirmDeletionSpy = sinon.spy();
renderBulkDeleteModal({
documentCount: 10,
onConfirmDeletion: onConfirmDeletionSpy,
});

userEvent.click(
screen.getAllByText('Delete 10 documents')[1].closest('button')!
);
expect(onConfirmDeletionSpy).to.have.been.calledOnce;
});
});
159 changes: 159 additions & 0 deletions packages/compass-crud/src/components/bulk-delete-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React from 'react';
import {
Modal,
ModalHeader,
ModalBody,
ModalFooter,
Button,
TextInput,
KeylineCard,
css,
cx,
spacing,
InfoSprinkle,
Label,
} from '@mongodb-js/compass-components';
import ReadonlyDocument from './readonly-document';

const modalFooterSpacingStyles = css({
gap: spacing[2],
});

const documentHorizontalWrapper = css({
display: 'flex',
flexDirection: 'row',
flex: 'none',
flexShrink: 0,
overflow: 'auto',
marginBottom: spacing[2],
gap: spacing[2],
maxWidth: '100%',
});

const documentContainerStyles = css({
display: 'flex',
flexDirection: 'column',
flex: 'none',
flexShrink: 0,
marginBottom: spacing[2],
width: '100%',
});

const documentStyles = css({
flexBasis: '164px',
flexGrow: 1,
flexShrink: 0,
overflow: 'auto',
padding: 0,
width: '100%',
});

const modalBodySpacingStyles = css({
marginTop: spacing[3],
paddingLeft: spacing[5],
display: 'flex',
flexDirection: 'column',
gap: spacing[3],
});

const previewStyles = css({
minHeight: '200px',
});

type QueryLabelProps = {
tooltip: string;
label: string;
};

const queryLabelStyles = css({
display: 'flex',
gap: spacing[2],
alignItems: 'center',
});

const QueryLabel: React.FunctionComponent<QueryLabelProps> = ({
tooltip,
label,
}) => {
return (
<div className={queryLabelStyles}>
<Label htmlFor="template-dropdown">{label}</Label>
<InfoSprinkle align="right">{tooltip}</InfoSprinkle>
</div>
);
};

type BulkDeleteModalProps = {
open: boolean;
documentCount: number;
filterQuery: string;
namespace: string;
sampleDocuments: Document[];
onCancel: () => void;
onConfirmDeletion: () => void;
};

const BulkDeleteModal: React.FunctionComponent<BulkDeleteModalProps> = ({
open,
documentCount,
filterQuery,
namespace,
sampleDocuments,
onCancel,
onConfirmDeletion,
}) => {
const preview = (
<div className={documentHorizontalWrapper}>
{sampleDocuments.map((doc, i) => {
return (
<KeylineCard
key={i}
className={cx(documentContainerStyles, previewStyles)}
>
<div className={documentStyles}>
<ReadonlyDocument doc={doc as any} expandAll={false} />
</div>
</KeylineCard>
);
})}
</div>
);

return (
<Modal setOpen={onCancel} open={open}>
<ModalHeader
title={`Delete ${documentCount} documents`}
subtitle={namespace}
variant={'danger'}
/>
<ModalBody variant={'danger'} className={modalBodySpacingStyles}>
<TextInput
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore the label can be any component, but it's weirdly typed to string
label={
<QueryLabel
label="Query"
tooltip="Return to the Documents tab to edit this query."
/>
}
disabled={true}
value={filterQuery}
/>
<div>
<b>Preview (sample of {sampleDocuments.length} documents)</b>
{preview}
</div>
</ModalBody>
<ModalFooter className={modalFooterSpacingStyles}>
<Button variant="danger" onClick={onConfirmDeletion}>
Delete {documentCount} documents
</Button>
<Button variant="default" onClick={onCancel}>
Close
</Button>
</ModalFooter>
</Modal>
);
};

export default BulkDeleteModal;
43 changes: 34 additions & 9 deletions packages/compass-crud/src/components/document-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { DocumentTableViewProps } from './table-view/document-table-view';
import DocumentTableView from './table-view/document-table-view';
import type { CrudToolbarProps } from './crud-toolbar';
import { CrudToolbar } from './crud-toolbar';
import { toJSString } from 'mongodb-query-parser';

import type { DOCUMENTS_STATUSES } from '../constants/documents-statuses';
import {
Expand All @@ -37,8 +38,8 @@ import type {
DocumentView,
QueryState,
} from '../stores/crud-store';
import type Document from 'hadron-document';
import { getToolbarSignal } from '../utils/toolbar-signal';
import BulkDeleteModal from './bulk-delete-modal';

const listAndJsonStyles = css({
padding: spacing[3],
Expand Down Expand Up @@ -147,13 +148,6 @@ class DocumentList extends React.Component<DocumentListProps> {
}
}

/**
* Handle opening the delete bulk dialog.
*/
handleDeleteButton() {
return;
}

/**
* Render the views for the document list.
*
Expand Down Expand Up @@ -242,6 +236,36 @@ class DocumentList extends React.Component<DocumentListProps> {
}
}

onOpenBulkDeleteDialog() {
this.props.store.openBulkDeleteDialog();
}

onCancelBulkDeleteDialog() {
this.props.store.closeBulkDeleteDialog();
}

onConfirmBulkDeleteDialog() {
void this.props.store.runBulkDelete();
}

/**
* Render the bulk deletion modal
*/
renderDeletionModal() {
return (
<BulkDeleteModal
open={this.props.store.state.bulkDelete.status === 'open'}
namespace={this.props.store.state.ns}
documentCount={this.props.store.state.bulkDelete.affected || 0}
filterQuery={toJSString(this.props.store.state.query.filter) || '{}'}
onCancel={this.onCancelBulkDeleteDialog.bind(this)}
onConfirmDeletion={this.onConfirmBulkDeleteDialog.bind(this)}
sampleDocuments={
this.props.store.state.bulkDelete.previews as any as Document[]
}
/>
);
}
/**
* Render EmptyContent view when no documents are present.
*
Expand Down Expand Up @@ -322,7 +346,7 @@ class DocumentList extends React.Component<DocumentListProps> {
isExportable={this.props.isExportable}
onApplyClicked={this.onApplyClicked.bind(this)}
onResetClicked={this.onResetClicked.bind(this)}
onDeleteButtonClicked={this.handleDeleteButton.bind(this)}
onDeleteButtonClicked={this.onOpenBulkDeleteDialog.bind(this)}
openExportFileDialog={this.props.openExportFileDialog}
outdated={this.props.outdated}
readonly={!this.props.isEditable}
Expand All @@ -348,6 +372,7 @@ class DocumentList extends React.Component<DocumentListProps> {
{this.renderZeroState()}
{this.renderContent()}
{this.renderInsertModal()}
{this.renderDeletionModal()}
</WorkspaceContainer>
</div>
);
Expand Down
Loading

0 comments on commit b296a11

Please sign in to comment.