Skip to content

Commit

Permalink
feat(tags): use MainToolbar in tags header MAASENG-2518 (#5273)
Browse files Browse the repository at this point in the history
Co-authored-by: Peter Makowski <peter.makowski@canonical.com>
  • Loading branch information
ndv99 and petermakowski committed Jan 11, 2024
1 parent b2e59a3 commit 681ca87
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 269 deletions.
176 changes: 168 additions & 8 deletions src/app/tags/components/TagsHeader/TagsHeader.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import TagsHeader from "./TagsHeader";
import { Route, Routes } from "react-router-dom-v5-compat";

import TagsHeader, { Label } from "./TagsHeader";

import urls from "app/base/urls";
import { TagSearchFilter } from "app/store/tag/selectors";
import { TagSidePanelViews } from "app/tags/constants";
import { rootState as rootStateFactory } from "testing/factories";
import {
rootState as rootStateFactory,
tag as tagFactory,
tagState as tagStateFactory,
} from "testing/factories";
import { renderWithBrowserRouter, screen, userEvent } from "testing/utils";

let scrollToSpy: jest.Mock;
Expand All @@ -16,10 +24,97 @@ afterEach(() => {
jest.restoreAllMocks();
});

it("displays the searchbox and group select when isDetails is false", () => {
renderWithBrowserRouter(
<TagsHeader
filter={TagSearchFilter.All}
isDetails={false}
onDelete={jest.fn()}
searchText=""
setFilter={jest.fn()}
setSearchText={jest.fn()}
setSidePanelContent={jest.fn()}
/>,
{
route: "/tags",
state: rootStateFactory(),
}
);

expect(screen.getByRole("searchbox", { name: "Search" })).toBeInTheDocument();
expect(screen.getByRole("tablist")).toBeInTheDocument();

expect(
screen.queryByRole("link", { name: /Back to all tags/i })
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: Label.DeleteButton })
).not.toBeInTheDocument();
expect(
screen.queryByRole("link", { name: Label.EditButton })
).not.toBeInTheDocument();
});

it("displays edit and delete buttons, and a return link when isDetails is true", () => {
const tag = tagFactory({ id: 1 });
const state = rootStateFactory({
tag: tagStateFactory({
loaded: true,
loading: false,
items: [tag],
}),
});
renderWithBrowserRouter(
<Routes>
<Route
element={
<TagsHeader
filter={TagSearchFilter.All}
isDetails={true}
onDelete={jest.fn()}
searchText=""
setFilter={jest.fn()}
setSearchText={jest.fn()}
setSidePanelContent={jest.fn()}
/>
}
path={urls.tags.tag.index(null)}
/>
</Routes>,
{
route: urls.tags.tag.index({ id: 1 }),
state,
}
);

expect(
screen.queryByRole("searchbox", { name: "Search" })
).not.toBeInTheDocument();
expect(screen.queryByRole("tablist")).not.toBeInTheDocument();

expect(
screen.getByRole("link", { name: /Back to all tags/i })
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: Label.DeleteButton })
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: Label.EditButton })
).toBeInTheDocument();
});

