Skip to content

Commit

Permalink
feat(compass-aggregation): adds drag-n-drop for use-cases available i…
Browse files Browse the repository at this point in the history
…n sidepanel (#4374)

* moved the markup around to shift Sidepanel into ui workspace

* refactored UseCaseCard into presentation and interactive components for DragOverlay

* moved the dnd context to ui workspace and added drop markers for use-cases

* fixes for broken tests

* minor restructuring

* fixes for broken tests

* added tracking and minor refactor

* PR feedback - refactored add stage into a separate component with drop marker and fixed the broken test

* PR feedback - rename stage-separator/stage-separator to stage-separator/index

* PR feedback - height of marker is reduced to 1px

* applies feedback from PR and fixes the glitch in sorting

* minor refactor

* PR feedback

* PR feedback - better props for usecase card layout

---------

Co-authored-by: Basit <1305718+mabaasit@users.noreply.github.com>
  • Loading branch information
himanshusinghs and mabaasit committed May 19, 2023
1 parent 898b871 commit 0e57319
Show file tree
Hide file tree
Showing 12 changed files with 467 additions and 233 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { type ComponentProps } from 'react';
import sinon from 'sinon';
import { expect } from 'chai';
import { AddStage } from './add-stage';
import { render, screen, within } from '@testing-library/react';
import { cleanup, render, screen, within } from '@testing-library/react';

const renderAddStage = (
props: Partial<ComponentProps<typeof AddStage>> = {}
Expand All @@ -11,6 +11,8 @@ const renderAddStage = (
};

describe('AddStage', function () {
afterEach(cleanup);

context('add stage icon button', function () {
it('renders icon button', function () {
renderAddStage({ variant: 'icon' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,23 @@ import {
} from '@mongodb-js/compass-components';
import { PIPELINE_HELP_URI } from '../../constants';

const iconContainerStyles = css({
textAlign: 'center',
marginTop: spacing[1] / 2,
marginBottom: spacing[1] / 2,
});

const buttonContainerStyles = css({
textAlign: 'center',
marginTop: spacing[4],
marginBottom: spacing[3],
});
const containerStyles = css({ textAlign: 'center' });

const linkContainerStyles = css({
textAlign: 'center',
marginTop: spacing[2],
marginBottom: spacing[2],
marginBottom: spacing[5],
position: 'relative',
});

type AddStageProps = {
export type AddStageProps = {
variant: 'button' | 'icon';
onAddStage: () => void;
};

export const AddStage = ({ onAddStage, variant }: AddStageProps) => {
if (variant === 'icon') {
return (
<div className={iconContainerStyles}>
return (
<div className={containerStyles}>
{variant === 'icon' ? (
<IconButton
aria-label="Add stage"
title="Add stage"
Expand All @@ -44,26 +34,24 @@ export const AddStage = ({ onAddStage, variant }: AddStageProps) => {
>
<Icon glyph="PlusWithCircle"></Icon>
</IconButton>
</div>
);
}

return (
<div className={buttonContainerStyles}>
<Button
data-testid="add-stage"
onClick={() => onAddStage()}
variant="primary"
leftGlyph={<Icon glyph="Plus"></Icon>}
>
Add Stage
</Button>

<div className={linkContainerStyles}>
<Link href={PIPELINE_HELP_URI}>
Learn more about aggregation pipeline stages
</Link>
</div>
) : (
<>
<Button
data-testid="add-stage"
onClick={() => onAddStage()}
variant="primary"
leftGlyph={<Icon glyph="Plus"></Icon>}
>
Add Stage
</Button>

<div className={linkContainerStyles}>
<Link href={PIPELINE_HELP_URI}>
Learn more about aggregation pipeline stages
</Link>
</div>
</>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ import {

import { getStageHelpLink } from '../../../utils/stage';
import type { StageWizardUseCase } from '.';
import { useDraggable } from '@dnd-kit/core';

export type DraggedUseCase = Pick<
StageWizardUseCase,
'id' | 'title' | 'stageOperator'
>;

type UseCaseCardProps = DraggedUseCase & {
onSelect: () => void;
};

type UseCaseCardLayoutProps = DraggedUseCase & {
onClick?: () => void;
};

const cardStyles = css({
cursor: 'pointer',
Expand All @@ -20,28 +34,54 @@ const cardTitleStyles = css({
marginRight: spacing[2],
});

type UseCaseCardProps = {
onSelect: () => void;
} & Pick<StageWizardUseCase, 'id' | 'title' | 'stageOperator'>;

const UseCaseCard = ({
id,
title,
stageOperator,
onSelect,
}: UseCaseCardProps) => {
export const UseCaseCardLayout = React.forwardRef(function UseCaseCardLayout(
{ id, title, stageOperator, ...props }: UseCaseCardLayoutProps,
ref: React.ForwardedRef<HTMLDivElement>
) {
return (
<KeylineCard
ref={ref}
data-testid={`use-case-${id}`}
onClick={onSelect}
className={cardStyles}
{...props}
>
<Body className={cardTitleStyles}>{title}</Body>
<Link target="_blank" href={getStageHelpLink(stageOperator) as string}>
{stageOperator}
</Link>
</KeylineCard>
);
});

const UseCaseCard = ({
id,
title,
stageOperator,
onSelect,
}: UseCaseCardProps) => {
const { setNodeRef, attributes, listeners } = useDraggable({
id,
data: {
type: 'use-case',
draggedUseCase: {
id,
title,
stageOperator,
},
},
});

return (
<UseCaseCardLayout
id={id}
title={title}
stageOperator={stageOperator}
onClick={onSelect}
ref={setNodeRef}
{...attributes}
{...listeners}
/>
);
};

export default UseCaseCard;
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import React from 'react';
import { Provider } from 'react-redux';
import type { ComponentProps } from 'react';
import { render, screen, within } from '@testing-library/react';
import { cleanup, render, screen, within } from '@testing-library/react';
import { expect } from 'chai';
import configureStore from '../../../test/configure-store';
import { PipelineBuilderWorkspace } from '.';
import { toggleSidePanel } from '../../modules/side-panel';

const renderBuilderWorkspace = (
props: Partial<ComponentProps<typeof PipelineBuilderWorkspace>> = {}
) => {
return render(
<Provider store={configureStore()}>
<PipelineBuilderWorkspace
isPanelOpen={false}
pipelineMode="as-text"
{...props}
/>
const store = configureStore();
render(
<Provider store={store}>
<PipelineBuilderWorkspace pipelineMode="as-text" {...props} />
</Provider>
);
return store;
};

describe('PipelineBuilderWorkspace', function () {
afterEach(cleanup);

it('renders builder ui workspace', function () {
renderBuilderWorkspace({ pipelineMode: 'builder-ui' });
const container = screen.getByTestId('pipeline-builder-workspace');
Expand All @@ -36,13 +37,17 @@ describe('PipelineBuilderWorkspace', function () {
});

it('renders side panel when enabled in builder ui mode', function () {
renderBuilderWorkspace({ pipelineMode: 'builder-ui', isPanelOpen: true });
const store = renderBuilderWorkspace({ pipelineMode: 'builder-ui' });
store.dispatch(toggleSidePanel());
const container = screen.getByTestId('pipeline-builder-workspace');
expect(within(container).getByTestId('aggregation-side-panel')).to.exist;
expect(() => {
within(container).getByTestId('aggregation-side-panel');
}).to.not.throw;
});

it('does not render side panel when enabled in as text mode', function () {
renderBuilderWorkspace({ pipelineMode: 'as-text', isPanelOpen: true });
const store = renderBuilderWorkspace({ pipelineMode: 'as-text' });
store.dispatch(toggleSidePanel());
const container = screen.getByTestId('pipeline-builder-workspace');
expect(() => {
within(container).getByTestId('aggregation-side-panel');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import type { RootState } from '../../modules';
import type { PipelineMode } from '../../modules/pipeline-builder/pipeline-mode';
import PipelineBuilderUIWorkspace from './pipeline-builder-ui-workspace';
import PipelineAsTextWorkspace from './pipeline-as-text-workspace';
import AggregationSidePanel from '../aggregation-side-panel';
import ResizeHandle from '../resize-handle';
import { Resizable } from 're-resizable';

const containerStyles = css({
display: 'flex',
Expand All @@ -18,68 +15,26 @@ const containerStyles = css({
height: '100%',
});

const workspaceStyles = css({
paddingBottom: spacing[3],
width: '100%',
overflow: 'auto',
});

type PipelineBuilderWorkspaceProps = {
pipelineMode: PipelineMode;
isPanelOpen: boolean;
};

export const PipelineBuilderWorkspace: React.FunctionComponent<
PipelineBuilderWorkspaceProps
> = ({ pipelineMode, isPanelOpen }) => {
const workspace =
pipelineMode === 'builder-ui' ? (
<PipelineBuilderUIWorkspace />
) : (
<PipelineAsTextWorkspace />
);

const isSidePanelEnabled = isPanelOpen && pipelineMode === 'builder-ui';

> = ({ pipelineMode }) => {
return (
<div className={containerStyles} data-testid="pipeline-builder-workspace">
<div className={workspaceStyles}>{workspace}</div>
{isSidePanelEnabled && (
<Resizable
defaultSize={{
width: '25%',
height: 'auto',
}}
minWidth={'15%'}
maxWidth={'50%'}
enable={{
left: true,
}}
handleComponent={{
left: <ResizeHandle />,
}}
handleStyles={{
left: {
left: '-1px', // default is -5px
// The sidepanel container is a card with radius.
// Having padding top, cleans the UI.
paddingTop: spacing[2],
},
}}
>
<AggregationSidePanel />
</Resizable>
{pipelineMode === 'builder-ui' ? (
<PipelineBuilderUIWorkspace />
) : (
<PipelineAsTextWorkspace />
)}
</div>
);
};

const mapState = ({
pipelineBuilder: { pipelineMode },
sidePanel: { isPanelOpen },
}: RootState) => ({
const mapState = ({ pipelineBuilder: { pipelineMode } }: RootState) => ({
pipelineMode,
isPanelOpen,
});

export default connect(mapState)(PipelineBuilderWorkspace);
Loading

0 comments on commit 0e57319

Please sign in to comment.