diff --git a/src/components/error-handling/ErrorHandler.composable.ts b/src/components/error-handling/ErrorHandler.composable.ts index e071f256e0..f034dc1b70 100644 --- a/src/components/error-handling/ErrorHandler.composable.ts +++ b/src/components/error-handling/ErrorHandler.composable.ts @@ -4,7 +4,7 @@ import { useNotificationStore } from "@data-app"; import { logger } from "@util-logger"; import { useI18n } from "vue-i18n"; -export type ErrorType = "notCreated" | "notLoaded" | "notUpdated" | "notDeleted" | "notMoved"; +export type ErrorType = "notCreated" | "notLoaded" | "notUpdated" | "notDeleted" | "notDuplicated" | "notMoved"; export type BoardObjectType = "board" | "boardColumn" | "boardRow" | "boardCard" | "boardElement"; diff --git a/src/locales/de.ts b/src/locales/de.ts index f15379fd0c..0720a81b51 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -453,14 +453,19 @@ export default { "Die angehängte Datei überschreitet die maximal zulässige Größe von {maxFileSizeWithUnit}.", "components.board.notifications.errors.notCreated": "{type} konnte nicht erstellt werden.", "components.board.notifications.errors.notDeleted": "{type} konnte nicht gelöscht werden.", + "components.board.notifications.errors.notDuplicated": "{type} konnte nicht dupliziert werden.", "components.board.notifications.errors.notLoaded": "{type} konnte nicht geladen werden.", "components.board.notifications.errors.notUpdated": "Die Änderungen konnten nicht gespeichert werden.", + "components.board.notifications.info.cardDuplicated": + "Inhalte aus Etherpads und Whiteboards sowie geschützte Einstellungen externer Tools werden nicht übernommen.", "components.board.screenReader.notification.cardCreated.success": "Eine Karte wurde von einem anderen Benutzer in Abschnitt {columnPosition} erstellt.", "components.board.screenReader.notification.columnCreated.success": "Ein Abschnitt wurde von einem anderen Benutzer erstellt.", "components.board.screenReader.notification.cardDeleted.success": "Eine Karte wurde von einem anderen Benutzer gelöscht.", + "components.board.screenReader.notification.cardDuplicated.success": + "Eine Karte wurde von einem anderen Benutzer dupliziert.", "components.board.screenReader.notification.columnDeleted.success": "Ein Abschnitt wurde von einem anderen Benutzer gelöscht.", "components.board.screenReader.notification.cardMovedInSameColumn.success": diff --git a/src/locales/en.ts b/src/locales/en.ts index 7d2967373e..6784d07851 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -449,12 +449,16 @@ export default { "The attached file exceeds the maximum permitted size of {maxFileSizeWithUnit}.", "components.board.notifications.errors.notCreated": "{type} could not be created.", "components.board.notifications.errors.notDeleted": "{type} could not be deleted.", + "components.board.notifications.errors.notDuplicated": "{type} could not be duplicated.", "components.board.notifications.errors.notLoaded": "{type} could not be loaded.", "components.board.notifications.errors.notUpdated": "Your changes could not be saved.", + "components.board.notifications.info.cardDuplicated": + "Content from Etherpads and whiteboards as well as protected settings from external tools will not be transferred.", "components.board.screenReader.notification.cardCreated.success": "A card was created by another user in column {columnPosition}.", "components.board.screenReader.notification.columnCreated.success": "A column was created by another user.", "components.board.screenReader.notification.cardDeleted.success": "A card was deleted by another user.", + "components.board.screenReader.notification.cardDuplicated.success": "A card was duplicated by another user.", "components.board.screenReader.notification.columnDeleted.success": "A column was deleted by another user.", "components.board.screenReader.notification.cardMovedInSameColumn.success": "In column {columnPosition}, a card was moved to position {newPosition}.", diff --git a/src/locales/es.ts b/src/locales/es.ts index 2a75256949..ea31c3d5d0 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -457,12 +457,16 @@ export default { "Los archivos adjuntos superan el tamaño máximo permitido de {maxFileSizeWithUnit}.", "components.board.notifications.errors.notCreated": "{type} no se ha podido crear.", "components.board.notifications.errors.notDeleted": "{type} no se ha podido eliminar.", + "components.board.notifications.errors.notDuplicated": "{type} no se ha podido duplicar.", "components.board.notifications.errors.notLoaded": "{type} no se ha podido cargar.", "components.board.notifications.errors.notUpdated": "No se han podido guardar los cambios.", + "components.board.notifications.info.cardDuplicated": + "No se transferirán los contenidos de Etherpads y pizarras blancas, ni tampoco los ajustes protegidos de herramientas externas.", "components.board.screenReader.notification.cardCreated.success": "Otro usuario ha creado una tarjeta en la columna {columnPosition}.", "components.board.screenReader.notification.columnCreated.success": "Una columna fue creada por otro usuario.", "components.board.screenReader.notification.cardDeleted.success": "Una tarjeta fue eliminada por otro usuario.", + "components.board.screenReader.notification.cardDuplicated.success": "Una tarjeta fue duplicada por otro usuario.", "components.board.screenReader.notification.columnDeleted.success": "Una columna fue eliminada por otro usuario.", "components.board.screenReader.notification.cardMovedInSameColumn.success": "En la columna {columnPosition}, se ha movido una carta a la posición {newPosition}.", diff --git a/src/locales/uk.ts b/src/locales/uk.ts index 0f78370ba4..643e7813d2 100644 --- a/src/locales/uk.ts +++ b/src/locales/uk.ts @@ -453,12 +453,16 @@ export default { "Вкладені файли перевищують максимально дозволений розмір {maxFileSizeWithUnit}.", "components.board.notifications.errors.notCreated": "{type}: Не вдалося створити.", "components.board.notifications.errors.notDeleted": "{type}: Не вдалося видалити.", + "components.board.notifications.errors.notDuplicated": "{type} не вдалося дублікувати.", "components.board.notifications.errors.notLoaded": "{type}: не вдалося завантажити.", "components.board.notifications.errors.notUpdated": "Зберегти зміни не вдалося.", + "components.board.notifications.info.cardDuplicated": + "IВміст з Etherpads і Whiteboards, а також захищені налаштування зовнішніх інструментів не переносяться.", "components.board.screenReader.notification.cardCreated.success": "Картку було створено іншим користувачем у колонці {columnPosition}.", "components.board.screenReader.notification.columnCreated.success": "Колонку створив інший користувач.", "components.board.screenReader.notification.cardDeleted.success": "Картка була видалена іншим користувачем.", + "components.board.screenReader.notification.cardDuplicated.success": "Картка була дуплікована іншим користувачем.", "components.board.screenReader.notification.columnDeleted.success": "Стовпець був видалений іншим користувачем.", "components.board.screenReader.notification.cardMovedInSameColumn.success": "У колонці {columnPosition} картку було переміщено на позицію {newPosition}.", diff --git a/src/modules/data/board/Board.store.ts b/src/modules/data/board/Board.store.ts index ea6b9e9d05..2b8765b07c 100644 --- a/src/modules/data/board/Board.store.ts +++ b/src/modules/data/board/Board.store.ts @@ -28,7 +28,7 @@ import { useBoardRestApi } from "./boardActions/boardRestApi.composable"; import { useBoardSocketApi } from "./boardActions/boardSocketApi.composable"; import { useBoardFocusHandler } from "./BoardFocusHandler.composable"; import { useCardStore } from "./Card.store"; -import { DeleteCardSuccessPayload } from "./cardActions/cardActionPayload.types"; +import { DeleteCardSuccessPayload, DuplicateCardSuccessPayload } from "./cardActions/cardActionPayload.types"; import { ColumnResponse } from "@/serverApi/v3"; import { HttpStatusCode } from "@/store/types/http-status-code.enum"; import { Board } from "@/types/board/Board"; @@ -134,6 +134,18 @@ export const useBoardStore = defineStore("boardStore", () => { } }; + const duplicateCardSuccess = (payload: DuplicateCardSuccessPayload) => { + if (!board.value) return; + + const { cardId, duplicatedCard } = payload; + const { columnIndex, cardIndex } = getCardLocation(cardId) ?? { columnIndex: 0, cardIndex: 0 }; + + board.value?.columns?.[columnIndex]?.cards?.splice(cardIndex + 1, 0, { + cardId: duplicatedCard.id, + height: duplicatedCard.height, + }); + }; + const deleteCardSuccess = (payload: DeleteCardSuccessPayload) => { if (!board.value) return; const cardId = payload.cardId; @@ -380,6 +392,7 @@ export const useBoardStore = defineStore("boardStore", () => { createColumnSuccess, deleteBoardRequest, deleteBoardSuccess, + duplicateCardSuccess, deleteCardSuccess, deleteColumnRequest, deleteColumnSuccess, diff --git a/src/modules/data/board/Board.store.unit.ts b/src/modules/data/board/Board.store.unit.ts index a2a1aa2ceb..808fa647ae 100644 --- a/src/modules/data/board/Board.store.unit.ts +++ b/src/modules/data/board/Board.store.unit.ts @@ -419,6 +419,39 @@ describe("BoardStore", () => { }); }); + describe("duplicateCardSuccess", () => { + it("should not duplicate card when a board is undefined", () => { + const { boardStore, cards } = setup({ createBoard: false }); + + boardStore.duplicateCardSuccess({ + cardId: cards[0].cardId, + duplicatedCard: cardResponseFactory.build(), + isOwnAction: true, + }); + + expect(boardStore.board).toBe(undefined); + }); + + it("should duplicate a card", () => { + const { boardStore, cards } = setup(); + const firstCardId = cards[0].cardId; + + const duplicatedCard = cardResponseFactory.build(); + boardStore.duplicateCardSuccess({ + cardId: firstCardId, + duplicatedCard, + isOwnAction: true, + }); + + const secondCard = boardStore.board?.columns[0].cards[1]; + + expect(secondCard).toEqual({ + cardId: duplicatedCard.id, + height: duplicatedCard.height, + }); + }); + }); + describe("deleteColumnSuccess", () => { it("should not delete a column when board value is undefined", () => { const { boardStore, firstColumn } = setup({ createBoard: false }); diff --git a/src/modules/data/board/BoardApi.composable.ts b/src/modules/data/board/BoardApi.composable.ts index b9d5348454..4dc5618e58 100644 --- a/src/modules/data/board/BoardApi.composable.ts +++ b/src/modules/data/board/BoardApi.composable.ts @@ -219,6 +219,11 @@ export const useBoardApi = () => { return response.data; }; + const duplicateCardCall = async (cardId: string) => { + const response = await cardsApi.cardControllerCopyCard(cardId); + return response.data; + }; + const moveCardCall = async (cardId: string, toColumnId: string, toPosition: number): Promise => { await cardsApi.cardControllerMoveCard(cardId, { toColumnId, @@ -304,6 +309,7 @@ export const useBoardApi = () => { updateColumnTitleCall, updateElementCall, createCardCall, + duplicateCardCall, getContextInfo, updateBoardLayoutCall, getElementWithParentHierarchyCall, diff --git a/src/modules/data/board/BoardApi.composable.unit.ts b/src/modules/data/board/BoardApi.composable.unit.ts index f0e86a330a..a717613658 100644 --- a/src/modules/data/board/BoardApi.composable.unit.ts +++ b/src/modules/data/board/BoardApi.composable.unit.ts @@ -125,6 +125,18 @@ describe("BoardApi.composable", () => { }); }); + describe("duplicateCardCall", () => { + it("should call cardControllerCopyCard api", async () => { + const { duplicateCardCall } = useBoardApi(); + + const id = "duplicate-card-id"; + + await duplicateCardCall(id); + + expect(cardApi.cardControllerCopyCard).toHaveBeenCalledWith(id); + }); + }); + describe("updateColumnTitleCall", () => { it("should call columnControllerUpdateColumnTitle api", async () => { const { updateColumnTitleCall } = useBoardApi(); diff --git a/src/modules/data/board/Card.store.ts b/src/modules/data/board/Card.store.ts index 36df04f3b8..a7ee000bb1 100644 --- a/src/modules/data/board/Card.store.ts +++ b/src/modules/data/board/Card.store.ts @@ -5,6 +5,7 @@ import { CreateElementSuccessPayload, DeleteCardSuccessPayload, DeleteElementSuccessPayload, + DuplicateCardSuccessPayload, FetchCardSuccessPayload, MoveElementSuccessPayload, UpdateCardHeightSuccessPayload, @@ -14,6 +15,7 @@ import { import { useCardRestApi } from "./cardActions/cardRestApi.composable"; import { useCardSocketApi } from "./cardActions/cardSocketApi.composable"; import { CardResponse, ContentElementType, PreferredToolResponse, ToolContextType } from "@/serverApi/v3"; +import { notifyInfo } from "@data-app"; import { useEnvConfig } from "@data-env"; import { useSharedEditMode, useSharedLastCreatedElement } from "@util-board"; import { defineStore } from "pinia"; @@ -56,7 +58,7 @@ export const useCardStore = defineStore("cardStore", () => { const updateCardTitleRequest = socketOrRest.updateCardTitleRequest; - const updateCardTitleSuccess = async (payload: UpdateCardTitleSuccessPayload) => { + const updateCardTitleSuccess = (payload: UpdateCardTitleSuccessPayload) => { const card = cards.value[payload.cardId]; if (card === undefined) return; @@ -65,16 +67,25 @@ export const useCardStore = defineStore("cardStore", () => { const updateCardHeightRequest = socketOrRest.updateCardHeightRequest; - const updateCardHeightSuccess = async (payload: UpdateCardHeightSuccessPayload) => { + const updateCardHeightSuccess = (payload: UpdateCardHeightSuccessPayload) => { const card = cards.value[payload.cardId]; if (card === undefined) return; card.height = payload.newHeight; }; + const duplicateCard = socketOrRest.duplicateCardRequest; + + const duplicateCardSuccess = (payload: DuplicateCardSuccessPayload) => { + if (payload.duplicatedCard.id) { + cards.value[payload.duplicatedCard.id] = payload.duplicatedCard; + notifyInfo("components.board.notifications.info.cardDuplicated"); + } + }; + const deleteCardRequest = socketOrRest.deleteCardRequest; - const deleteCardSuccess = async (payload: DeleteCardSuccessPayload) => { + const deleteCardSuccess = (payload: DeleteCardSuccessPayload) => { const card = cards.value[payload.cardId]; if (card === undefined) return; @@ -86,11 +97,11 @@ export const useCardStore = defineStore("cardStore", () => { const createElementRequest = socketOrRest.createElementRequest; - const createPreferredElement = async (payload: CreateElementRequestPayload, tool: PreferredToolResponse) => { + const createPreferredElement = (payload: CreateElementRequestPayload, tool: PreferredToolResponse) => { restApi.createPreferredElement(payload, tool); }; - const createElementSuccess = async (payload: CreateElementSuccessPayload) => { + const createElementSuccess = (payload: CreateElementSuccessPayload) => { const card = cards.value[payload.cardId]; if (card === undefined) return; @@ -128,7 +139,7 @@ export const useCardStore = defineStore("cardStore", () => { if (toPosition < 0) return; if (toPosition >= card.elements.length) return; - socketOrRest.moveElementRequest({ + await socketOrRest.moveElementRequest({ elementId, toCardId: cardId, toPosition, @@ -151,7 +162,7 @@ export const useCardStore = defineStore("cardStore", () => { const deleteElementRequest = socketOrRest.deleteElementRequest; - const deleteElementSuccess = async (payload: DeleteElementSuccessPayload): Promise => { + const deleteElementSuccess = (payload: DeleteElementSuccessPayload) => { const card = cards.value[payload.cardId]; if (card === undefined) return; @@ -172,7 +183,7 @@ export const useCardStore = defineStore("cardStore", () => { const updateElementRequest = socketOrRest.updateElementRequest; - const updateElementSuccess = async (payload: UpdateElementSuccessPayload) => { + const updateElementSuccess = (payload: UpdateElementSuccessPayload) => { const cardToUpdate = Object.values(cards.value).find((c) => c.elements.some((e) => e.id === payload.elementId)); if (cardToUpdate === undefined) return; const cardId = cardToUpdate.id; @@ -201,7 +212,7 @@ export const useCardStore = defineStore("cardStore", () => { return previousElement.id; }; - const loadPreferredTools = async (contextType: ToolContextType): Promise => { + const loadPreferredTools = async (contextType: ToolContextType) => { isPreferredToolsLoading.value = true; preferredTools.value = (await restApi.getPreferredTools(contextType)) || []; @@ -226,6 +237,8 @@ export const useCardStore = defineStore("cardStore", () => { fetchCardRequest, fetchCardSuccess, cards, + duplicateCard, + duplicateCardSuccess, deleteCardRequest, deleteCardSuccess, getCard, diff --git a/src/modules/data/board/Card.store.unit.ts b/src/modules/data/board/Card.store.unit.ts index 25ad041052..74b16e2b23 100644 --- a/src/modules/data/board/Card.store.unit.ts +++ b/src/modules/data/board/Card.store.unit.ts @@ -21,10 +21,6 @@ import { cloneDeep } from "lodash-es"; import { createPinia, setActivePinia } from "pinia"; import type { Mock } from "vitest"; import { computed, Ref, ref } from "vue"; -import { useI18n } from "vue-i18n"; - -vi.mock("vue-i18n"); -(useI18n as Mock).mockReturnValue({ t: (key: string) => key }); vi.mock("@data-board/BoardApi.composable"); const mockedUseBoardApi = vi.mocked(useBoardApi); @@ -79,6 +75,7 @@ describe("CardStore", () => { updateElementRequest: vi.fn(), moveElementRequest: vi.fn(), deleteCardRequest: vi.fn(), + duplicateCardRequest: vi.fn(), updateCardTitleRequest: vi.fn(), updateCardHeightRequest: vi.fn(), disconnectSocketRequest: vi.fn(), @@ -94,6 +91,7 @@ describe("CardStore", () => { updateElementRequest: vi.fn(), moveElementRequest: vi.fn(), deleteCardRequest: vi.fn(), + duplicateCardRequest: vi.fn(), updateCardTitleRequest: vi.fn(), updateCardHeightRequest: vi.fn(), disconnectSocketRequest: vi.fn(), @@ -192,12 +190,12 @@ describe("CardStore", () => { describe("createCardSuccess", () => { describe("when card is provided", () => { - it("should add the card to the store", async () => { + it("should add the card to the store", () => { const { cardStore } = setup(); const newCardId = "idNewCard"; const newCard = cardResponseFactory.build({ id: newCardId }); - await cardStore.createCardSuccess({ + cardStore.createCardSuccess({ newCard, columnId: "any-column-id", isOwnAction: true, @@ -236,7 +234,7 @@ describe("CardStore", () => { }); describe("deleteCardSuccess", () => { - it("should not delete any card when card is undefined", async () => { + it("should not delete any card when card is undefined", () => { const { cardStore } = setup(); const oldCards = cloneDeep(cardStore.cards); @@ -249,7 +247,7 @@ describe("CardStore", () => { expect(cardStore.cards).toEqual(oldCards); }); - it("set editModeId to undefined if cardId is equal to editModeId", async () => { + it("set editModeId to undefined if cardId is equal to editModeId", () => { const { cardStore, cardId } = setup(); editModeId.value = cardId; @@ -262,7 +260,7 @@ describe("CardStore", () => { expect(setEditModeId).toHaveBeenCalledWith(undefined); }); - it("should delete a card", async () => { + it("should delete a card", () => { const { cardStore, cardId } = setup(); cardStore.deleteCardSuccess({ @@ -274,6 +272,62 @@ describe("CardStore", () => { }); }); + describe("duplicateCardRequest", () => { + it("should call socket Api if feature flag is enabled", () => { + const { cardStore, cardId } = setup(true); + + cardStore.duplicateCard({ + cardId, + }); + + expect(mockedCardSocketApiActions.duplicateCardRequest).toHaveBeenCalledWith({ + cardId, + }); + }); + + it("should call rest Api if feature flag is enabled", () => { + const { cardStore, cardId } = setup(); + + cardStore.duplicateCard({ + cardId, + }); + + expect(mockedCardRestApiActions.duplicateCardRequest).toHaveBeenCalledWith({ + cardId, + }); + }); + }); + + describe("duplicateCardSuccess", () => { + it("should not duplicate card when card is undefined", () => { + const { cardStore } = setup(); + + const oldCards = cloneDeep(cardStore.cards); + + cardStore.duplicateCardSuccess({ + cardId: "unknownId", + duplicatedCard: cardResponseFactory.build({ id: undefined }), + isOwnAction: true, + }); + + expect(cardStore.cards).toEqual(oldCards); + }); + + it("should duplicate card", () => { + const { cardStore } = setup(); + + const cardId = "newCardId"; + const duplicatedCard = cardResponseFactory.build({ id: cardId }); + cardStore.duplicateCardSuccess({ + cardId: "unknownId", + duplicatedCard, + isOwnAction: true, + }); + + expect(cardStore.cards[cardId]).toEqual(duplicatedCard); + }); + }); + describe("updateCardTitleRequest", () => { it("should call socket Api if feature flag is enabled", () => { const { cardStore, cardId } = setup(true); @@ -296,7 +350,7 @@ describe("CardStore", () => { describe("updateCardTitleSuccess", () => { const NEW_TITLE = "newTitle"; - it("should not update card title when card is undefined", async () => { + it("should not update card title when card is undefined", () => { const { cardStore } = setup(); const cardTitles = Object.values(cardStore.cards).map((card) => card.title); @@ -310,7 +364,7 @@ describe("CardStore", () => { expect(Object.values(cardStore.cards).map((card) => card.title)).toEqual(cardTitles); }); - it("should update card title", async () => { + it("should update card title", () => { const { cardStore, cardId } = setup(); cardStore.updateCardTitleSuccess({ @@ -345,7 +399,7 @@ describe("CardStore", () => { describe("updateCardHeightSuccess", () => { const NEW_HEIGHT = 100; - it("should not update card height when card is undefined", async () => { + it("should not update card height when card is undefined", () => { const { cardStore } = setup(); const cardHeights = Object.values(cardStore.cards).map((card) => card.height); @@ -359,7 +413,7 @@ describe("CardStore", () => { expect(Object.values(cardStore.cards).map((card) => card.height)).toEqual(cardHeights); }); - it("should update card height", async () => { + it("should update card height", () => { const { cardStore, cardId } = setup(); cardStore.updateCardHeightSuccess({ @@ -420,12 +474,12 @@ describe("CardStore", () => { describe("createElementSuccess", () => { describe("when element is provided", () => { - it("should add element to specified position", async () => { + it("should add element to specified position", () => { const { cardStore, cardId } = setup(); const newElement = drawingElementResponseFactory.build(); const toPosition = 1; - await cardStore.createElementSuccess({ + cardStore.createElementSuccess({ type: ContentElementType.Drawing, cardId, newElement, diff --git a/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.ts b/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.ts index 6c99589b5d..077854df2b 100644 --- a/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.ts +++ b/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.ts @@ -16,6 +16,7 @@ import { CreateElementSuccessPayload, DeleteCardSuccessPayload, DeleteElementSuccessPayload, + DuplicateCardSuccessPayload, MoveElementSuccessPayload, UpdateCardTitleSuccessPayload, UpdateElementSuccessPayload, @@ -30,6 +31,7 @@ export const SR_I18N_KEYS_MAP = { CARD_CREATED_SUCCESS: "components.board.screenReader.notification.cardCreated.success", COLUMN_CREATED_SUCCESS: "components.board.screenReader.notification.columnCreated.success", CARD_DELETED_SUCCESS: "components.board.screenReader.notification.cardDeleted.success", + CARD_DUPLICATED_SUCCESS: "components.board.screenReader.notification.cardDuplicated.success", COLUMN_DELETED_SUCCESS: "components.board.screenReader.notification.columnDeleted.success", CARD_MOVED_IN_SAME_COLUMN_SUCCESS: "components.board.screenReader.notification.cardMovedInSameColumn.success", CARD_MOVED_TO_ANOTHER_COLUMN_SUCCESS: "components.board.screenReader.notification.cardMovedToAnotherColumn.success", @@ -90,6 +92,13 @@ export const useBoardAriaNotification = () => { notifyOnScreenReader(t(SR_I18N_KEYS_MAP.CARD_DELETED_SUCCESS)); }; + const notifyDuplicateCardSuccess = (action: DuplicateCardSuccessPayload) => { + const { isOwnAction } = action; + if (isOwnAction) return; + + notifyOnScreenReader(t(SR_I18N_KEYS_MAP.CARD_DUPLICATED_SUCCESS)); + }; + const notifyDeleteColumnSuccess = (action: DeleteColumnSuccessPayload) => { const { isOwnAction } = action; if (isOwnAction) return; @@ -299,6 +308,7 @@ export const useBoardAriaNotification = () => { notifyCreateColumnSuccess, notifyCreateElementSuccess, notifyDeleteCardSuccess, + notifyDuplicateCardSuccess, notifyDeleteColumnSuccess, notifyDeleteElementSuccess, notifyMoveCardSuccess, diff --git a/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.unit.ts b/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.unit.ts index b3c1df5000..dae9b8acf6 100644 --- a/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.unit.ts +++ b/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.unit.ts @@ -91,6 +91,19 @@ describe("useBoardAriaNotification", () => { expect(element?.innerHTML).toContain(SR_I18N_KEYS_MAP.CARD_DELETED_SUCCESS); }); + it("should notify on cardDuplicate", () => { + const { notifyDuplicateCardSuccess } = useBoardAriaNotification(); + const element = document.getElementById("notify-screen-reader-polite"); + + notifyDuplicateCardSuccess({ + cardId: "unknown-id", + duplicatedCard: cardResponseFactory.build(), + isOwnAction: false, + }); + vi.advanceTimersByTime(3000); + expect(element?.innerHTML).toContain(SR_I18N_KEYS_MAP.CARD_DUPLICATED_SUCCESS); + }); + it("should notify on columnDelete", () => { const { notifyDeleteColumnSuccess } = useBoardAriaNotification(); const element = document.getElementById("notify-screen-reader-polite"); diff --git a/src/modules/data/board/boardActions/boardSocketApi.composable.ts b/src/modules/data/board/boardActions/boardSocketApi.composable.ts index 06b917eb24..6265b73a74 100644 --- a/src/modules/data/board/boardActions/boardSocketApi.composable.ts +++ b/src/modules/data/board/boardActions/boardSocketApi.composable.ts @@ -45,6 +45,7 @@ export const useBoardSocketApi = () => { const successActions = [ on(BoardActions.createCardSuccess, boardStore.createCardSuccess), on(BoardActions.createColumnSuccess, boardStore.createColumnSuccess), + on(CardActions.duplicateCardSuccess, boardStore.duplicateCardSuccess), on(CardActions.deleteCardSuccess, boardStore.deleteCardSuccess), on(BoardActions.deleteColumnSuccess, boardStore.deleteColumnSuccess), on(BoardActions.deleteBoardSuccess, boardStore.deleteBoardSuccess), diff --git a/src/modules/data/board/cardActions/cardActionPayload.types.ts b/src/modules/data/board/cardActions/cardActionPayload.types.ts index e707d5bde7..a88eae983d 100644 --- a/src/modules/data/board/cardActions/cardActionPayload.types.ts +++ b/src/modules/data/board/cardActions/cardActionPayload.types.ts @@ -57,6 +57,16 @@ export type DeleteElementSuccessPayload = { }; export type DeleteElementFailurePayload = DeleteElementRequestPayload; +export type DuplicateCardRequestPayload = { + cardId: string; +}; +export type DuplicateCardSuccessPayload = { + cardId: string; + duplicatedCard: CardResponse; + isOwnAction: boolean; +}; +export type DuplicateCardFailurePayload = DuplicateCardRequestPayload; + export type DeleteCardRequestPayload = { cardId: string; }; diff --git a/src/modules/data/board/cardActions/cardActions.ts b/src/modules/data/board/cardActions/cardActions.ts index 1eddf373f7..91b4ecc453 100644 --- a/src/modules/data/board/cardActions/cardActions.ts +++ b/src/modules/data/board/cardActions/cardActions.ts @@ -9,6 +9,9 @@ import { DeleteElementRequestPayload, DeleteElementSuccessPayload, DisconnectSocketRequestPayload, + DuplicateCardFailurePayload, + DuplicateCardRequestPayload, + DuplicateCardSuccessPayload, FetchCardFailurePayload, FetchCardRequestPayload, FetchCardSuccessPayload, @@ -57,6 +60,10 @@ export const updateCardTitleRequest = createAction("update-card-title-request", export const updateCardTitleSuccess = createAction("update-card-title-success", props()); export const updateCardTitleFailure = createAction("update-card-title-failure", props()); +export const duplicateCardRequest = createAction("duplicate-card-request", props()); +export const duplicateCardSuccess = createAction("duplicate-card-success", props()); +export const duplicateCardFailure = createAction("duplicate-card-failure", props()); + export const updateCardHeightRequest = createAction( "update-card-height-request", props() diff --git a/src/modules/data/board/cardActions/cardRestApi.composable.ts b/src/modules/data/board/cardActions/cardRestApi.composable.ts index b12b4e18b7..11d681480c 100644 --- a/src/modules/data/board/cardActions/cardRestApi.composable.ts +++ b/src/modules/data/board/cardActions/cardRestApi.composable.ts @@ -6,6 +6,7 @@ import { CreateElementRequestPayload, DeleteCardRequestPayload, DeleteElementRequestPayload, + DuplicateCardRequestPayload, FetchCardRequestPayload, MoveElementRequestPayload, UpdateCardHeightRequestPayload, @@ -54,6 +55,7 @@ export const useCardRestApi = () => { moveElementCall, updateCardTitle, updateCardHeightCall, + duplicateCardCall, } = useBoardApi(); const { fetchPreferredTools } = useContextExternalToolApi(); @@ -216,6 +218,24 @@ export const useCardRestApi = () => { } }; + const duplicateCardRequest = async (payload: DuplicateCardRequestPayload) => { + const card = cardStore.getCard(payload.cardId); + if (card === undefined) return; + + try { + const duplicatedCard = await duplicateCardCall(payload.cardId); + + if (duplicatedCard.id) { + boardStore.duplicateCardSuccess({ cardId: payload.cardId, duplicatedCard, isOwnAction: true }); + cardStore.duplicateCardSuccess({ cardId: payload.cardId, duplicatedCard, isOwnAction: true }); + } + } catch (error) { + handleError(error, { + 404: notifyWithTemplateAndReload("notDuplicated", "boardCard"), + }); + } + }; + const fetchCardRequest = async (payload: FetchCardRequestPayload): Promise => { await delay(100); try { @@ -275,6 +295,7 @@ export const useCardRestApi = () => { deleteElementRequest, moveElementRequest, updateElementRequest, + duplicateCardRequest, deleteCardRequest, fetchCardRequest, updateCardTitleRequest, diff --git a/src/modules/data/board/cardActions/cardRestApi.composable.unit.ts b/src/modules/data/board/cardActions/cardRestApi.composable.unit.ts index 36884befd2..8b40b8a971 100644 --- a/src/modules/data/board/cardActions/cardRestApi.composable.unit.ts +++ b/src/modules/data/board/cardActions/cardRestApi.composable.unit.ts @@ -704,6 +704,50 @@ describe("useCardRestApi", () => { }); }); + describe("duplicateCardRequest", () => { + it("should not call duplicateCardSuccess action when card is undefined", async () => { + const { cardStore } = setup(); + const { duplicateCardRequest } = useCardRestApi(); + + cardStore.getCard.mockReturnValue(undefined); + + await duplicateCardRequest({ cardId: "cardId" }); + + expect(cardStore.duplicateCardSuccess).not.toHaveBeenCalled(); + }); + + it("should call duplicateCardSuccess action if the API call is successful", async () => { + const { cardStore, card } = setup(); + const { duplicateCardRequest } = useCardRestApi(); + const cardId = card.id; + + cardStore.getCard.mockReturnValue(card); + const duplicatedCard = { ...card, id: "newCardId" }; + mockedBoardApiCalls.duplicateCardCall.mockResolvedValue(duplicatedCard); + + await duplicateCardRequest({ cardId }); + + expect(cardStore.duplicateCardSuccess).toHaveBeenCalledWith({ + cardId, + duplicatedCard, + isOwnAction: true, + }); + }); + + it("should call handleError if the API call fails", async () => { + const { cardStore, card } = setup(); + const { duplicateCardRequest } = useCardRestApi(); + const cardId = card.id; + + cardStore.getCard.mockReturnValue(card); + mockedBoardApiCalls.duplicateCardCall.mockRejectedValue({}); + + await duplicateCardRequest({ cardId }); + + expect(mockedErrorHandler.handleError).toHaveBeenCalled(); + }); + }); + describe("fetchCardRequest", () => { it("should call fetchCardSuccess action if the API call is successful", async () => { vi.useFakeTimers(); diff --git a/src/modules/data/board/cardActions/cardSocketApi.composable.ts b/src/modules/data/board/cardActions/cardSocketApi.composable.ts index e781ba7140..00b5f76453 100644 --- a/src/modules/data/board/cardActions/cardSocketApi.composable.ts +++ b/src/modules/data/board/cardActions/cardSocketApi.composable.ts @@ -6,6 +6,7 @@ import { CreateElementRequestPayload, DeleteCardRequestPayload, DeleteElementRequestPayload, + DuplicateCardRequestPayload, FetchCardRequestPayload, MoveElementRequestPayload, UpdateCardHeightRequestPayload, @@ -34,6 +35,7 @@ export const useCardSocketApi = () => { notifyUpdateCardTitleSuccess, notifyCreateElementSuccess, notifyDeleteElementSuccess, + notifyDuplicateCardSuccess, notifyMoveElementSuccess, notifyUpdateElementSuccess, } = useBoardAriaNotification(); @@ -48,6 +50,7 @@ export const useCardSocketApi = () => { on(CardActions.fetchCardSuccess, cardStore.fetchCardSuccess), on(CardActions.updateCardTitleSuccess, cardStore.updateCardTitleSuccess), on(CardActions.updateCardHeightSuccess, cardStore.updateCardHeightSuccess), + on(CardActions.duplicateCardSuccess, cardStore.duplicateCardSuccess), ]; const failureActions = [ @@ -58,6 +61,7 @@ export const useCardSocketApi = () => { on(CardActions.fetchCardFailure, ({ cardIds }) => reloadBoard(cardIds[0])), on(CardActions.updateCardTitleFailure, ({ cardId }) => reloadBoard(cardId)), on(CardActions.deleteCardFailure, ({ cardId }) => reloadBoard(cardId)), + on(CardActions.duplicateCardFailure, ({ cardId }) => reloadBoard(cardId)), ]; const ariaLiveNotification = [ @@ -66,6 +70,7 @@ export const useCardSocketApi = () => { on(CardActions.deleteElementSuccess, notifyDeleteElementSuccess), on(CardActions.moveElementSuccess, notifyMoveElementSuccess), on(CardActions.updateElementSuccess, notifyUpdateElementSuccess), + on(CardActions.duplicateCardSuccess, notifyDuplicateCardSuccess), ]; handle( @@ -139,6 +144,10 @@ export const useCardSocketApi = () => { emitOnSocket("update-card-height-request", payload); }; + const duplicateCardRequest = (payload: DuplicateCardRequestPayload) => { + emitOnSocket("duplicate-card-request", payload); + }; + const reloadBoard = (cardId = "") => { const boardStore = useBoardStore(); const { board } = storeToRefs(boardStore); @@ -164,5 +173,6 @@ export const useCardSocketApi = () => { fetchCardRequest, updateCardTitleRequest, updateCardHeightRequest, + duplicateCardRequest, }; }; diff --git a/src/modules/data/board/cardActions/cardSocketApi.composable.unit.ts b/src/modules/data/board/cardActions/cardSocketApi.composable.unit.ts index 4be201d450..66f5dfbf3a 100644 --- a/src/modules/data/board/cardActions/cardSocketApi.composable.unit.ts +++ b/src/modules/data/board/cardActions/cardSocketApi.composable.unit.ts @@ -4,6 +4,8 @@ import { DeleteCardFailurePayload, DeleteCardRequestPayload, DeleteElementFailurePayload, + DuplicateCardFailurePayload, + DuplicateCardRequestPayload, FetchCardFailurePayload, MoveElementFailurePayload, UpdateCardHeightFailurePayload, @@ -155,6 +157,20 @@ describe("useCardSocketApi", () => { expect(cardStore.deleteCardSuccess).toHaveBeenCalledWith(payload); }); + it("should call duplicateCardSuccess for corresponding action", () => { + const cardStore = mockedPiniaStoreTyping(useCardStore); + const { dispatch } = useCardSocketApi(); + + const payload = { + cardId: "unknown-id", + duplicatedCard: cardResponseFactory.build(), + isOwnAction: true, + }; + dispatch(CardActions.duplicateCardSuccess(payload)); + + expect(cardStore.duplicateCardSuccess).toHaveBeenCalledWith(payload); + }); + it("should call fetchCardSuccess for corresponding action", () => { const cardStore = mockedPiniaStoreTyping(useCardStore); const { dispatch } = useCardSocketApi(); @@ -278,6 +294,17 @@ describe("useCardSocketApi", () => { expect(boardStore.reloadBoard).toHaveBeenCalled(); }); + it("should reload the board for duplicateCardFailure action", () => { + const { dispatch, boardStore } = setupWithFakeBoard(); + + const payload: DuplicateCardFailurePayload = { + cardId: "cardId", + }; + dispatch(CardActions.duplicateCardFailure(payload)); + + expect(boardStore.reloadBoard).toHaveBeenCalled(); + }); + it("should reload the board for fetchCardFailure action", () => { const { dispatch, boardStore } = setupWithFakeBoard(); @@ -401,6 +428,17 @@ describe("useCardSocketApi", () => { }); }); + describe("duplicateCardRequest", () => { + it("should call emitOnSocket with correct parameters", () => { + const payload: DuplicateCardRequestPayload = { cardId: "cardId" }; + const { duplicateCardRequest } = useCardSocketApi(); + + duplicateCardRequest(payload); + + expect(socketMock.emitOnSocket).toHaveBeenCalledWith("duplicate-card-request", payload); + }); + }); + describe("updateCardTitleRequest", () => { const payload = { cardId: "cardId", diff --git a/src/modules/feature/board/board/BoardHeader.unit.ts b/src/modules/feature/board/board/BoardHeader.unit.ts index 1f402727bf..90ced7590e 100644 --- a/src/modules/feature/board/board/BoardHeader.unit.ts +++ b/src/modules/feature/board/board/BoardHeader.unit.ts @@ -9,8 +9,8 @@ import { useBoardFocusHandler, useBoardPermissions } from "@data-board"; import { createTestingPinia } from "@pinia/testing"; import { KebabMenuActionChangeLayout, - KebabMenuActionCopy, KebabMenuActionDelete, + KebabMenuActionDuplicate, KebabMenuActionPublish, KebabMenuActionRename, KebabMenuActionRevert, @@ -234,8 +234,8 @@ describe("BoardHeader", () => { it("should emit 'copy:board'", async () => { const { wrapper } = setup(); - const copyButton = wrapper.findComponent(KebabMenuActionCopy); - await copyButton.trigger("click"); + const duplicateButton = wrapper.findComponent(KebabMenuActionDuplicate); + await duplicateButton.trigger("click"); expect(wrapper.emitted("copy:board")).toHaveLength(1); }); diff --git a/src/modules/feature/board/board/BoardHeader.vue b/src/modules/feature/board/board/BoardHeader.vue index 166786d42c..f6b1e76607 100644 --- a/src/modules/feature/board/board/BoardHeader.vue +++ b/src/modules/feature/board/board/BoardHeader.vue @@ -28,7 +28,7 @@ - + @@ -57,8 +57,8 @@ import { useEnvConfig } from "@data-env"; import { BoardMenuScope } from "@ui-board"; import { KebabMenuActionChangeLayout, - KebabMenuActionCopy, KebabMenuActionDelete, + KebabMenuActionDuplicate, KebabMenuActionPublish, KebabMenuActionRename, KebabMenuActionRevert, diff --git a/src/modules/feature/board/card/CardHost.unit.ts b/src/modules/feature/board/card/CardHost.unit.ts index f3b4ebcfb2..977a8cb1f8 100644 --- a/src/modules/feature/board/card/CardHost.unit.ts +++ b/src/modules/feature/board/card/CardHost.unit.ts @@ -3,7 +3,6 @@ import CardHost from "./CardHost.vue"; import ContentElementList from "./ContentElementList.vue"; import { CardResponse } from "@/serverApi/v3"; import { BoardPermissionChecks, defaultPermissions } from "@/types/board/Permissions"; -import { mockedPiniaStoreTyping } from "@@/tests/test-utils"; import setupDeleteConfirmationComposableMock from "@@/tests/test-utils/composable-mocks/setupDeleteConfirmationComposableMock"; import { cardResponseFactory, fileElementResponseFactory } from "@@/tests/test-utils/factory"; import { createTestingI18n, createTestingVuetify } from "@@/tests/test-utils/setup"; @@ -12,7 +11,12 @@ import { createMock, DeepMocked } from "@golevelup/ts-vitest"; import { createTestingPinia } from "@pinia/testing"; import { BoardMenuScope } from "@ui-board"; import { useDeleteConfirmationDialog } from "@ui-confirmation-dialog"; -import { KebabMenuActionDelete, KebabMenuActionEdit, KebabMenuActionShareLink } from "@ui-kebab-menu"; +import { + KebabMenuActionDelete, + KebabMenuActionDuplicate, + KebabMenuActionEdit, + KebabMenuActionShareLink, +} from "@ui-kebab-menu"; import { useCourseBoardEditMode, useShareBoardLink, useSharedEditMode, useSharedLastCreatedElement } from "@util-board"; import { shallowMount } from "@vue/test-utils"; import { computed, ref } from "vue"; @@ -129,8 +133,6 @@ describe("CardHost", () => { }, }); - mockedPiniaStoreTyping(useCardStore); - return { wrapper, cardId, @@ -191,6 +193,19 @@ describe("CardHost", () => { }); describe("card menus", () => { + describe("when users clicks duplicate menu btn", () => { + it("should call cardStore.duplicateCardRequest", async () => { + mockedBoardPermissions.hasEditPermission.value = true; + const { wrapper, cardId } = setup(); + + const duplicateButton = wrapper.findComponent(KebabMenuActionDuplicate); + + await duplicateButton.trigger("click"); + + expect(useCardStore().duplicateCard).toHaveBeenCalledWith({ cardId }); + }); + }); + describe("when users clicks share link menu", () => { it("should copy a share link", async () => { mockedBoardPermissions.hasDeletePermission.value = true; diff --git a/src/modules/feature/board/card/CardHost.vue b/src/modules/feature/board/card/CardHost.vue index fabe081d5f..d8574d6092 100644 --- a/src/modules/feature/board/card/CardHost.vue +++ b/src/modules/feature/board/card/CardHost.vue @@ -42,6 +42,11 @@ :data-testid="boardMenuTestId" > + { const onCloseDetailView = () => (isDetailView.value = false); const onMoveContentElementDown = async ({ payload: elementId, elementIndex }: ElementMove) => - cardStore.moveElementRequest(props.cardId, elementId, elementIndex, +1); + await cardStore.moveElementRequest(props.cardId, elementId, elementIndex, +1); const onMoveContentElementUp = async ({ payload: elementId, elementIndex }: ElementMove) => - cardStore.moveElementRequest(props.cardId, elementId, elementIndex, -1); + await cardStore.moveElementRequest(props.cardId, elementId, elementIndex, -1); const onMoveContentElementKeyboard = async ({ payload: elementId, elementIndex }: ElementMove, key: string) => { if (!verticalCursorKeys.includes(key)) return; @@ -212,6 +222,12 @@ const boardMenuClasses = computed(() => { return "hidden"; }); +const duplicateCard = async () => { + if (!card.value) return; + + await cardStore.duplicateCard({ cardId: card.value.id }); +}; + onMounted(async () => { if (card.value === undefined) { await cardStore.fetchCardRequest({ cardIds: [cardId.value] }); diff --git a/src/modules/ui/kebab-menu/KebabMenuActionDuplicate.unit.ts b/src/modules/ui/kebab-menu/KebabMenuActionDuplicate.unit.ts new file mode 100644 index 0000000000..5983f88732 --- /dev/null +++ b/src/modules/ui/kebab-menu/KebabMenuActionDuplicate.unit.ts @@ -0,0 +1,39 @@ +import { createTestingI18n, createTestingVuetify } from "@@/tests/test-utils/setup"; +import { KebabMenuActionDuplicate } from "@ui-kebab-menu"; +import { mount } from "@vue/test-utils"; + +describe("KebabMenuActionDuplicate", () => { + it("should render component", () => { + const wrapper = mount(KebabMenuActionDuplicate, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + }); + + expect(wrapper.exists()).toBe(true); + expect(wrapper.text()).toBe("common.actions.duplicate"); + }); + + it("should set generic data-testid", () => { + const wrapper = mount(KebabMenuActionDuplicate, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + }); + + expect(wrapper.attributes("data-testid")).toBe("kebab-menu-action-duplicate"); + }); + + it("should set custom data-testid", () => { + const wrapper = mount(KebabMenuActionDuplicate, { + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + attrs: { + "data-testid": "custom-duplicate-action", + }, + }); + + expect(wrapper.attributes("data-testid")).toBe("custom-duplicate-action"); + }); +}); diff --git a/src/modules/ui/kebab-menu/KebabMenuActionCopy.vue b/src/modules/ui/kebab-menu/KebabMenuActionDuplicate.vue similarity index 59% rename from src/modules/ui/kebab-menu/KebabMenuActionCopy.vue rename to src/modules/ui/kebab-menu/KebabMenuActionDuplicate.vue index 93cbcf64ee..d882d4c9fc 100644 --- a/src/modules/ui/kebab-menu/KebabMenuActionCopy.vue +++ b/src/modules/ui/kebab-menu/KebabMenuActionDuplicate.vue @@ -1,5 +1,5 @@ @@ -7,6 +7,10 @@ diff --git a/src/modules/ui/kebab-menu/index.ts b/src/modules/ui/kebab-menu/index.ts index 0f48b9ca35..d7652a5090 100644 --- a/src/modules/ui/kebab-menu/index.ts +++ b/src/modules/ui/kebab-menu/index.ts @@ -2,9 +2,9 @@ import KebabMenu from "./KebabMenu.vue"; import KebabMenuAction from "./KebabMenuAction.vue"; import KebabMenuActionChangeLayout from "./KebabMenuActionChangeLayout.vue"; import KebabMenuActionChangePermission from "./KebabMenuActionChangePermission.vue"; -import KebabMenuActionCopy from "./KebabMenuActionCopy.vue"; import KebabMenuActionDelete from "./KebabMenuActionDelete.vue"; import KebabMenuActionDeleteMemberInvitation from "./KebabMenuActionDeleteMemberInvitation.vue"; +import KebabMenuActionDuplicate from "./KebabMenuActionDuplicate.vue"; import KebabMenuActionEdit from "./KebabMenuActionEdit.vue"; import KebabMenuActionLeaveRoom from "./KebabMenuActionLeaveRoom.vue"; import KebabMenuActionMoveDown from "./KebabMenuActionMoveDown.vue"; @@ -26,9 +26,9 @@ export { KebabMenuAction, KebabMenuActionChangeLayout, KebabMenuActionChangePermission, - KebabMenuActionCopy, KebabMenuActionDelete, KebabMenuActionDeleteMemberInvitation, + KebabMenuActionDuplicate, KebabMenuActionEdit, KebabMenuActionLeaveRoom, KebabMenuActionMoveDown, diff --git a/src/serverApi/v3/api.ts b/src/serverApi/v3/api.ts index f42905f7e6..342fb49dbd 100644 --- a/src/serverApi/v3/api.ts +++ b/src/serverApi/v3/api.ts @@ -14467,6 +14467,44 @@ export class BoardApi extends BaseAPI implements BoardApiInterface { */ export const BoardCardApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @summary Copy a single card. + * @param {string} cardId The id of the card. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cardControllerCopyCard: async (cardId: string, options: any = {}): Promise => { + // verify required parameter 'cardId' is not null or undefined + assertParamExists('cardControllerCopyCard', 'cardId', cardId) + const localVarPath = `/cards/{cardId}/copy` + .replace(`{${"cardId"}}`, encodeURIComponent(String(cardId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Create a new element on a card. @@ -14732,6 +14770,17 @@ export const BoardCardApiAxiosParamCreator = function (configuration?: Configura export const BoardCardApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = BoardCardApiAxiosParamCreator(configuration) return { + /** + * + * @summary Copy a single card. + * @param {string} cardId The id of the card. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async cardControllerCopyCard(cardId: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.cardControllerCopyCard(cardId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Create a new element on a card. @@ -14812,6 +14861,16 @@ export const BoardCardApiFp = function(configuration?: Configuration) { export const BoardCardApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = BoardCardApiFp(configuration) return { + /** + * + * @summary Copy a single card. + * @param {string} cardId The id of the card. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cardControllerCopyCard(cardId: string, options?: any): AxiosPromise { + return localVarFp.cardControllerCopyCard(cardId, options).then((request) => request(axios, basePath)); + }, /** * * @summary Create a new element on a card. @@ -14885,6 +14944,16 @@ export const BoardCardApiFactory = function (configuration?: Configuration, base * @interface BoardCardApi */ export interface BoardCardApiInterface { + /** + * + * @summary Copy a single card. + * @param {string} cardId The id of the card. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof BoardCardApiInterface + */ + cardControllerCopyCard(cardId: string, options?: any): AxiosPromise; + /** * * @summary Create a new element on a card. @@ -14958,6 +15027,18 @@ export interface BoardCardApiInterface { * @extends {BaseAPI} */ export class BoardCardApi extends BaseAPI implements BoardCardApiInterface { + /** + * + * @summary Copy a single card. + * @param {string} cardId The id of the card. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof BoardCardApi + */ + public cardControllerCopyCard(cardId: string, options?: any) { + return BoardCardApiFp(this.configuration).cardControllerCopyCard(cardId, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Create a new element on a card.