From 8668190956183f39100c2ce71ec87be487cf8802 Mon Sep 17 00:00:00 2001 From: caichi <54824604+caichi-t@users.noreply.github.com> Date: Thu, 21 Mar 2024 14:49:49 +0900 Subject: [PATCH] fix(web): after deleting the first view, the current view is not changed (#1106) * move file * add: view test * fix: the behavior when removing first view * add: filter saving * add: saving column setting * add: test.setTimeout * fix: change test.timeout() to test.slow() --- web/e2e/project/{ => content}/content.spec.ts | 9 +- web/e2e/project/content/view.spec.ts | 169 ++++++++++++++++++ web/e2e/project/item/fields/group.spec.ts | 1 + .../Project/Content/ViewsMenu/hooks.ts | 40 ++--- .../Project/Content/ViewsMenu/index.tsx | 7 +- 5 files changed, 195 insertions(+), 31 deletions(-) rename web/e2e/project/{ => content}/content.spec.ts (95%) create mode 100644 web/e2e/project/content/view.spec.ts diff --git a/web/e2e/project/content.spec.ts b/web/e2e/project/content/content.spec.ts similarity index 95% rename from web/e2e/project/content.spec.ts rename to web/e2e/project/content/content.spec.ts index 1f4a71eeb0..9d321f1f61 100644 --- a/web/e2e/project/content.spec.ts +++ b/web/e2e/project/content/content.spec.ts @@ -1,11 +1,10 @@ import { closeNotification } from "@reearth-cms/e2e/common/notification"; +import { crudComment } from "@reearth-cms/e2e/project/utils/comment"; +import { handleFieldForm } from "@reearth-cms/e2e/project/utils/field"; +import { createModel } from "@reearth-cms/e2e/project/utils/model"; +import { createProject, deleteProject } from "@reearth-cms/e2e/project/utils/project"; import { expect, test } from "@reearth-cms/e2e/utils"; -import { crudComment } from "./utils/comment"; -import { handleFieldForm } from "./utils/field"; -import { createModel } from "./utils/model"; -import { createProject, deleteProject } from "./utils/project"; - test.beforeEach(async ({ reearth, page }) => { await reearth.goto("/", { waitUntil: "domcontentloaded" }); await createProject(page); diff --git a/web/e2e/project/content/view.spec.ts b/web/e2e/project/content/view.spec.ts new file mode 100644 index 0000000000..52b5b93d3d --- /dev/null +++ b/web/e2e/project/content/view.spec.ts @@ -0,0 +1,169 @@ +import { Page } from "@playwright/test"; + +import { closeNotification } from "@reearth-cms/e2e/common/notification"; +import { handleFieldForm } from "@reearth-cms/e2e/project/utils/field"; +import { createModel } from "@reearth-cms/e2e/project/utils/model"; +import { createProject, deleteProject } from "@reearth-cms/e2e/project/utils/project"; +import { createWorkspace, deleteWorkspace } from "@reearth-cms/e2e/project/utils/workspace"; +import { expect, test } from "@reearth-cms/e2e/utils"; + +test.beforeEach(async ({ reearth, page }) => { + await reearth.goto("/", { waitUntil: "domcontentloaded" }); + await createWorkspace(page); + await createProject(page); + await createModel(page); +}); + +test.afterEach(async ({ page }) => { + await deleteProject(page); + await deleteWorkspace(page); +}); + +const itemAdd = async (page: Page, data: string) => { + await page.getByRole("button", { name: "plus New Item" }).click(); + await page.getByLabel("text").click(); + await page.getByLabel("text").fill(data); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.getByRole("alert").last()).toContainText("Successfully created Item!"); + await closeNotification(page); + await page.getByLabel("Back").click(); +}; + +test("View CRUD has succeeded", async ({ page }) => { + test.slow(); + await page.locator("li").filter({ hasText: "Text" }).locator("div").first().click(); + await handleFieldForm(page, "text"); + await closeNotification(page); + await page.getByText("Content").click(); + await itemAdd(page, "text1"); + await itemAdd(page, "text2"); + await itemAdd(page, "sample1"); + await itemAdd(page, "sample2"); + await page.getByRole("button", { name: "Save as new view" }).click(); + await page.getByLabel("View Name").click(); + await page.getByLabel("View Name").fill("view1"); + await page.getByRole("button", { name: "OK" }).click(); + await expect(page.getByRole("alert").last()).toContainText("Successfully created view!"); + await closeNotification(page); + await expect(page.getByText("view1")).toBeVisible(); + await expect(page.getByRole("tab").nth(0)).toHaveAttribute("aria-selected", "true"); + + await page.getByLabel("more").locator("svg").click(); + await page.getByText("Rename").click(); + await page.getByLabel("View Name").click(); + await page.getByLabel("View Name").fill("new view1"); + await page.getByRole("button", { name: "OK" }).click(); + await expect(page.getByRole("alert").last()).toContainText("Successfully renamed view!"); + await closeNotification(page); + await expect(page.getByText("new view1")).toBeVisible(); + await page.getByLabel("more").locator("svg").click(); + await page.getByText("Remove View").click(); + await page.getByRole("button", { name: "Remove" }).click(); + await expect(page.getByRole("alert").last()).toContainText( + "input: deleteView model should have at least one view", + ); + await closeNotification(page); + + await page.getByText("text", { exact: true }).click(); + await expect( + page.getByRole("cell", { name: "text caret-up caret-down" }).locator(".anticon-caret-up"), + ).toHaveClass(/active/); + await expect(page.locator(".ant-table-row").nth(0)).toContainText("sample1"); + await expect(page.locator(".ant-table-row").nth(1)).toContainText("sample2"); + + await page.getByRole("button", { name: "plus Filter" }).click(); + await page.getByRole("menuitem", { name: "text" }).click(); + await expect(page.getByRole("button", { name: "text close" })).toBeVisible(); + await page.getByText("is").first().click(); + await page.getByText("contains", { exact: true }).click(); + await page.getByPlaceholder("Enter the value").click(); + await page.getByPlaceholder("Enter the value").fill("text"); + await page.getByRole("button", { name: "Confirm" }).click(); + + await page.getByRole("main").getByLabel("setting").locator("svg").click(); + await expect(page.getByRole("cell", { name: "Status" })).toBeVisible(); + await page.locator("div:nth-child(3) > .ant-tree-checkbox").click(); + await expect(page.getByRole("cell", { name: "Status" })).not.toBeVisible(); + + await page.getByRole("button", { name: "Save as new view" }).click(); + await page.getByLabel("View Name").click(); + await page.getByLabel("View Name").fill("view2"); + await page.getByRole("button", { name: "OK" }).click(); + await expect(page.getByRole("alert").last()).toContainText("Successfully created view!"); + await closeNotification(page); + await expect(page.getByRole("tab").nth(0)).toHaveAttribute("aria-selected", "false"); + await expect(page.getByRole("tab").nth(1)).toHaveAttribute("aria-selected", "true"); + await expect( + page.getByRole("cell", { name: "text caret-up caret-down" }).locator(".anticon-caret-up"), + ).toHaveClass(/active/); + await expect(page.locator(".ant-table-row").nth(0)).toContainText("text1"); + await expect(page.locator(".ant-table-row").nth(1)).toContainText("text2"); + + await page.getByText("new view1").click(); + await expect(page.getByRole("tab").nth(0)).toHaveAttribute("aria-selected", "true"); + await expect(page.getByRole("tab").nth(1)).toHaveAttribute("aria-selected", "false"); + await expect( + page.getByRole("cell", { name: "text caret-up caret-down" }).locator(".anticon-caret-up"), + ).not.toHaveClass(/active/); + await expect(page.getByRole("button", { name: "text close" })).not.toBeVisible(); + await expect(page.locator(".ant-table-row").nth(0)).toContainText("sample2"); + await expect(page.locator(".ant-table-row").nth(1)).toContainText("sample1"); + await expect(page.getByRole("cell", { name: "Status" })).toBeVisible(); + await page.getByRole("main").getByLabel("setting").locator("svg").click(); + await expect(page.locator("div:nth-child(3) > .ant-tree-checkbox")).toHaveClass( + /ant-tree-checkbox-checked/, + ); + await page.getByRole("main").getByLabel("setting").locator("svg").click(); + + await page.getByText("text", { exact: true }).first().click(); + await page.getByText("text", { exact: true }).first().click(); + await page.getByRole("button", { name: "plus Filter" }).click(); + await page.getByRole("menuitem", { name: "text" }).click(); + await expect(page.getByRole("button", { name: "text close" })).toBeVisible(); + await page.getByText("is", { exact: true }).first().click(); + await page.getByText("end with", { exact: true }).click(); + await page.getByPlaceholder("Enter the value").click(); + await page.getByPlaceholder("Enter the value").fill("1"); + await page.getByRole("button", { name: "Confirm" }).click(); + + await page.getByRole("tab", { name: "new view1 more" }).locator("svg").click(); + await page.getByText("Update View").click(); + await expect(page.getByRole("alert").last()).toContainText("Successfully updated view!"); + await closeNotification(page); + + await page.getByText("view2").click(); + await expect( + page.getByRole("cell", { name: "text caret-up caret-down" }).locator(".anticon-caret-up"), + ).toHaveClass(/active/); + await expect(page.getByRole("button", { name: "text close" })).toBeVisible(); + await expect(page.locator(".ant-table-row").nth(0)).toContainText("text1"); + await expect(page.locator(".ant-table-row").nth(1)).toContainText("text2"); + await expect(page.getByRole("cell", { name: "Status" })).not.toBeVisible(); + await page.getByRole("main").getByLabel("setting").locator("svg").click(); + await expect(page.locator("div:nth-child(3) > .ant-tree-checkbox")).not.toHaveClass( + /ant-tree-checkbox-checked/, + ); + await page.getByRole("main").getByLabel("setting").locator("svg").click(); + + await page.getByText("new view1").click(); + await expect( + page.getByRole("cell", { name: "text caret-up caret-down" }).locator(".anticon-caret-down"), + ).toHaveClass(/active/); + await expect(page.locator(".ant-table-row").nth(0)).toContainText("text1"); + await expect(page.locator(".ant-table-row").nth(1)).toContainText("sample1"); + + await page.getByRole("tab", { name: "new view1 more" }).click(); + await page.getByRole("tab", { name: "new view1 more" }).locator("svg").click(); + await page.getByText("Remove View").click(); + await page.getByRole("button", { name: "Remove" }).click(); + await expect(page.getByRole("alert").last()).toContainText("Successfully deleted view!"); + await closeNotification(page); + await expect(page.getByText("new view1")).not.toBeVisible(); + await expect(page.getByText("view2")).toBeVisible(); + await expect(page.getByRole("tab").nth(0)).toHaveAttribute("aria-selected", "true"); + await expect( + page.getByRole("cell", { name: "text caret-up caret-down" }).locator(".anticon-caret-up"), + ).toHaveClass(/active/); + await expect(page.locator(".ant-table-row").nth(0)).toContainText("text1"); + await expect(page.locator(".ant-table-row").nth(1)).toContainText("text2"); +}); diff --git a/web/e2e/project/item/fields/group.spec.ts b/web/e2e/project/item/fields/group.spec.ts index 91d0e465ed..88a8924ec0 100644 --- a/web/e2e/project/item/fields/group.spec.ts +++ b/web/e2e/project/item/fields/group.spec.ts @@ -15,6 +15,7 @@ test.afterEach(async ({ page }) => { }); test("Group field creating and updating has succeeded", async ({ page }) => { + test.slow(); await expect( page.locator("li").filter({ hasText: "Reference" }).locator("div").first(), ).toBeVisible(); diff --git a/web/src/components/organisms/Project/Content/ViewsMenu/hooks.ts b/web/src/components/organisms/Project/Content/ViewsMenu/hooks.ts index 8297093304..c67e15fa7e 100644 --- a/web/src/components/organisms/Project/Content/ViewsMenu/hooks.ts +++ b/web/src/components/organisms/Project/Content/ViewsMenu/hooks.ts @@ -1,4 +1,5 @@ import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; +import { useParams } from "react-router-dom"; import Notification from "@reearth-cms/components/atoms/Notification"; import { AndCondition, View } from "@reearth-cms/components/molecules/View/types"; @@ -22,7 +23,6 @@ import { useProject } from "@reearth-cms/state"; import { CurrentViewType } from "../ContentList/hooks"; type Params = { - modelId?: string; currentView: CurrentViewType; setCurrentView: Dispatch>; onViewChange: () => void; @@ -30,10 +30,12 @@ type Params = { export type modalStateType = "rename" | "create"; -export default ({ modelId, currentView, setCurrentView, onViewChange }: Params) => { +export default ({ currentView, setCurrentView, onViewChange }: Params) => { + const { modelId } = useParams(); const t = useT(); const [prevModelId, setPrevModelId] = useState(); const [viewModalShown, setViewModalShown] = useState(false); + const [views, setViews] = useState([]); const [selectedView, setSelectedView] = useState(); const [modalState, setModalState] = useState("create"); const [submitting, setSubmitting] = useState(false); @@ -46,17 +48,22 @@ export default ({ modelId, currentView, setCurrentView, onViewChange }: Params) skip: !modelId, }); - const views = useMemo(() => { + useEffect(() => { const viewList = data?.view - ?.map(view => fromGraphQLView(view as GQLView)) + ?.map(view => fromGraphQLView(view as GQLView)) .filter((view): view is View => !!view); - - if (prevModelId !== modelId && viewList) { - setSelectedView(viewList && viewList.length > 0 ? viewList[0] : undefined); - setPrevModelId(modelId); + if (viewList) { + if (prevModelId !== modelId) { + setSelectedView(viewList[0]); + setPrevModelId(modelId); + } else if (viewList.length > views.length) { + setSelectedView(viewList[viewList.length - 1]); + } else if (viewList.length < views.length) { + setSelectedView(viewList[0]); + } } - return viewList ?? []; - }, [data?.view, modelId, prevModelId]); + setViews(viewList ?? []); + }, [data?.view, modelId, prevModelId, views.length]); useEffect(() => { if (selectedView) { @@ -129,7 +136,6 @@ export default ({ modelId, currentView, setCurrentView, onViewChange }: Params) Notification.error({ message: t("Failed to create view.") }); return; } - setSelectedView(fromGraphQLView(view.data.createView.view as GQLView)); setViewModalShown(false); onViewChange(); Notification.success({ message: t("Successfully created view!") }); @@ -173,7 +179,6 @@ export default ({ modelId, currentView, setCurrentView, onViewChange }: Params) Notification.error({ message: t("Failed to update view.") }); return; } - setSelectedView(fromGraphQLView(view.data.updateView.view as GQLView)); Notification.success({ message: t("Successfully updated view!") }); handleViewModalReset(); }, @@ -206,7 +211,6 @@ export default ({ modelId, currentView, setCurrentView, onViewChange }: Params) Notification.error({ message: t("Failed to rename view.") }); return; } - setSelectedView(fromGraphQLView(view.data.updateView.view as GQLView)); Notification.success({ message: t("Successfully renamed view!") }); handleViewModalReset(); }, @@ -217,10 +221,6 @@ export default ({ modelId, currentView, setCurrentView, onViewChange }: Params) refetchQueries: ["GetViews"], }); - const handleViewDeletionModalClose = useCallback(() => { - setSelectedView(fromGraphQLView(views[0] as GQLView)); - }, [views]); - const handleViewDelete = useCallback( async (viewId?: string) => { if (!viewId) return; @@ -230,10 +230,9 @@ export default ({ modelId, currentView, setCurrentView, onViewChange }: Params) } else { Notification.success({ message: t("Successfully deleted view!") }); onViewChange(); - handleViewDeletionModalClose(); } }, - [deleteView, handleViewDeletionModalClose, onViewChange, t], + [deleteView, onViewChange, t], ); return { @@ -241,8 +240,6 @@ export default ({ modelId, currentView, setCurrentView, onViewChange }: Params) modalState, handleViewRenameModalOpen, handleViewCreateModalOpen, - handleViewDelete, - handleViewDeletionModalClose, selectedView, setSelectedView, viewModalShown, @@ -251,5 +248,6 @@ export default ({ modelId, currentView, setCurrentView, onViewChange }: Params) handleViewCreate, handleViewUpdate, handleViewRename, + handleViewDelete, }; }; diff --git a/web/src/components/organisms/Project/Content/ViewsMenu/index.tsx b/web/src/components/organisms/Project/Content/ViewsMenu/index.tsx index f841a63700..0f88443a0c 100644 --- a/web/src/components/organisms/Project/Content/ViewsMenu/index.tsx +++ b/web/src/components/organisms/Project/Content/ViewsMenu/index.tsx @@ -1,5 +1,4 @@ import React, { Dispatch, SetStateAction } from "react"; -import { useParams } from "react-router-dom"; import ViewFormModal from "@reearth-cms/components/molecules/View/ViewFormModal"; import ViewsMenuMolecule from "@reearth-cms/components/molecules/View/viewsMenu"; @@ -15,8 +14,6 @@ export type Props = { }; const ViewsMenu: React.FC = ({ currentView, setCurrentView, onViewChange }) => { - const { modelId } = useParams(); - const { views, modalState, @@ -28,10 +25,10 @@ const ViewsMenu: React.FC = ({ currentView, setCurrentView, onViewChange submitting, handleViewModalReset, handleViewCreate, - handleViewRename, handleViewUpdate, + handleViewRename, handleViewDelete, - } = useHooks({ modelId, currentView, setCurrentView, onViewChange }); + } = useHooks({ currentView, setCurrentView, onViewChange }); return ( <>