Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
Expand All @@ -33,31 +33,61 @@ import Header from './Header';

// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Header', () => {
const props = {
interface HeaderTestProps {
id: string;
dashboardId: string;
parentId: string;
component: any;
depth: number;
parentComponent: any;
index: number;
editMode: boolean;
embeddedMode: boolean;
filters: Record<string, any>;
handleComponentDrop: () => void;
deleteComponent: sinon.SinonSpy;
updateComponents: sinon.SinonSpy;
}

const baseComponent = newComponentFactory(HEADER_TYPE);
const props: HeaderTestProps = {
id: 'id',
dashboardId: '1',
parentId: 'parentId',
component: newComponentFactory(HEADER_TYPE),
component: {
...baseComponent,
id: 'id',
meta: {
...(baseComponent.meta || {}),
text: 'New Title',
},
},
depth: 1,
parentComponent: newComponentFactory(DASHBOARD_GRID_TYPE),
index: 0,
editMode: false,
embeddedMode: false,
filters: {},
handleComponentDrop() {},
deleteComponent() {},
updateComponents() {},
handleComponentDrop: () => {},
deleteComponent: sinon.spy(),
updateComponents: sinon.spy(),
};

function setup(overrideProps) {
function setup(overrideProps: Partial<HeaderTestProps> = {}) {
return render(
<Provider store={mockStoreWithTabs}>
<DndProvider backend={HTML5Backend}>
<Header {...props} {...overrideProps} />
<Header {...(props as HeaderTestProps)} {...overrideProps} />
</DndProvider>
</Provider>,
);
}

beforeEach(() => {
if (props.deleteComponent) props.deleteComponent.resetHistory();
if (props.updateComponents) props.updateComponents.resetHistory();
});

test('should render a Draggable', () => {
setup();
expect(screen.getByTestId('dragdroppable-object')).toBeInTheDocument();
Expand Down Expand Up @@ -97,11 +127,13 @@ describe('Header', () => {
fireEvent.change(titleInput, { target: { value: 'New title' } });
fireEvent.blur(titleInput);

const headerId = props.component.id;
const headerId = props.id;
expect(updateComponents.callCount).toBe(1);
expect(updateComponents.getCall(0).args[0][headerId].meta.text).toBe(
'New title',
);
const componentUpdates = updateComponents.getCall(0).args[0] as Record<
string,
any
>;
expect(componentUpdates[headerId].meta.text).toBe('New title');
});

test('should render a DeleteComponentButton when focused in editMode', () => {
Expand All @@ -114,8 +146,8 @@ describe('Header', () => {
const deleteComponent = sinon.spy();
setup({ editMode: true, deleteComponent });

const trashButton = screen.getByRole('img', { name: 'delete' });
fireEvent.click(trashButton.parentElement);
const trashButton = screen.getByRole('button', { name: 'delete' });
fireEvent.click(trashButton);

expect(deleteComponent.callCount).toBe(1);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
Expand All @@ -17,7 +17,6 @@
* under the License.
*/
import { PureComponent } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { css, styled } from '@apache-superset/core/ui';

Expand All @@ -32,30 +31,63 @@ import BackgroundStyleDropdown from 'src/dashboard/components/menu/BackgroundSty
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
import headerStyleOptions from 'src/dashboard/util/headerStyleOptions';
import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
import { componentShape } from 'src/dashboard/util/propShapes';
import {
SMALL_HEADER,
BACKGROUND_TRANSPARENT,
MEDIUM_HEADER,
LARGE_HEADER,
BACKGROUND_WHITE,
} from 'src/dashboard/util/constants';
import * as componentTypes from 'src/dashboard/util/componentTypes';

const propTypes = {
id: PropTypes.string.isRequired,
dashboardId: PropTypes.string.isRequired,
parentId: PropTypes.string.isRequired,
component: componentShape.isRequired,
depth: PropTypes.number.isRequired,
parentComponent: componentShape.isRequired,
index: PropTypes.number.isRequired,
editMode: PropTypes.bool.isRequired,
embeddedMode: PropTypes.bool.isRequired,
export type ComponentType =
(typeof componentTypes)[keyof typeof componentTypes];

// redux
handleComponentDrop: PropTypes.func.isRequired,
deleteComponent: PropTypes.func.isRequired,
updateComponents: PropTypes.func.isRequired,
};
export type HeaderStyleValue =
| typeof SMALL_HEADER
| typeof MEDIUM_HEADER
| typeof LARGE_HEADER;

const defaultProps = {};
export type BackgroundStyleValue =
| typeof BACKGROUND_TRANSPARENT
| typeof BACKGROUND_WHITE;

export interface ComponentMeta {
width?: number;
height?: number;
text: string;
headerSize?: HeaderStyleValue;
background?: BackgroundStyleValue;
chartId?: number;
[key: string]: any;
}

export interface ComponentShape {
id: string;
type: ComponentType;
parents?: string[];
children?: string[];
meta: ComponentMeta;
}

interface HeaderProps {
id: string;
dashboardId: string;
parentId: string;
component: ComponentShape;
depth: number;
parentComponent: ComponentShape;
index: number;
editMode: boolean;
embeddedMode: boolean;
handleComponentDrop: (dropResult: any) => void;
deleteComponent: (id: string, parentId: string) => void;
updateComponents: (changes: Record<string, ComponentShape>) => void;
}

interface HeaderState {
isFocused: boolean;
}

const HeaderStyles = styled.div`
${({ theme }) => css`
Expand Down Expand Up @@ -127,28 +159,33 @@ const HeaderStyles = styled.div`
`}
`;

class Header extends PureComponent {
constructor(props) {
class Header extends PureComponent<HeaderProps, HeaderState> {
handleChangeSize: (nextValue: string) => void;
handleChangeBackground: (nextValue: string) => void;
handleChangeText: (nextValue: string) => void;

constructor(props: HeaderProps) {
super(props);
this.state = {
isFocused: false,
};
this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
this.handleChangeFocus = this.handleChangeFocus.bind(this);
this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
this.handleChangeSize = this.handleUpdateMeta.bind(this, 'headerSize');
this.handleChangeBackground = this.handleUpdateMeta.bind(
this,
'background',
);
this.handleChangeText = this.handleUpdateMeta.bind(this, 'text');

this.handleChangeSize = (nextValue: string) =>
this.handleUpdateMeta('headerSize', nextValue);
this.handleChangeBackground = (nextValue: string) =>
this.handleUpdateMeta('background', nextValue);
this.handleChangeText = (nextValue: string) =>
this.handleUpdateMeta('text', nextValue);
}

handleChangeFocus(nextFocus) {
handleChangeFocus(nextFocus: boolean): void {
this.setState(() => ({ isFocused: nextFocus }));
}

handleUpdateMeta(metaKey, nextValue) {
handleUpdateMeta(metaKey: keyof ComponentMeta, nextValue: string): void {
const { updateComponents, component } = this.props;
if (nextValue && component.meta[metaKey] !== nextValue) {
updateComponents({
Expand All @@ -159,11 +196,11 @@ class Header extends PureComponent {
[metaKey]: nextValue,
},
},
});
} as Record<string, ComponentShape>);
}
}

handleDeleteComponent() {
handleDeleteComponent(): void {
const { deleteComponent, id, parentId } = this.props;
deleteComponent(id, parentId);
}
Expand Down Expand Up @@ -202,7 +239,11 @@ class Header extends PureComponent {
disableDragDrop={isFocused}
editMode={editMode}
>
{({ dragSourceRef }) => (
{({
dragSourceRef,
}: {
dragSourceRef: React.Ref<HTMLDivElement> | undefined;
}) => (
<div ref={dragSourceRef}>
{editMode &&
depth <= 2 && ( // drag handle looks bad when nested
Expand All @@ -216,12 +257,12 @@ class Header extends PureComponent {
<PopoverDropdown
id={`${component.id}-header-style`}
options={headerStyleOptions}
value={component.meta.headerSize}
value={component.meta.headerSize as string}
onChange={this.handleChangeSize}
/>,
<BackgroundStyleDropdown
id={`${component.id}-background`}
value={component.meta.background}
value={component.meta.background as string}
onChange={this.handleChangeBackground}
/>,
]}
Expand All @@ -231,8 +272,8 @@ class Header extends PureComponent {
className={cx(
'dashboard-component',
'dashboard-component-header',
headerStyle.className,
rowStyle.className,
headerStyle?.className,
rowStyle?.className,
)}
>
{editMode && (
Expand All @@ -249,7 +290,10 @@ class Header extends PureComponent {
showTooltip={false}
/>
{!editMode && !embeddedMode && (
<AnchorLink id={component.id} dashboardId={dashboardId} />
<AnchorLink
id={component.id}
dashboardId={Number(dashboardId)}
/>
)}
</HeaderStyles>
</WithPopoverMenu>
Expand All @@ -260,7 +304,4 @@ class Header extends PureComponent {
}
}

Header.propTypes = propTypes;
Header.defaultProps = defaultProps;

export default Header;