it("can call a function to display the add tag form", async () => {
const setSidePanelContent = jest.fn();
renderWithBrowserRouter(
<TagsHeader setSidePanelContent={setSidePanelContent} />,
<TagsHeader
filter={TagSearchFilter.All}
isDetails={false}
onDelete={jest.fn()}
searchText=""
setFilter={jest.fn()}
setSearchText={jest.fn()}
setSidePanelContent={setSidePanelContent}
/>,
{
route: "/tags",
state: rootStateFactory(),
Expand All @@ -33,12 +128,77 @@ it("can call a function to display the add tag form", async () => {
});

it("displays the default title", () => {
renderWithBrowserRouter(<TagsHeader setSidePanelContent={jest.fn()} />, {
route: "/tags",
state: rootStateFactory(),
});
renderWithBrowserRouter(
<TagsHeader
filter={TagSearchFilter.All}
isDetails={false}
onDelete={jest.fn()}
searchText=""
setFilter={jest.fn()}
setSearchText={jest.fn()}
setSidePanelContent={jest.fn()}
/>,
{
route: "/tags",
state: rootStateFactory(),
}
);
expect(
screen.getByRole("heading", { level: 1, name: "Tags" })
).toBeInTheDocument();
expect(screen.getByTestId("section-header-title").textContent).toBe("Tags");
expect(screen.getByTestId("main-toolbar-heading").textContent).toBe("Tags");
});

it("can update the filter", async () => {
const setFilter = jest.fn();
renderWithBrowserRouter(
<TagsHeader
filter={TagSearchFilter.All}
isDetails={false}
onDelete={jest.fn()}
searchText=""
setFilter={setFilter}
setSearchText={jest.fn()}
setSidePanelContent={jest.fn()}
/>,
{
route: "/tags",
state: rootStateFactory(),
}
);

await userEvent.click(screen.getByRole("tab", { name: Label.Manual }));
expect(setFilter).toHaveBeenCalledWith(TagSearchFilter.Manual);
});

it("can go to the tag edit page", async () => {
const tag = tagFactory({ id: 1 });
const state = rootStateFactory({
tag: tagStateFactory({
loaded: true,
loading: false,
items: [tag],
}),
});
renderWithBrowserRouter(
<Routes>
<Route
element={
<TagsHeader
filter={TagSearchFilter.All}
isDetails={true}
onDelete={jest.fn()}
searchText=""
setFilter={jest.fn()}
setSearchText={jest.fn()}
setSidePanelContent={jest.fn()}
/>
}
path={urls.tags.tag.index(null)}
/>
</Routes>,
{ route: urls.tags.tag.index({ id: 1 }), state }
);
await userEvent.click(screen.getByRole("link", { name: "Edit" }));
expect(window.location.pathname).toBe(urls.tags.tag.update({ id: 1 }));
});
134 changes: 111 additions & 23 deletions src/app/tags/components/TagsHeader/TagsHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,134 @@
import { Button } from "@canonical/react-components";
import { MainToolbar } from "@canonical/maas-react-components";
import { Button, Icon } from "@canonical/react-components";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom-v5-compat";

import MachinesHeader from "app/base/components/node/MachinesHeader";
import SearchBox from "app/base/components/SearchBox";
import SegmentedControl from "app/base/components/SegmentedControl";
import { useGetURLId } from "app/base/hooks";
import type { SetSidePanelContent } from "app/base/side-panel-context";
import { useFetchMachineCount } from "app/store/machine/utils/hooks";
import urls from "app/base/urls";
import type { RootState } from "app/store/root/types";
import tagSelectors, { TagSearchFilter } from "app/store/tag/selectors";
import type { Tag } from "app/store/tag/types";
import { TagMeta } from "app/store/tag/types";
import { TagSidePanelViews } from "app/tags/constants";
import { TagViewState } from "app/tags/types";

export type Props = {
filter: TagSearchFilter;
setFilter: (filter: TagSearchFilter) => void;
searchText: string;
setSearchText: (searchText: string) => void;
isDetails: boolean;
setSidePanelContent: SetSidePanelContent;
tagViewState?: TagViewState | null;
onDelete: (id: Tag[TagMeta.PK], fromDetails?: boolean) => void;
};

export enum Label {
CreateButton = "Create new tag",
EditButton = "Edit",
DeleteButton = "Delete",
Header = "Tags header",
All = "All tags",
Manual = "Manual tags",
Auto = "Automatic tags",
}

export const TagsHeader = ({
filter,
setFilter,
searchText,
setSearchText,
isDetails,
setSidePanelContent,
tagViewState,
onDelete,
}: Props): JSX.Element => {
const { machineCount } = useFetchMachineCount();
// Don't show the buttons when any of the forms are visible.
const showButtons = !tagViewState;
const id = useGetURLId(TagMeta.PK);
const tag = useSelector((state: RootState) =>
tagSelectors.getById(state, id)
);

return (
<MachinesHeader
aria-label={Label.Header}
buttons={
tagViewState === TagViewState.Updating
? null
: [
<Button
appearance="positive"
onClick={() =>
setSidePanelContent({ view: TagSidePanelViews.AddTag })
}
>
{Label.CreateButton}
</Button>,
]
}
machineCount={machineCount}
title="Tags"
/>
<MainToolbar>
<MainToolbar.Title>Tags</MainToolbar.Title>
{isDetails ? (
<Link className="u-sv3" to={urls.tags.index}>
&lsaquo; Back to all tags
</Link>
) : null}
<MainToolbar.Controls>
{tagViewState === TagViewState.Updating ? null : (
<>
{isDetails && tag ? (
<>
{showButtons ? (
<>
<Button
element={Link}
hasIcon
state={{ canGoBack: true }}
to={{
pathname: urls.tags.tag.update({ id: tag.id }),
}}
>
<Icon name="edit" /> <span>{Label.EditButton}</span>
</Button>
<Button
appearance="negative"
hasIcon
onClick={() => onDelete(tag[TagMeta.PK], true)}
>
<Icon className="is-light" name="delete" />{" "}
<span>{Label.DeleteButton}</span>
</Button>
</>
) : null}
</>
) : (
<>
<SearchBox
externallyControlled
onChange={setSearchText}
value={searchText}
/>
<SegmentedControl
aria-label="tag filter"
onSelect={setFilter}
options={[
{
label: Label.All,
value: TagSearchFilter.All,
},
{
label: Label.Manual,
value: TagSearchFilter.Manual,
},
{
label: Label.Auto,
value: TagSearchFilter.Auto,
},
]}
selected={filter}
/>
</>
)}
<Button
appearance="positive"
onClick={() =>
setSidePanelContent({ view: TagSidePanelViews.AddTag })
}
>
{Label.CreateButton}
</Button>
</>
)}
</MainToolbar.Controls>
</MainToolbar>
);
};

Expand Down
Loading

0 comments on commit 681ca87

Please sign in to comment.