Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c508576
BC-10395 - update api definition
virgilchiriac Oct 14, 2025
cd1eb1c
card duplication groundwork
odalys-dataport Oct 15, 2025
e5ae410
Merge branch 'main' into BC-10395-duplicate-card
wolfganggreschus Oct 16, 2025
b95ffee
rename to duplicate
odalys-dataport Oct 16, 2025
a74b5d3
Merge branch 'BC-10395-duplicate-card' of https://github.com/hpi-schu…
odalys-dataport Oct 16, 2025
6594224
Change return type from cardControllerCopyCard to CardResponse
wolfganggreschus Oct 16, 2025
d0f7b93
use CardResponse type
odalys-dataport Oct 16, 2025
a950171
implement socket functions
odalys-dataport Oct 16, 2025
74c5816
implement socket aria notifications
odalys-dataport Oct 16, 2025
c9ba161
add generic success notifications
odalys-dataport Oct 16, 2025
77c902d
translated notifications
odalys-dataport Oct 17, 2025
1c28cf1
adjust function signature add rest api tests
odalys-dataport Oct 17, 2025
3a2e6f7
fix cardduplicate success test in rest api
odalys-dataport Oct 17, 2025
e3938ac
remove success notification
odalys-dataport Oct 20, 2025
a656268
add CardHost tests
odalys-dataport Oct 20, 2025
4b37258
add CardHost tests
odalys-dataport Oct 20, 2025
30ec50e
added specific duplication error message
odalys-dataport Oct 20, 2025
824e58f
use correct property name copiedCard
hoeppner-dataport Oct 20, 2025
7cb8faa
also update boardStore to put card at correct position
hoeppner-dataport Oct 20, 2025
5def1b6
Merge branch 'main' of https://github.com/hpi-schul-cloud/nuxt-client…
hoeppner-dataport Oct 20, 2025
102297f
rename copiedCard to duplicatedCard
odalys-dataport Oct 21, 2025
3b8fb68
add Board.store tests
odalys-dataport Oct 21, 2025
84e4a2c
Merge branch 'main' into BC-10395-duplicate-card
odalys-dataport Oct 21, 2025
0854e90
Merge branch 'main' into BC-10395-duplicate-card
odalys-dataport Oct 22, 2025
65a5e65
Merge branch 'main' of https://github.com/hpi-schul-cloud/nuxt-client…
odalys-dataport Oct 23, 2025
31ea2b1
slimmer duplicateCardSuccess function in Board.store
odalys-dataport Oct 23, 2025
3626c96
pass data-testid to KebabMenuActionCopy as attribute
odalys-dataport Oct 23, 2025
b7f7729
rename KebabMenuActionCopy to KebabMenuActionDuplicate
odalys-dataport Oct 23, 2025
6d30dd2
add test for KebabMenuActionDuplicate
odalys-dataport Oct 23, 2025
578a8eb
remove unneccesary reload board when card is duplicate
hoeppner-dataport Oct 27, 2025
61deeb7
Merge branch 'main' of https://github.com/hpi-schul-cloud/nuxt-client…
odalys-dataport Oct 27, 2025
9097f9f
Merge branch 'BC-10395-duplicate-card' of https://github.com/hpi-schu…
odalys-dataport Oct 27, 2025
a0cb95e
chore: also call boardStore.duplicateCardSuccess (in REST-Api mode)
hoeppner-dataport Oct 27, 2025
bdf7708
Merge branch 'BC-10395-duplicate-card' of https://github.com/hpi-schu…
odalys-dataport Oct 27, 2025
d69167b
move notification to card store
odalys-dataport Oct 27, 2025
e154b9c
Merge branch 'main' into BC-10395-duplicate-card
odalys-dataport Oct 29, 2025
8a70bc5
show loading state dialog wip
odalys-dataport Oct 29, 2025
0191c37
wip remove loading for now
luejoh Oct 30, 2025
d9d0cf0
fix unit test
luejoh Oct 30, 2025
c3292fd
leftover import
luejoh Oct 30, 2025
aceb682
Merge branch 'main' into BC-10395-duplicate-card
virgilchiriac Oct 30, 2025
290ba07
Merge branch 'main' into BC-10395-duplicate-card
virgilchiriac Oct 30, 2025
28b6b64
Merge branch 'main' into BC-10395-duplicate-card
virgilchiriac Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/error-handling/ErrorHandler.composable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
5 changes: 5 additions & 0 deletions src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/uk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.",
Expand Down
15 changes: 14 additions & 1 deletion src/modules/data/board/Board.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -380,6 +392,7 @@ export const useBoardStore = defineStore("boardStore", () => {
createColumnSuccess,
deleteBoardRequest,
deleteBoardSuccess,
duplicateCardSuccess,
deleteCardSuccess,
deleteColumnRequest,
deleteColumnSuccess,
Expand Down
33 changes: 33 additions & 0 deletions src/modules/data/board/Board.store.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
6 changes: 6 additions & 0 deletions src/modules/data/board/BoardApi.composable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> => {
await cardsApi.cardControllerMoveCard(cardId, {
toColumnId,
Expand Down Expand Up @@ -304,6 +309,7 @@ export const useBoardApi = () => {
updateColumnTitleCall,
updateElementCall,
createCardCall,
duplicateCardCall,
getContextInfo,
updateBoardLayoutCall,
getElementWithParentHierarchyCall,
Expand Down
12 changes: 12 additions & 0 deletions src/modules/data/board/BoardApi.composable.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
31 changes: 22 additions & 9 deletions src/modules/data/board/Card.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CreateElementSuccessPayload,
DeleteCardSuccessPayload,
DeleteElementSuccessPayload,
DuplicateCardSuccessPayload,
FetchCardSuccessPayload,
MoveElementSuccessPayload,
UpdateCardHeightSuccessPayload,
Expand All @@ -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";
Expand Down Expand Up @@ -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;

Expand All @@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -151,7 +162,7 @@ export const useCardStore = defineStore("cardStore", () => {

const deleteElementRequest = socketOrRest.deleteElementRequest;

const deleteElementSuccess = async (payload: DeleteElementSuccessPayload): Promise<void> => {
const deleteElementSuccess = (payload: DeleteElementSuccessPayload) => {
const card = cards.value[payload.cardId];
if (card === undefined) return;

Expand All @@ -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;
Expand Down Expand Up @@ -201,7 +212,7 @@ export const useCardStore = defineStore("cardStore", () => {
return previousElement.id;
};

const loadPreferredTools = async (contextType: ToolContextType): Promise<void> => {
const loadPreferredTools = async (contextType: ToolContextType) => {
isPreferredToolsLoading.value = true;

preferredTools.value = (await restApi.getPreferredTools(contextType)) || [];
Expand All @@ -226,6 +237,8 @@ export const useCardStore = defineStore("cardStore", () => {
fetchCardRequest,
fetchCardSuccess,
cards,
duplicateCard,
duplicateCardSuccess,
deleteCardRequest,
deleteCardSuccess,
getCard,
Expand Down
Loading
Loading