diff --git a/src/components/administration/ExternalToolSection.unit.ts b/src/components/administration/ExternalToolSection.unit.ts index 6d2adea44a..e2fb4f8d9e 100644 --- a/src/components/administration/ExternalToolSection.unit.ts +++ b/src/components/administration/ExternalToolSection.unit.ts @@ -51,6 +51,9 @@ describe("ExternalToolSection", () => { [NOTIFIER_MODULE_KEY.valueOf()]: notifierModule, [AUTH_MODULE_KEY.valueOf()]: authModule, }, + stubs: { + VIcon: true, + }, }); return { @@ -152,25 +155,17 @@ describe("ExternalToolSection", () => { const firstRow = tableRows.at(0).findAll("td"); const secondRow = tableRows.at(1).findAll("td"); - expect(firstRow.at(1).text()).toEqual( + expect(firstRow.at(1).find("span").text()).toEqual( "components.externalTools.status.latest" ); - expect( - firstRow - .at(1) - .findComponent({ name: "v-icon" }) - .find("path") - .attributes("d") - ).toEqual(mdiCheckCircle); - expect(secondRow.at(1).text()).toEqual( + expect(firstRow.at(1).findComponent({ name: "v-icon" }).text()).toEqual( + mdiCheckCircle + ); + expect(secondRow.at(1).find("span").text()).toEqual( "components.externalTools.status.outdated" ); expect( - secondRow - .at(1) - .findComponent({ name: "v-icon" }) - .find("path") - .attributes("d") + secondRow.at(1).findComponent({ name: "v-icon" }).text() ).toEqual(mdiRefreshCircle); }); diff --git a/src/components/data-board/BoardApi.composable.ts b/src/components/data-board/BoardApi.composable.ts index 5b9f97f41c..dfcb9f02f4 100644 --- a/src/components/data-board/BoardApi.composable.ts +++ b/src/components/data-board/BoardApi.composable.ts @@ -3,21 +3,22 @@ import { BoardCardApiFactory, BoardColumnApiFactory, BoardElementApiFactory, + BoardResponse, + CardResponse, ColumnResponse, ContentElementType, + CreateCardBodyParamsRequiredEmptyElementsEnum, CreateContentElementBodyParams, + ExternalToolElementContent, FileElementContent, RichTextElementContent, - CreateCardBodyParamsRequiredEmptyElementsEnum, RoomsApiFactory, - BoardResponse, - CardResponse, SubmissionContainerElementContent, } from "@/serverApi/v3"; -import { $axios, mapAxiosErrorToResponseError } from "@/utils/api"; import { AnyContentElement } from "@/types/board/ContentElement"; -import { AxiosPromise } from "axios"; +import { $axios, mapAxiosErrorToResponseError } from "@/utils/api"; import { createApplicationError } from "@/utils/create-application-error.factory"; +import { AxiosPromise } from "axios"; export const useBoardApi = () => { const boardApi = BoardApiFactory(undefined, "/v3", $axios); @@ -90,6 +91,13 @@ export const useBoardApi = () => { }; } + if (element.type === ContentElementType.ExternalTool) { + return { + content: element.content as ExternalToolElementContent, + type: ContentElementType.ExternalTool, + }; + } + throw new Error("element.type mapping is undefined for updateElementCall"); }; diff --git a/src/components/data-board/BoardApi.composable.unit.ts b/src/components/data-board/BoardApi.composable.unit.ts index 1d6701f9e9..3edc48d1ff 100644 --- a/src/components/data-board/BoardApi.composable.unit.ts +++ b/src/components/data-board/BoardApi.composable.unit.ts @@ -70,15 +70,15 @@ describe("BoardApi.composable", () => { describe("updateCardHeight", () => { it("should call cardControllerUpdateCardHeight api", async () => { const { updateCardHeightCall } = useBoardApi(); - const PAYLOAD = { + const payload = { id: "update-card-id", height: 200, }; - await updateCardHeightCall(PAYLOAD.id, PAYLOAD.height); + await updateCardHeightCall(payload.id, payload.height); expect(cardApi.cardControllerUpdateCardHeight).toHaveBeenCalledWith( - PAYLOAD.id, - { height: PAYLOAD.height } + payload.id, + { height: payload.height } ); }); }); @@ -86,15 +86,15 @@ describe("BoardApi.composable", () => { describe("updateCardTitle", () => { it("should call cardControllerUpdateCardTitle api", async () => { const { updateCardTitle } = useBoardApi(); - const PAYLOAD = { + const payload = { id: "update-card-id", title: "update-title", }; - await updateCardTitle(PAYLOAD.id, PAYLOAD.title); + await updateCardTitle(payload.id, payload.title); expect(cardApi.cardControllerUpdateCardTitle).toHaveBeenCalledWith( - PAYLOAD.id, - { title: PAYLOAD.title } + payload.id, + { title: payload.title } ); }); }); @@ -102,15 +102,15 @@ describe("BoardApi.composable", () => { describe("updateColumnTitleCall", () => { it("should call columnControllerUpdateColumnTitle api", async () => { const { updateColumnTitleCall } = useBoardApi(); - const PAYLOAD = { + const payload = { id: "update-column-id", title: "update-title", }; - await updateColumnTitleCall(PAYLOAD.id, PAYLOAD.title); + await updateColumnTitleCall(payload.id, payload.title); expect(columnApi.columnControllerUpdateColumnTitle).toHaveBeenCalledWith( - PAYLOAD.id, - { title: PAYLOAD.title } + payload.id, + { title: payload.title } ); }); }); @@ -118,7 +118,7 @@ describe("BoardApi.composable", () => { describe("updateElementTitleCall", () => { it("should call elementControllerUpdateElement api with RichtTextElement", async () => { const { updateElementCall } = useBoardApi(); - const PAYLOAD = { + const payload = { id: "richt-text-element-id", type: ContentElementType.RichText, content: { @@ -129,20 +129,20 @@ describe("BoardApi.composable", () => { timestamps: timestampsResponseFactory.build(), }; const data = { - content: PAYLOAD.content, + content: payload.content, type: ContentElementType.RichText, }; - await updateElementCall(PAYLOAD); + await updateElementCall(payload); expect(elementApi.elementControllerUpdateElement).toHaveBeenCalledWith( - PAYLOAD.id, + payload.id, { data } ); }); it("should call elementControllerUpdateElement api with FileElement", async () => { const { updateElementCall } = useBoardApi(); - const PAYLOAD = { + const payload = { id: "file-element-id", type: ContentElementType.File, content: { @@ -152,20 +152,20 @@ describe("BoardApi.composable", () => { timestamps: timestampsResponseFactory.build(), }; const data = { - content: PAYLOAD.content, + content: payload.content, type: ContentElementType.File, }; - await updateElementCall(PAYLOAD); + await updateElementCall(payload); expect(elementApi.elementControllerUpdateElement).toHaveBeenCalledWith( - PAYLOAD.id, + payload.id, { data } ); }); it("should call elementControllerUpdateElement api with SubmissionContainerElement", async () => { const { updateElementCall } = useBoardApi(); - const PAYLOAD = { + const payload = { id: "file-element-id", type: ContentElementType.SubmissionContainer, content: { @@ -174,24 +174,47 @@ describe("BoardApi.composable", () => { timestamps: timestampsResponseFactory.build(), }; const data = { - content: PAYLOAD.content, + content: payload.content, type: ContentElementType.SubmissionContainer, }; - await updateElementCall(PAYLOAD); + await updateElementCall(payload); expect(elementApi.elementControllerUpdateElement).toHaveBeenCalledWith( - PAYLOAD.id, + payload.id, + { data } + ); + }); + + it("should call elementControllerUpdateElement api with ExternalToolElement", async () => { + const { updateElementCall } = useBoardApi(); + const payload = { + id: "external-tool-element-id", + type: ContentElementType.ExternalTool, + content: { + contextExternalToolId: "context-external-tool-id", + }, + timestamps: timestampsResponseFactory.build(), + }; + const data = { + content: payload.content, + type: ContentElementType.ExternalTool, + }; + + await updateElementCall(payload); + + expect(elementApi.elementControllerUpdateElement).toHaveBeenCalledWith( + payload.id, { data } ); }); it("should throw error for unkown element type", async () => { const { updateElementCall } = useBoardApi(); - const PAYLOAD = { + const payload = { type: "unkown" as ContentElementType, } as AnyContentElement; - await expect(updateElementCall(PAYLOAD)).rejects.toThrow( + await expect(updateElementCall(payload)).rejects.toThrow( new Error("element.type mapping is undefined for updateElementCall") ); }); @@ -200,13 +223,13 @@ describe("BoardApi.composable", () => { describe("createElementCall", () => { it("should call cardControllerCreateElement api", async () => { const { createElementCall } = useBoardApi(); - const PAYLOAD = "card-id"; + const payload = "card-id"; - await createElementCall(PAYLOAD, { + await createElementCall(payload, { type: ContentElementType.RichText, }); expect(cardApi.cardControllerCreateElement).toHaveBeenCalledWith( - PAYLOAD, + payload, { type: ContentElementType.RichText } ); }); @@ -215,21 +238,21 @@ describe("BoardApi.composable", () => { describe("deleteCardCall", () => { it("should call cardControllerDeleteCard api", async () => { const { deleteCardCall } = useBoardApi(); - const PAYLOAD = "card-id"; + const payload = "card-id"; - await deleteCardCall(PAYLOAD); - expect(cardApi.cardControllerDeleteCard).toHaveBeenCalledWith(PAYLOAD); + await deleteCardCall(payload); + expect(cardApi.cardControllerDeleteCard).toHaveBeenCalledWith(payload); }); }); describe("deleteColumnCall", () => { it("should call columnControllerDeleteColumn api", async () => { const { deleteColumnCall } = useBoardApi(); - const PAYLOAD = "column-id"; + const payload = "column-id"; - await deleteColumnCall(PAYLOAD); + await deleteColumnCall(payload); expect(columnApi.columnControllerDeleteColumn).toHaveBeenCalledWith( - PAYLOAD + payload ); }); }); @@ -237,11 +260,11 @@ describe("BoardApi.composable", () => { describe("deleteElementCall", () => { it("should call elementControllerDeleteElement api", async () => { const { deleteElementCall } = useBoardApi(); - const PAYLOAD = "element-id"; + const payload = "element-id"; - await deleteElementCall(PAYLOAD); + await deleteElementCall(payload); expect(elementApi.elementControllerDeleteElement).toHaveBeenCalledWith( - PAYLOAD + payload ); }); }); @@ -261,17 +284,17 @@ describe("BoardApi.composable", () => { FAKE_RESPONSE as unknown as AxiosPromise ); - const PAYLOAD = "column-id"; + const payload = "column-id"; const INITIAL_ELEMENTS = { requiredEmptyElements: [ serverApi.CreateCardBodyParamsRequiredEmptyElementsEnum.RichText, ], }; - const result = await createCardCall(PAYLOAD); + const result = await createCardCall(payload); expect(columnApi.columnControllerCreateCard).toHaveBeenCalledWith( - PAYLOAD, + payload, INITIAL_ELEMENTS ); @@ -282,7 +305,7 @@ describe("BoardApi.composable", () => { describe("moveCardCall", () => { it("should call cardControllerMoveCard api", async () => { const { moveCardCall } = useBoardApi(); - const PAYLOAD = { + const payload = { cardId: "card-id", position: { toColumnId: "col-id", @@ -291,14 +314,14 @@ describe("BoardApi.composable", () => { }; await moveCardCall( - PAYLOAD.cardId, - PAYLOAD.position.toColumnId, - PAYLOAD.position.toPosition + payload.cardId, + payload.position.toColumnId, + payload.position.toPosition ); expect(cardApi.cardControllerMoveCard).toHaveBeenCalledWith( - PAYLOAD.cardId, + payload.cardId, { - ...PAYLOAD.position, + ...payload.position, } ); }); @@ -307,7 +330,7 @@ describe("BoardApi.composable", () => { describe("moveColumnCall", () => { it("should call columnControllerMoveColumn api", async () => { const { moveColumnCall } = useBoardApi(); - const PAYLOAD = { + const payload = { columnId: "column-id", position: { toBoardId: "board-id", @@ -316,14 +339,14 @@ describe("BoardApi.composable", () => { }; await moveColumnCall( - PAYLOAD.columnId, - PAYLOAD.position.toBoardId, - PAYLOAD.position.toPosition + payload.columnId, + payload.position.toBoardId, + payload.position.toPosition ); expect(columnApi.columnControllerMoveColumn).toHaveBeenCalledWith( - PAYLOAD.columnId, + payload.columnId, { - ...PAYLOAD.position, + ...payload.position, } ); }); @@ -332,7 +355,7 @@ describe("BoardApi.composable", () => { describe("moveElementCall", () => { it("should call elementControllerMoveElement api", async () => { const { moveElementCall } = useBoardApi(); - const PAYLOAD = { + const payload = { elementId: "element-id", position: { toCardId: "card-id", @@ -341,14 +364,14 @@ describe("BoardApi.composable", () => { }; await moveElementCall( - PAYLOAD.elementId, - PAYLOAD.position.toCardId, - PAYLOAD.position.toPosition + payload.elementId, + payload.position.toCardId, + payload.position.toPosition ); expect(elementApi.elementControllerMoveElement).toHaveBeenCalledWith( - PAYLOAD.elementId, + payload.elementId, { - ...PAYLOAD.position, + ...payload.position, } ); }); @@ -357,14 +380,14 @@ describe("BoardApi.composable", () => { describe("getContextInfo", () => { it("should call boardControllerGetBoardContext api", async () => { const { getContextInfo } = useBoardApi(); - const PAYLOAD = { + const payload = { boardId: "myid123", }; - await getContextInfo(PAYLOAD.boardId); + await getContextInfo(payload.boardId); expect(boardApi.boardControllerGetBoardContext).toHaveBeenCalledWith( - PAYLOAD.boardId + payload.boardId ); }); diff --git a/src/components/feature-board-external-tool-element/ExternalToolElement.unit.ts b/src/components/feature-board-external-tool-element/ExternalToolElement.unit.ts new file mode 100644 index 0000000000..82e7ecc6c5 --- /dev/null +++ b/src/components/feature-board-external-tool-element/ExternalToolElement.unit.ts @@ -0,0 +1,448 @@ +import { + ContentElementType, + ExternalToolElementResponse, +} from "@/serverApi/v3"; +import ContextExternalToolsModule from "@/store/context-external-tools"; +import { ExternalToolDisplayData } from "@/store/external-tool"; +import { CONTEXT_EXTERNAL_TOOLS_MODULE_KEY, I18N_KEY } from "@/utils/inject"; +import { createModuleMocks } from "@/utils/mock-store-module"; +import { + externalToolDisplayDataFactory, + i18nMock, + timestampsResponseFactory, +} from "@@/tests/test-utils"; +import createComponentMocks from "@@/tests/test-utils/componentMocks"; +import { createMock } from "@golevelup/ts-jest"; +import { mdiPuzzleOutline } from "@mdi/js"; +import { useDeleteConfirmationDialog } from "@ui-confirmation-dialog"; +import { MountOptions, shallowMount, Wrapper } from "@vue/test-utils"; +import Vue from "vue"; +import ExternalToolElement from "./ExternalToolElement.vue"; + +jest.mock("@data-board", () => { + return { + useBoardFocusHandler: jest.fn(), + }; +}); +jest.mock("@ui-confirmation-dialog"); + +const TEST_ELEMENT: ExternalToolElementResponse = { + id: "external-tool-element-id", + content: {}, + type: ContentElementType.ExternalTool, + timestamps: timestampsResponseFactory.build(), +}; + +describe("ExternalToolElement", () => { + const getWrapper = ( + props: { + element: ExternalToolElementResponse; + isEditMode: boolean; + }, + displayData: ExternalToolDisplayData[] = [] + ) => { + document.body.setAttribute("data-app", "true"); + + const contextExternalToolsModule = createModuleMocks( + ContextExternalToolsModule, + { + getExternalToolDisplayDataList: displayData, + } + ); + + const useDeleteConfirmationDialogReturnValue = + createMock>(); + jest + .mocked(useDeleteConfirmationDialog) + .mockReturnValue(useDeleteConfirmationDialogReturnValue); + + const wrapper: Wrapper = shallowMount( + ExternalToolElement as MountOptions, + { + ...createComponentMocks({ + i18n: true, + }), + propsData: { + isFirstElement: false, + isLastElement: false, + hasMultipleElements: false, + ...props, + }, + provide: { + [I18N_KEY.valueOf()]: i18nMock, + [CONTEXT_EXTERNAL_TOOLS_MODULE_KEY.valueOf()]: + contextExternalToolsModule, + }, + } + ); + + return { + wrapper, + contextExternalToolsModule, + useDeleteConfirmationDialogReturnValue, + }; + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("when no tool is selected", () => { + describe("when not in edit mode", () => { + const setup = () => { + const { wrapper } = getWrapper({ + element: TEST_ELEMENT, + isEditMode: false, + }); + + return { + wrapper, + }; + }; + + it("should hide the element", () => { + const { wrapper } = setup(); + + const element = wrapper.findComponent({ ref: "externalToolElement" }); + + expect(element.isVisible()).toEqual(false); + }); + }); + + describe("when in edit mode", () => { + const setup = () => { + const { wrapper } = getWrapper({ + element: TEST_ELEMENT, + isEditMode: true, + }); + + return { + wrapper, + }; + }; + + it("should hide the element", () => { + const { wrapper } = setup(); + + const element = wrapper.findComponent({ ref: "externalToolElement" }); + + expect(element.isVisible()).toEqual(true); + }); + }); + }); + + describe("Icon", () => { + describe("when not logo is defined", () => { + const setup = () => { + const contextExternalToolId = "context-external-tool-id"; + + const { wrapper } = getWrapper( + { + element: { + ...TEST_ELEMENT, + content: { contextExternalToolId }, + }, + isEditMode: false, + }, + [ + externalToolDisplayDataFactory.build({ + contextExternalToolId, + logoUrl: undefined, + }), + ] + ); + + return { + wrapper, + }; + }; + + it("should show the default icon", () => { + const { wrapper } = setup(); + + const icon = wrapper + .findComponent({ ref: "externalToolElement" }) + .findComponent({ name: "v-icon" }); + + expect(icon.text()).toEqual(mdiPuzzleOutline); + }); + }); + + describe("when a logo is defined", () => { + const setup = () => { + const contextExternalToolId = "context-external-tool-id"; + + const { wrapper } = getWrapper( + { + element: { + ...TEST_ELEMENT, + content: { contextExternalToolId }, + }, + isEditMode: false, + }, + [ + externalToolDisplayDataFactory.build({ + contextExternalToolId, + logoUrl: "logo-url", + }), + ] + ); + + return { + wrapper, + }; + }; + + it("should show the logo", () => { + const { wrapper } = setup(); + + const img = wrapper + .findComponent({ ref: "externalToolElement" }) + .findComponent({ name: "v-img" }); + + expect(img.isVisible()).toEqual(true); + }); + }); + }); + + describe("Title", () => { + describe("when no tool is selected", () => { + const setup = () => { + const { wrapper } = getWrapper({ + element: TEST_ELEMENT, + isEditMode: true, + }); + + return { + wrapper, + }; + }; + + it("should display a selection text", () => { + const { wrapper } = setup(); + + const title = wrapper + .findComponent({ ref: "externalToolElement" }) + .find(".title"); + + expect(title.text()).toEqual( + "feature-board-external-tool-element.placeholder.selectTool" + ); + }); + }); + + describe("when the title is loading", () => { + const setup = () => { + const contextExternalToolId = "context-external-tool-id"; + + const { wrapper } = getWrapper( + { + element: { + ...TEST_ELEMENT, + content: { contextExternalToolId }, + }, + isEditMode: false, + }, + [] + ); + + return { + wrapper, + }; + }; + + it("should display '...'", () => { + const { wrapper } = setup(); + + const title = wrapper + .findComponent({ ref: "externalToolElement" }) + .find(".title"); + + expect(title.text()).toEqual("..."); + }); + }); + + describe("when the title is available", () => { + const setup = () => { + const contextExternalToolId = "context-external-tool-id"; + const toolDisplayData = externalToolDisplayDataFactory.build({ + contextExternalToolId, + logoUrl: "logo-url", + }); + + const { wrapper } = getWrapper( + { + element: { + ...TEST_ELEMENT, + content: { contextExternalToolId }, + }, + isEditMode: false, + }, + [toolDisplayData] + ); + + return { + wrapper, + toolDisplayData, + }; + }; + + it("should display the tools name", () => { + const { wrapper, toolDisplayData } = setup(); + + const title = wrapper + .findComponent({ ref: "externalToolElement" }) + .find(".title"); + + expect(title.text()).toEqual(toolDisplayData.name); + }); + }); + }); + + describe("Loading", () => { + describe("when the component is loading", () => { + const setup = () => { + const contextExternalToolId = "context-external-tool-id"; + + const { wrapper } = getWrapper( + { + element: { + ...TEST_ELEMENT, + content: { contextExternalToolId }, + }, + isEditMode: false, + }, + [] + ); + + return { + wrapper, + }; + }; + + it("should display a loading state", () => { + const { wrapper } = setup(); + + const title = wrapper.findComponent({ ref: "externalToolElement" }); + + expect(title.attributes("loading")).toEqual("true"); + }); + }); + + describe("when the component has finished loading", () => { + const setup = () => { + const contextExternalToolId = "context-external-tool-id"; + + const { wrapper } = getWrapper( + { + element: { + ...TEST_ELEMENT, + content: { contextExternalToolId }, + }, + isEditMode: false, + }, + [externalToolDisplayDataFactory.build({ contextExternalToolId })] + ); + + return { + wrapper, + }; + }; + + it("should display a loading state", () => { + const { wrapper } = setup(); + + const title = wrapper.findComponent({ ref: "externalToolElement" }); + + expect(title.attributes("loading")).toBeFalsy(); + }); + }); + }); + + describe("Menu", () => { + describe("when in edit mode", () => { + const setup = () => { + const { wrapper } = getWrapper({ + element: TEST_ELEMENT, + isEditMode: true, + }); + + return { + wrapper, + }; + }; + + it("should display the three dot menu", () => { + const { wrapper } = setup(); + + const menu = wrapper.findComponent({ ref: "externalToolElementMenu" }); + + expect(menu.isVisible()).toEqual(true); + }); + }); + + describe("when in display mode", () => { + const setup = () => { + const { wrapper } = getWrapper({ + element: TEST_ELEMENT, + isEditMode: false, + }); + + return { + wrapper, + }; + }; + + it("should not display the three dot menu", () => { + const { wrapper } = setup(); + + const menu = wrapper.findComponent({ ref: "externalToolElementMenu" }); + + expect(menu.exists()).toEqual(false); + }); + }); + + describe("when deleting the element", () => { + const setup = () => { + const contextExternalToolId = "context-external-tool-id"; + const toolDisplayData = externalToolDisplayDataFactory.build({ + contextExternalToolId, + logoUrl: "logo-url", + }); + + const { wrapper, useDeleteConfirmationDialogReturnValue } = getWrapper( + { + element: { + ...TEST_ELEMENT, + content: { contextExternalToolId }, + }, + isEditMode: true, + }, + [toolDisplayData] + ); + + return { + wrapper, + useDeleteConfirmationDialogReturnValue, + toolDisplayData, + }; + }; + + it("should display a delete dialog", () => { + const { + wrapper, + useDeleteConfirmationDialogReturnValue, + toolDisplayData, + } = setup(); + + const menu = wrapper.findComponent({ ref: "externalToolElementMenu" }); + + menu.vm.$emit("delete:element"); + + expect( + useDeleteConfirmationDialogReturnValue.askDeleteConfirmation + ).toHaveBeenCalledWith(toolDisplayData.name, "boardElement"); + }); + }); + }); +}); diff --git a/src/components/feature-board-external-tool-element/ExternalToolElement.vue b/src/components/feature-board-external-tool-element/ExternalToolElement.vue new file mode 100644 index 0000000000..a966f4caa6 --- /dev/null +++ b/src/components/feature-board-external-tool-element/ExternalToolElement.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/src/components/feature-board-external-tool-element/ExternalToolElementMenu.unit.ts b/src/components/feature-board-external-tool-element/ExternalToolElementMenu.unit.ts new file mode 100644 index 0000000000..c9120f8964 --- /dev/null +++ b/src/components/feature-board-external-tool-element/ExternalToolElementMenu.unit.ts @@ -0,0 +1,210 @@ +import createComponentMocks from "@@/tests/test-utils/componentMocks"; +import { MountOptions, shallowMount, Wrapper } from "@vue/test-utils"; +import Vue from "vue"; +import ExternalToolElementMenu from "./ExternalToolElementMenu.vue"; + +describe("ExternalToolElementMenu", () => { + const getWrapper = (props: { + isFirstElement: boolean; + isLastElement: boolean; + hasMultipleElements: boolean; + }) => { + document.body.setAttribute("data-app", "true"); + + const wrapper: Wrapper = shallowMount( + ExternalToolElementMenu as MountOptions, + { + ...createComponentMocks({ + i18n: true, + }), + propsData: props, + } + ); + + return { + wrapper, + }; + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("when the element can move up", () => { + const setup = () => { + const { wrapper } = getWrapper({ + hasMultipleElements: true, + isFirstElement: false, + isLastElement: false, + }); + + return { + wrapper, + }; + }; + + it("should have a menu option to move up", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.find( + '[data-testid="board-external-tool-element-edit-menu-move-up"]' + ); + + expect(menuItem.exists()).toEqual(true); + }); + + it("should emit the move up event on click", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.find( + '[data-testid="board-external-tool-element-edit-menu-move-up"]' + ); + + menuItem.vm.$emit("click"); + + expect(wrapper.emitted("move-up:element")).toBeDefined(); + }); + }); + + describe("when the element can move down", () => { + const setup = () => { + const { wrapper } = getWrapper({ + hasMultipleElements: true, + isFirstElement: false, + isLastElement: false, + }); + + return { + wrapper, + }; + }; + + it("should have a menu option to move down", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.find( + '[data-testid="board-external-tool-element-edit-menu-move-down"]' + ); + + expect(menuItem.exists()).toEqual(true); + }); + + it("should emit the move down event on click", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.find( + '[data-testid="board-external-tool-element-edit-menu-move-down"]' + ); + + menuItem.vm.$emit("click"); + + expect(wrapper.emitted("move-down:element")).toBeDefined(); + }); + }); + + describe("when the element cannot move up or down", () => { + const setup = () => { + const { wrapper } = getWrapper({ + hasMultipleElements: false, + isFirstElement: true, + isLastElement: true, + }); + + return { + wrapper, + }; + }; + + it("should not have a menu option to move up", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.find( + '[data-testid="board-external-tool-element-edit-menu-move-up"]' + ); + + expect(menuItem.exists()).toEqual(false); + }); + + it("should not have a menu option to move down", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.find( + '[data-testid="board-external-tool-element-edit-menu-move-down"]' + ); + + expect(menuItem.exists()).toEqual(false); + }); + }); + + describe("Edit Button", () => { + const setup = () => { + const { wrapper } = getWrapper({ + hasMultipleElements: true, + isFirstElement: false, + isLastElement: false, + }); + + return { + wrapper, + }; + }; + + it("should have a menu option to edit", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.find( + '[data-testid="board-external-tool-element-edit-menu-edit"]' + ); + + expect(menuItem.exists()).toEqual(true); + }); + + it("should emit the edit event on click", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.find( + '[data-testid="board-external-tool-element-edit-menu-edit"]' + ); + + menuItem.vm.$emit("click"); + + expect(wrapper.emitted("edit:element")).toBeDefined(); + }); + }); + + describe("Delete Button", () => { + const setup = () => { + const { wrapper } = getWrapper({ + hasMultipleElements: true, + isFirstElement: false, + isLastElement: false, + }); + + return { + wrapper, + }; + }; + + it("should have a menu option to delete", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.find( + '[data-testid="board-external-tool-element-edit-menu-delete"]' + ); + + expect(menuItem.exists()).toEqual(true); + }); + + it("should emit the delete event on click", () => { + const { wrapper } = setup(); + + const menuItem = wrapper.find( + '[data-testid="board-external-tool-element-edit-menu-delete"]' + ); + + menuItem.vm.$emit("click"); + + expect(wrapper.emitted("delete:element")).toBeDefined(); + }); + }); +}); diff --git a/src/components/feature-board-external-tool-element/ExternalToolElementMenu.vue b/src/components/feature-board-external-tool-element/ExternalToolElementMenu.vue new file mode 100644 index 0000000000..8a47f3d696 --- /dev/null +++ b/src/components/feature-board-external-tool-element/ExternalToolElementMenu.vue @@ -0,0 +1,105 @@ + + + diff --git a/src/components/feature-board-external-tool-element/index.ts b/src/components/feature-board-external-tool-element/index.ts new file mode 100644 index 0000000000..df8c8ea9b7 --- /dev/null +++ b/src/components/feature-board-external-tool-element/index.ts @@ -0,0 +1,3 @@ +import ExternalToolElement from "./ExternalToolElement.vue"; + +export { ExternalToolElement }; diff --git a/src/components/feature-board/card/ContentElementList.unit.ts b/src/components/feature-board/card/ContentElementList.unit.ts index 61feaae96b..f9eaedc094 100644 --- a/src/components/feature-board/card/ContentElementList.unit.ts +++ b/src/components/feature-board/card/ContentElementList.unit.ts @@ -4,7 +4,9 @@ import { Envs } from "@/store/types/env-config"; import { AnyContentElement } from "@/types/board/ContentElement"; import { ENV_CONFIG_MODULE_KEY, I18N_KEY } from "@/utils/inject"; import { createModuleMocks } from "@/utils/mock-store-module"; +import { i18nMock } from "@@/tests/test-utils"; import createComponentMocks from "@@/tests/test-utils/componentMocks"; +import { ExternalToolElement } from "@feature-board-external-tool-element"; import { FileContentElement } from "@feature-board-file-element"; import { SubmissionContentElement } from "@feature-board-submission-element"; import { RichTextContentElement } from "@feature-board-text-element"; @@ -25,6 +27,7 @@ describe("ContentElementList", () => { const mockedEnvConfigModule = createModuleMocks(EnvConfigModule, { getEnv: createMock({ FEATURE_COLUMN_BOARD_SUBMISSIONS_ENABLED: true, + FEATURE_COLUMN_BOARD_EXTERNAL_TOOLS_ENABLED: true, }), }); @@ -32,7 +35,7 @@ describe("ContentElementList", () => { ...createComponentMocks({}), propsData: { ...props }, provide: { - [I18N_KEY.valueOf()]: { t: (key: string) => key }, + [I18N_KEY.valueOf()]: i18nMock, [ENV_CONFIG_MODULE_KEY.valueOf()]: mockedEnvConfigModule, }, }); @@ -60,6 +63,10 @@ describe("ContentElementList", () => { elementType: ContentElementType.SubmissionContainer, component: SubmissionContentElement, }, + { + elementType: ContentElementType.ExternalTool, + component: ExternalToolElement, + }, ])( "should render elements based on type %s", ({ elementType, component }) => { @@ -84,6 +91,10 @@ describe("ContentElementList", () => { elementType: ContentElementType.SubmissionContainer, component: SubmissionContentElement, }, + { + elementType: ContentElementType.ExternalTool, + component: ExternalToolElement, + }, ])( "should propagate isEditMode to child elements", ({ elementType, component }) => { diff --git a/src/components/feature-board/card/ContentElementList.vue b/src/components/feature-board/card/ContentElementList.vue index 98283ea96e..1f49018fb2 100644 --- a/src/components/feature-board/card/ContentElementList.vue +++ b/src/components/feature-board/card/ContentElementList.vue @@ -34,6 +34,19 @@ @move-up:edit="onMoveElementUp(index, element)" @delete:element="onDeleteElement" /> + @@ -41,21 +54,24 @@