Skip to content
Snippets Groups Projects
Unverified Commit 426da4a3 authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Update collection empty state (#23406)

parent 4cf62603
No related branches found
No related tags found
No related merge requests found
Showing
with 341 additions and 380 deletions
......@@ -74,12 +74,14 @@ import {
ALERT_TYPE_TIMESERIES_GOAL,
} from "metabase-lib/lib/Alert";
import { utf8_to_b64url } from "metabase/lib/encoding";
import { CollectionId } from "metabase-types/api";
type QuestionUpdateFn = (q: Question) => Promise<void> | null | undefined;
export type QuestionCreatorOpts = {
databaseId?: DatabaseId;
tableId?: TableId;
collectionId?: CollectionId;
metadata?: Metadata;
parameterValues?: ParameterValues;
type?: "query" | "native";
......
import { NativePermissions } from "./permissions";
export type DatabaseId = number;
export type InitialSyncStatus = "incomplete" | "complete" | "aborted";
......@@ -7,8 +9,10 @@ export interface Database {
name: string;
engine: string;
is_sample: boolean;
is_saved_questions: boolean;
creator_id?: number;
created_at: string;
timezone?: string;
native_permissions: NativePermissions;
initial_sync_status: InitialSyncStatus;
}
......@@ -5,9 +5,11 @@ export const createMockDatabase = (opts?: Partial<Database>): Database => ({
name: "Database",
engine: "H2",
is_sample: false,
is_saved_questions: false,
creator_id: undefined,
created_at: "2015-01-01T20:10:30.200",
timezone: "UTC",
native_permissions: "write",
initial_sync_status: "complete",
...opts,
});
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
export const EmptyStateRoot = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`;
export const EmptyStateTitle = styled.div`
color: ${color("text-dark")};
font-size: 1.5rem;
font-weight: bold;
line-height: 2rem;
margin-top: 2.5rem;
margin-bottom: 0.75rem;
`;
export const EmptyStateDescription = styled.div`
color: ${color("text-medium")};
font-size: 1rem;
line-height: 1.5rem;
margin-bottom: 1.5rem;
text-align: center;
`;
export const EmptyStateIconForeground = styled.path`
fill: ${color("bg-light")};
stroke: ${color("brand")};
`;
export const EmptyStateIconBackground = styled.path`
fill: ${color("brand-light")};
stroke: ${color("brand")};
`;
import React from "react";
import { t } from "ttag";
import Button from "metabase/core/components/Button";
import NewItemMenu from "metabase/containers/NewItemMenu";
import { ANALYTICS_CONTEXT } from "metabase/collections/constants";
import {
EmptyStateDescription,
EmptyStateIconBackground,
EmptyStateIconForeground,
EmptyStateRoot,
EmptyStateTitle,
} from "./CollectionEmptyState.styled";
const CollectionEmptyState = (): JSX.Element => {
return (
<EmptyStateRoot data-testid="collection-empty-state">
<CollectionEmptyIcon />
<EmptyStateTitle>{t`This collection is empty`}</EmptyStateTitle>
<EmptyStateDescription>{t`Use collections to organize and group dashboards and questions for your team or yourself`}</EmptyStateDescription>
<NewItemMenu
trigger={<Button icon="add">{t`Create a new…`}</Button>}
analyticsContext={ANALYTICS_CONTEXT}
/>
</EmptyStateRoot>
);
};
const CollectionEmptyIcon = (): JSX.Element => {
return (
<svg width="117" height="94" fill="none" xmlns="http://www.w3.org/2000/svg">
<EmptyStateIconForeground
fillRule="evenodd"
clipRule="evenodd"
d="M12.5 1C6.148 1 .995 6.151 1 12.505l.023 69C1.029 87.854 6.175 93 12.523 93H104.5C110.853 93 116 87.851 116 81.5V22.196c0-6.352-5.147-11.5-11.501-11.5H65.357a5.752 5.752 0 0 1-5.307-3.533l-1.099-2.63A5.752 5.752 0 0 0 53.644 1H12.5Z"
strokeWidth="2"
/>
<EmptyStateIconBackground
d="M1 13C1 6.373 6.373 1 13 1h39.76a8 8 0 0 1 7.017 4.157l.446.815a8 8 0 0 0 7.017 4.158H107a9 9 0 0 1 9 9V26l-2.714-3.137a16.003 16.003 0 0 0-12.099-5.53H15.383a16 16 0 0 0-13.155 6.893L1 26V13Z"
strokeWidth="2"
/>
</svg>
);
};
export default CollectionEmptyState;
export { default } from "./CollectionEmptyState";
......@@ -10,10 +10,11 @@ import PageHeading from "metabase/components/type/PageHeading";
import Tooltip from "metabase/components/Tooltip";
import CollectionEditMenu from "metabase/collections/components/CollectionEditMenu";
import NewCollectionItemMenu from "metabase/collections/components/NewCollectionItemMenu";
import NewItemMenu from "metabase/containers/NewItemMenu";
import { color } from "metabase/lib/colors";
import { PLUGIN_COLLECTION_COMPONENTS } from "metabase/plugins";
import { ANALYTICS_CONTEXT } from "metabase/collections/constants";
import {
BookmarkIcon,
......@@ -140,7 +141,15 @@ function Menu(props) {
return (
<MenuContainer data-testid="collection-menu">
{hasWritePermission && <NewCollectionItemMenu {...props} />}
{hasWritePermission && (
<NewItemMenu
{...props}
collectionId={collectionId}
triggerIcon="add"
triggerTooltip={t`New…`}
analyticsContext={ANALYTICS_CONTEXT}
/>
)}
<EditMenu {...props} />
<PermissionsLink {...props} />
<TimelinesLink {...props} />
......
import React from "react";
import { render, screen } from "@testing-library/react";
import { screen } from "@testing-library/react";
import { renderWithProviders } from "__support__/ui";
import Header from "./CollectionHeader";
const collection = {
......@@ -8,7 +8,7 @@ const collection = {
};
it("should display collection name", () => {
render(<Header collection={collection} />);
renderWithProviders(<Header collection={collection} />);
screen.getByText(collection.name);
});
......@@ -16,7 +16,9 @@ it("should display collection name", () => {
describe("description tooltip", () => {
describe("should not be displayed", () => {
it("if description is not received", () => {
const { container } = render(<Header collection={collection} />);
const { container } = renderWithProviders(
<Header collection={collection} />,
);
expect(container.textContent).toEqual("Name");
});
});
......@@ -25,7 +27,9 @@ describe("description tooltip", () => {
it("if description is received", () => {
const description = "description";
render(<Header collection={{ ...collection, description }} />);
renderWithProviders(
<Header collection={{ ...collection, description }} />,
);
screen.getByText(description);
});
......@@ -37,13 +41,13 @@ describe("permissions link", () => {
describe("should not be displayed", () => {
it("if user is not admin", () => {
render(<Header collection={collection} />);
renderWithProviders(<Header collection={collection} />);
expect(screen.queryByLabelText(ariaLabel)).not.toBeInTheDocument();
});
it("for personal collections", () => {
render(
renderWithProviders(
<Header
isAdmin={true}
collection={{ ...collection, personal_owner_id: 1 }}
......@@ -54,7 +58,7 @@ describe("permissions link", () => {
});
it("if a collection is a personal collection child", () => {
render(
renderWithProviders(
<Header
isAdmin={true}
collection={collection}
......@@ -68,7 +72,7 @@ describe("permissions link", () => {
describe("should be displayed", () => {
it("if user is admin", () => {
render(<Header collection={collection} isAdmin={true} />);
renderWithProviders(<Header collection={collection} isAdmin={true} />);
screen.getByLabelText(ariaLabel);
});
......@@ -80,13 +84,15 @@ describe("link to add new collection items", () => {
describe("should not be displayed", () => {
it("when no detail is passed in the collection to determine if user can change collection", () => {
render(<Header collection={collection} />);
renderWithProviders(<Header collection={collection} />);
expect(screen.queryByLabelText(ariaLabel)).not.toBeInTheDocument();
});
it("if user is not allowed to change collection", () => {
render(<Header collection={{ ...collection, can_write: false }} />);
renderWithProviders(
<Header collection={{ ...collection, can_write: false }} />,
);
expect(screen.queryByLabelText(ariaLabel)).not.toBeInTheDocument();
});
......@@ -94,7 +100,9 @@ describe("link to add new collection items", () => {
describe("should be displayed", () => {
it("if user is allowed to change collection", () => {
render(<Header collection={{ ...collection, can_write: true }} />);
renderWithProviders(
<Header collection={{ ...collection, can_write: true }} />,
);
screen.getByLabelText(ariaLabel);
});
......@@ -106,7 +114,7 @@ describe("link to add new collection items", () => {
describe("should not be displayed", () => {
it("if user is not allowed to change collection", () => {
render(<Header collection={collection} />);
renderWithProviders(<Header collection={collection} />);
expect(screen.queryByLabelText(ariaLabel)).not.toBeInTheDocument();
});
......@@ -114,7 +122,9 @@ describe("link to add new collection items", () => {
describe("should be displayed", () => {
it("if user is allowed to change collection", () => {
render(<Header collection={{ ...collection, can_write: true }} />);
renderWithProviders(
<Header collection={{ ...collection, can_write: true }} />,
);
screen.getByLabelText(ariaLabel);
});
......
import React, { ReactNode } from "react";
import { extractCollectionId } from "metabase/lib/urls";
import CollectionContent from "../../containers/CollectionContent";
export interface CollectionLandingProps {
params: CollectionLandingParams;
children?: ReactNode;
}
export interface CollectionLandingParams {
slug: string;
}
const CollectionLanding = ({
params: { slug },
children,
}: CollectionLandingProps) => {
const collectionId = extractCollectionId(slug);
const isRoot = collectionId === "root";
return (
<>
<CollectionContent isRoot={isRoot} collectionId={collectionId} />
{children}
</>
);
};
export default CollectionLanding;
export { default } from "./CollectionLanding";
import styled from "@emotion/styled";
export const PageWrapper = styled.div`
overflow: hidden;
`;
import React from "react";
import PropTypes from "prop-types";
import { t } from "ttag";
import { newQuestion, newDashboard, newCollection } from "metabase/lib/urls";
import EntityMenu from "metabase/components/EntityMenu";
import { ANALYTICS_CONTEXT } from "metabase/collections/constants";
const propTypes = {
collection: PropTypes.object,
list: PropTypes.arrayOf(PropTypes.object),
canCreateQuestions: PropTypes.bool,
};
function NewCollectionItemMenu({ collection, canCreateQuestions }) {
const items = [
canCreateQuestions && {
icon: "insight",
title: t`Question`,
link: newQuestion({ mode: "notebook", collectionId: collection.id }),
event: `${ANALYTICS_CONTEXT};New Item Menu;Question Click`,
},
{
icon: "dashboard",
title: t`Dashboard`,
link: newDashboard(collection.id),
event: `${ANALYTICS_CONTEXT};New Item Menu;Dashboard Click`,
},
{
icon: "folder",
title: t`Collection`,
link: newCollection(collection.id),
event: `${ANALYTICS_CONTEXT};New Item Menu;Collection Click`,
},
].filter(Boolean);
return <EntityMenu items={items} triggerIcon="add" tooltip={t`New…`} />;
}
NewCollectionItemMenu.propTypes = propTypes;
export default NewCollectionItemMenu;
......@@ -13,7 +13,7 @@ import { getIsBookmarked } from "metabase/collections/selectors";
import { getIsNavbarOpen, openNavbar } from "metabase/redux/app";
import BulkActions from "metabase/collections/components/BulkActions";
import CollectionEmptyState from "metabase/components/CollectionEmptyState";
import CollectionEmptyState from "metabase/collections/components/CollectionEmptyState";
import Header from "metabase/collections/containers/CollectionHeader";
import ItemsTable from "metabase/collections/components/ItemsTable";
import PinnedItemOverview from "metabase/collections/components/PinnedItemOverview";
......
......@@ -18,8 +18,5 @@ export const CollectionTable = styled.div<CollectionTableProps>`
`;
export const CollectionEmptyContent = styled.div`
display: flex;
justify-content: center;
align-items: start;
margin-top: 3rem;
margin-top: calc(20vh - 3.5rem);
`;
import React from "react";
function CollectionEmptyState() {
return (
<svg
style={{ maxWidth: 1120 }}
data-testid="collection-empty-state"
viewBox="0 0 944 672"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
opacity="0.7"
x="0.5"
y="0.5"
width="463"
height="215"
rx="7.5"
fill="white"
stroke="#F0F0F0"
/>
<rect
width="42"
height="200"
transform="matrix(-1 0 0 1 428 16)"
fill="#EDF2F5"
fillOpacity="0.5"
/>
<rect
width="42"
height="177"
transform="matrix(-1 0 0 1 378 39)"
fill="#EDF2F5"
fillOpacity="0.5"
/>
<rect
width="42"
height="127.941"
transform="matrix(-1 0 0 1 328 88.0588)"
fill="#EDF2F5"
fillOpacity="0.5"
/>
<rect
width="42"
height="83.8235"
transform="matrix(-1 0 0 1 278 132.176)"
fill="#EDF2F5"
fillOpacity="0.5"
/>
<rect
width="42"
height="41.1765"
transform="matrix(-1 0 0 1 228 174.824)"
fill="#EDF2F5"
fillOpacity="0.5"
/>
<rect
width="42"
height="147.059"
transform="matrix(-1 0 0 1 178 68.9412)"
fill="#EDF2F5"
fillOpacity="0.5"
/>
<rect
width="42"
height="110"
transform="matrix(-1 0 0 1 128 106)"
fill="#EDF2F5"
fillOpacity="0.5"
/>
<rect
width="42"
height="84"
transform="matrix(-1 0 0 1 78 132)"
fill="#EDF2F5"
fillOpacity="0.5"
/>
<rect
opacity="0.7"
x="483.049"
y="0.5"
width="460.451"
height="215"
rx="7.5"
fill="white"
stroke="#F0F0F0"
/>
<path
opacity="0.5"
d="M550.992 198.14L515.496 184.744L480 192.48V208C480 212.418 483.582 216 488 216H933.451C937.869 216 941.451 212.418 941.451 208V24L905.954 35.1628L870.458 59.7209L834.962 44.093L799.466 77.5814L763.97 59.7209L728.473 86.5116L692.977 104.372L657.481 77.5814L621.985 176.93L586.489 150.14L550.992 198.14Z"
fill="#EDF2F5"
/>
<path
d="M481.275 193.44L515.496 184.744L550.992 198.14L586.488 150.14L621.985 176.93L657.481 77.5814L692.977 104.372L728.473 86.5116L763.969 59.7209L799.466 77.5814L834.962 44.093L870.458 59.7209L905.954 35.1628L941.45 24"
stroke="#EDF2F5"
strokeWidth="2"
/>
<rect
opacity="0.7"
x="0.5"
y="232.5"
width="463"
height="127"
rx="7.5"
fill="white"
stroke="#F0F0F0"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M216 281.714C216 279.505 217.791 277.714 220 277.714H244H245C246.657 277.714 248 279.057 248 280.714V281.714V285.714V305.714C248 307.923 246.209 309.714 244 309.714H220C218.136 309.714 216.57 308.44 216.126 306.714H216V305.714V285.714V281.714ZM244 285.714V305.714H220V285.714H244ZM233.455 296.623H223.273V300.987H233.455V296.623ZM223.273 289.805H240.727V294.169H223.273V289.805ZM240.727 296.623H236.364V300.987H240.727V296.623Z"
fill="#EDF2F5"
/>
<rect
opacity="0.7"
x="480.5"
y="232.5"
width="463"
height="127"
rx="7.5"
fill="white"
stroke="#F0F0F0"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M696 281.714C696 279.505 697.791 277.714 700 277.714H724H725C726.657 277.714 728 279.057 728 280.714V281.714V285.714V305.714C728 307.923 726.209 309.714 724 309.714H700C698.136 309.714 696.57 308.44 696.126 306.714H696V305.714V285.714V281.714ZM724 285.714V305.714H700V285.714H724ZM713.455 296.623H703.273V300.987H713.455V296.623ZM703.273 289.805H720.727V294.169H703.273V289.805ZM720.727 296.623H716.364V300.987H720.727V296.623Z"
fill="#EDF2F5"
/>
<rect y="440" width="258.683" height="8" rx="4" fill="#EDF2F5" />
<rect
x="630.708"
y="440"
width="113.898"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect
x="830.102"
y="440"
width="103.28"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect y="472" width="402.503" height="8" rx="4" fill="#EDF2F5" />
<rect
x="630.708"
y="472"
width="99.4192"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect
x="830.102"
y="472"
width="113.898"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect y="504" width="207.526" height="8" rx="4" fill="#EDF2F5" />
<rect
x="630.708"
y="504"
width="130.307"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect
x="830.102"
y="504"
width="89.7669"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect y="536" width="354.241" height="8" rx="4" fill="#EDF2F5" />
<rect
x="630.708"
y="536"
width="113.898"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect
x="830.102"
y="536"
width="103.28"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect y="568" width="370.65" height="8" rx="4" fill="#EDF2F5" />
<rect
x="630.708"
y="568"
width="106.176"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect
x="830.102"
y="568"
width="89.7669"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect y="600" width="233.587" height="8" rx="4" fill="#EDF2F5" />
<rect
x="630.708"
y="600"
width="138.029"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect
x="830.102"
y="600"
width="103.28"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect y="632" width="284.744" height="8" rx="4" fill="#EDF2F5" />
<rect
x="630.708"
y="632"
width="106.176"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect
x="830.102"
y="632"
width="103.28"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect y="664" width="258.683" height="8" rx="4" fill="#EDF2F5" />
<rect
x="630.708"
y="664"
width="113.898"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect
x="830.102"
y="664"
width="113.898"
height="8"
rx="4"
fill="#EDF2F5"
/>
<rect y="408" width="50.1239" height="8" rx="4" fill="#EDF2F5" />
<rect x="631" y="408" width="73.515" height="8" rx="4" fill="#EDF2F5" />
<rect x="830" y="408" width="60.9841" height="8" rx="4" fill="#EDF2F5" />
</svg>
);
}
export default CollectionEmptyState;
/* eslint-disable react/prop-types */
import React from "react";
import { extractCollectionId } from "metabase/lib/urls";
import { PageWrapper } from "metabase/collections/components/Layout";
import CollectionContent from "metabase/collections/containers/CollectionContent";
import { ContentBox } from "./CollectionLanding.styled";
const CollectionLanding = ({ params: { slug }, children }) => {
const collectionId = extractCollectionId(slug);
const isRoot = collectionId === "root";
return (
<PageWrapper>
<ContentBox>
<CollectionContent isRoot={isRoot} collectionId={collectionId} />
</ContentBox>
{
// Need to have this here so the child modals will show up
children
}
</PageWrapper>
);
};
export default CollectionLanding;
import styled from "@emotion/styled";
import { breakpointMinSmall } from "metabase/styled-components/theme/media-queries";
export const ContentBox = styled.div`
overflow-y: auto;
${breakpointMinSmall} {
display: block;
}
`;
import React, { ReactNode, useCallback, useMemo, useState } from "react";
import { t } from "ttag";
import * as Urls from "metabase/lib/urls";
import Modal from "metabase/components/Modal";
import EntityMenu from "metabase/components/EntityMenu";
import CreateDashboardModal from "metabase/components/CreateDashboardModal";
import CollectionCreate from "metabase/collections/containers/CollectionCreate";
import { Collection, CollectionId } from "metabase-types/api";
type ModalType = "new-dashboard" | "new-collection";
export interface NewItemMenuProps {
className?: string;
collectionId?: CollectionId;
trigger?: ReactNode;
triggerIcon?: string;
triggerTooltip?: string;
analyticsContext?: string;
hasDataAccess: boolean;
hasNativeWrite: boolean;
hasDatabaseWithJsonEngine: boolean;
onChangeLocation: (location: string) => void;
onCloseNavbar: () => void;
}
const NewItemMenu = ({
className,
collectionId,
trigger,
triggerIcon,
triggerTooltip,
analyticsContext,
hasDataAccess,
hasNativeWrite,
hasDatabaseWithJsonEngine,
onChangeLocation,
onCloseNavbar,
}: NewItemMenuProps) => {
const [modal, setModal] = useState<ModalType>();
const handleModalClose = useCallback(() => {
setModal(undefined);
}, []);
const handleCollectionSave = useCallback(
(collection: Collection) => {
handleModalClose();
onChangeLocation(Urls.collection(collection));
},
[handleModalClose, onChangeLocation],
);
const menuItems = useMemo(() => {
const items = [];
if (hasDataAccess) {
items.push({
title: t`Question`,
icon: "insight",
link: Urls.newQuestion({
mode: "notebook",
creationType: "custom_question",
collectionId,
}),
event: `${analyticsContext};New Question Click;`,
onClose: onCloseNavbar,
});
}
if (hasNativeWrite) {
items.push({
title: hasDatabaseWithJsonEngine ? t`Native query` : t`SQL query`,
icon: "sql",
link: Urls.newQuestion({
type: "native",
creationType: "native_question",
collectionId,
}),
event: `${analyticsContext};New SQL Query Click;`,
onClose: onCloseNavbar,
});
}
items.push(
{
title: t`Dashboard`,
icon: "dashboard",
action: () => setModal("new-dashboard"),
event: `${analyticsContext};New Dashboard Click;`,
},
{
title: t`Collection`,
icon: "folder",
action: () => setModal("new-collection"),
event: `${analyticsContext};New Collection Click;`,
},
);
return items;
}, [
collectionId,
hasDataAccess,
hasNativeWrite,
hasDatabaseWithJsonEngine,
analyticsContext,
onCloseNavbar,
]);
return (
<>
<EntityMenu
className={className}
items={menuItems}
trigger={trigger}
triggerIcon={triggerIcon}
tooltip={triggerTooltip}
/>
{modal && (
<Modal onClose={handleModalClose}>
{modal === "new-collection" ? (
<CollectionCreate
collectionId={collectionId}
onClose={handleModalClose}
onSaved={handleCollectionSave}
/>
) : modal === "new-dashboard" ? (
<CreateDashboardModal
collectionId={collectionId}
onClose={handleModalClose}
/>
) : null}
</Modal>
)}
</>
);
};
export default NewItemMenu;
export { default } from "./NewItemMenu";
import { ReactNode } from "react";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import { closeNavbar } from "metabase/redux/app";
import NewItemMenu from "metabase/components/NewItemMenu";
import {
getHasDataAccess,
getHasDatabaseWithJsonEngine,
getHasNativeWrite,
} from "metabase/nav/selectors";
import { State } from "metabase-types/store";
interface MenuOwnProps {
className?: string;
trigger?: ReactNode;
triggerIcon?: string;
triggerTooltip?: string;
analyticsContext?: string;
}
interface MenuStateProps {
hasDataAccess: boolean;
hasNativeWrite: boolean;
hasDatabaseWithJsonEngine: boolean;
}
interface MenuDispatchProps {
onChangeLocation: (location: string) => void;
onCloseNavbar: () => void;
}
const mapStateToProps = (state: State): MenuStateProps => ({
hasDataAccess: getHasDataAccess(state),
hasNativeWrite: getHasNativeWrite(state),
hasDatabaseWithJsonEngine: getHasDatabaseWithJsonEngine(state),
});
const mapDispatchToProps = {
onChangeLocation: push,
onCloseNavbar: closeNavbar,
};
export default connect<MenuStateProps, MenuDispatchProps, MenuOwnProps, State>(
mapStateToProps,
mapDispatchToProps,
)(NewItemMenu);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment