Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

N21-2320 Share links for board cards #3497

Merged
merged 9 commits into from
Jan 22, 2025
5 changes: 5 additions & 0 deletions src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ export default {
"common.words.color.turquoise": "Türkis",
"common.words.color.yellow": "Gelb",
"common.words.copiedToClipboard": "In die Zwischenablage kopiert",
"common.words.copyLinkToClipboard.success":
"Link in die Zwischenablage kopiert",
"common.words.copyLinkToClipboard.failure":
"Link konnte nicht in die Zwischenablage kopiert werden",
"common.words.courseGroups": "Kursgruppen",
"common.words.courses": "Kurse",
"common.words.draft": "Entwurf",
Expand Down Expand Up @@ -371,6 +375,7 @@ export default {
"components.board.action.moveLeft": "Nach links verschieben",
"components.board.action.moveRight": "Nach rechts verschieben",
"components.board.action.moveUp": "Nach oben verschieben",
"components.board.action.shareLink.card": "Link zur Karte kopieren",
"components.board.alert.info.teacher":
"Dieser Bereich ist sichtbar für alle Kursteilnehmenden.",
"components.board.alert.info.draft":
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ export default {
"common.words.color.turquoise": "Turquoise",
"common.words.color.yellow": "Yellow",
"common.words.copiedToClipboard": "Copied to the clipboard",
"common.words.copyLinkToClipboard.success": "Link copied to clipboard",
"common.words.copyLinkToClipboard.failure":
"Link could not be copied to clipboard",
"common.words.courseGroups": "Course Groups",
"common.words.courses": "Courses",
"common.words.draft": "draft",
Expand Down Expand Up @@ -370,6 +373,7 @@ export default {
"components.board.action.moveLeft": "Move left",
"components.board.action.moveRight": "Move right",
"components.board.action.moveUp": "Move up",
"components.board.action.shareLink.card": "Copy link to card",
"components.board.alert.info.teacher":
"This board is visible to all course participants.",
"components.board.alert.info.draft":
Expand Down
4 changes: 4 additions & 0 deletions src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ export default {
"common.words.color.turquoise": "Turquesa",
"common.words.color.yellow": "Amarillo",
"common.words.copiedToClipboard": "Copiado en el portapapeles",
"common.words.copyLinkToClipboard.success": "Enlace copiado al portapapeles",
"common.words.copyLinkToClipboard.failure":
"El enlace no se pudo copiar al portapapeles",
"common.words.courseGroups": "grupos de cursos",
"common.words.courses": "Cursos",
"common.words.draft": "borrador",
Expand Down Expand Up @@ -375,6 +378,7 @@ export default {
"components.board.action.moveLeft": "Mover a la izquierda",
"components.board.action.moveRight": "Mover a la derecha",
"components.board.action.moveUp": "Levantar",
"components.board.action.shareLink.card": "Enlace a la ficha",
"components.board.alert.info.teacher":
"Este tablero es visible para todos los participantes en el curso.",
"components.board.alert.info.draft":
Expand Down
5 changes: 5 additions & 0 deletions src/locales/uk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ export default {
"common.words.color.turquoise": "Бірюза",
"common.words.color.yellow": "Жовтий",
"common.words.copiedToClipboard": "Скопійовано в буфер обміну",
"common.words.copyLinkToClipboard.success":
"Посилання скопійовано в буфер обміну",
"common.words.copyLinkToClipboard.failure":
"Не вдалося скопіювати посилання в буфер обміну",
"common.words.courseGroups": "курсові групи",
"common.words.courses": "Мій курс",
"common.words.draft": "чернетка",
Expand Down Expand Up @@ -378,6 +382,7 @@ export default {
"components.board.action.moveLeft": "Перемістіться вліво",
"components.board.action.moveRight": "Перемістіться праворуч",
"components.board.action.moveUp": "Рухатися вгору",
"components.board.action.shareLink.card": "Скопіювати посилання на Карту",
"components.board.alert.info.teacher": "Цю дошку бачать усі учасники курсу.",
"components.board.alert.info.draft": "Ця дошка невидима для учасників курсу.",
"components.board.column.defaultTitle": "Нова колонка",
Expand Down
33 changes: 32 additions & 1 deletion src/modules/feature/board/board/Board.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ describe("Board", () => {
});
mockExtractDataAttribute.mockReturnValue("column-id");

route = createMock<ReturnType<typeof useRoute>>();
route = createMock<ReturnType<typeof useRoute>>({
hash: "",
});
useRouteMock.mockReturnValue(route);

router = createMock<Router>();
Expand Down Expand Up @@ -389,6 +391,35 @@ describe("Board", () => {
expect(cardStore.loadPreferredTools).not.toHaveBeenCalled();
});
});

describe("when the url has a hash", () => {
const setup2 = () => {
setup();

const elementId = "card-12345";
route.hash = `#${elementId}`;

const domElementMock = createMock<HTMLElement>();
const querySelectorSpy = jest.spyOn(document, "querySelector");
querySelectorSpy.mockReturnValueOnce(domElementMock);

return {
domElementMock,
};
};

it("should scroll to and focus the element", async () => {
const { domElementMock } = setup2();

await nextTick();

expect(domElementMock.scrollIntoView).toHaveBeenCalledWith({
block: "start",
inline: "center",
});
expect(domElementMock.focus).toHaveBeenCalled();
});
});
});

describe("when component is unMounted", () => {
Expand Down
22 changes: 20 additions & 2 deletions src/modules/feature/board/board/Board.vue
Original file line number Diff line number Diff line change
Expand Up @@ -284,15 +284,33 @@ const onUpdateBoardTitle = async (newTitle: string) => {
boardStore.updateBoardTitleRequest({ boardId: props.boardId, newTitle });
};

onMounted(() => {
const scrollToNodeAndFocus = (scrollTargetId: string) => {
const targetElement: HTMLElement | null = document.querySelector(
`[data-scroll-target="${scrollTargetId}"]`
);

targetElement?.scrollIntoView({ block: "start", inline: "center" });
targetElement?.focus();
};

onMounted(async () => {
resetPageInformation();
setAlert();
useBoardInactivity();
boardStore.fetchBoardRequest({ boardId: props.boardId });
const boardFetchPromise = boardStore.fetchBoardRequest({
boardId: props.boardId,
});

if (hasCreateToolPermission) {
cardStore.loadPreferredTools(ToolContextType.BoardElement);
}

await boardFetchPromise;

if (route.hash) {
const scrollTargetId: string = route.hash.slice(1);
scrollToNodeAndFocus(scrollTargetId);
}
});

onUnmounted(() => {
Expand Down
57 changes: 49 additions & 8 deletions src/modules/feature/board/card/CardHost.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@ import {
} from "@data-board";
import { createMock, DeepMocked } from "@golevelup/ts-jest";
import { createTestingPinia } from "@pinia/testing";
import { KebabMenuActionDelete } from "@ui-kebab-menu";
import { BoardMenuScope } from "@ui-board";
import { useDeleteConfirmationDialog } from "@ui-confirmation-dialog";
import {
KebabMenuActionDelete,
KebabMenuActionEdit,
KebabMenuActionShareLink,
} from "@ui-kebab-menu";
import {
useBoardNotifier,
useCourseBoardEditMode,
useShareBoardLink,
useSharedEditMode,
useSharedLastCreatedElement,
} from "@util-board";
Expand All @@ -43,6 +49,7 @@ const mockedUseBoardNotifier = jest.mocked(useBoardNotifier);
const mockedSharedLastCreatedElement = jest.mocked(useSharedLastCreatedElement);
const mockedEditMode = jest.mocked(useCourseBoardEditMode);
const mockedUseSharedEditMode = jest.mocked(useSharedEditMode);
const mockedUseShareBoardLink = jest.mocked(useShareBoardLink);

jest.mock("@data-board/BoardFocusHandler.composable");
const mockedBoardFocusHandler = jest.mocked(useBoardFocusHandler);
Expand All @@ -65,6 +72,7 @@ describe("CardHost", () => {
let mockedSharedLastCreatedElementCalls: DeepMocked<
ReturnType<typeof useSharedLastCreatedElement>
>;
let useShareBoardLinkMock: DeepMocked<ReturnType<typeof useShareBoardLink>>;

beforeEach(() => {
setupStores({ envConfigModule: EnvConfigModule });
Expand All @@ -83,6 +91,11 @@ describe("CardHost", () => {
isInEditMode: computed(() => true),
});

useShareBoardLinkMock = createMock<ReturnType<typeof useShareBoardLink>>({
getShareLinkId: jest.fn().mockReturnValue("shareLinkId"),
});
mockedUseShareBoardLink.mockReturnValue(useShareBoardLinkMock);

mockedBoardFocusHandler.mockReturnValue({
isFocusContained: computed(() => true),
isFocused: computed(() => true),
Expand Down Expand Up @@ -138,6 +151,8 @@ describe("CardHost", () => {
});
}

const cardId = card?.id ?? "cardId";

const wrapper = shallowMount(CardHost, {
global: {
plugins: [
Expand All @@ -154,7 +169,7 @@ describe("CardHost", () => {
],
},
propsData: {
cardId: card?.id ?? "cardId",
cardId,
height: card?.height ?? 0,
columnIndex: 0,
rowIndex: 1,
Expand All @@ -163,7 +178,10 @@ describe("CardHost", () => {

mockedPiniaStoreTyping(useCardStore);

return { wrapper };
return {
wrapper,
cardId,
};
};

describe("when component is mounted", () => {
Expand Down Expand Up @@ -203,20 +221,43 @@ describe("CardHost", () => {

describe("user permissions", () => {
describe("when user is not permitted to delete", () => {
it("should not be rendered on DOM", () => {
it("should not show an edit button", () => {
mockedBoardPermissions.hasDeletePermission = false;
const { wrapper } = setup();

const deleteButton = wrapper.findComponent(KebabMenuActionEdit);

expect(deleteButton.exists()).toEqual(false);
});

it("should not show a delete button", () => {
mockedBoardPermissions.hasDeletePermission = false;
const { wrapper } = setup();

const boardMenuComponent = wrapper.findAllComponents({
name: "BoardMenu",
});
const deleteButton = wrapper.findComponent(KebabMenuActionDelete);

expect(boardMenuComponent.length).toStrictEqual(0);
expect(deleteButton.exists()).toEqual(false);
});
});
});

describe("card menus", () => {
describe("when users clicks share link menu", () => {
it("should copy a share link", async () => {
mockedBoardPermissions.hasDeletePermission = true;
const { wrapper, cardId } = setup();

const shareLinkButton = wrapper.findComponent(KebabMenuActionShareLink);

await shareLinkButton.trigger("click");

expect(useShareBoardLinkMock.copyShareLink).toHaveBeenCalledWith(
cardId,
BoardMenuScope.CARD
);
});
});

describe("when users click delete menu", () => {
it("should emit 'delete:card'", async () => {
mockedBoardPermissions.hasDeletePermission = true;
Expand Down
24 changes: 20 additions & 4 deletions src/modules/feature/board/card/CardHost.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
:ripple="false"
:hover="isHovered"
:data-testid="cardTestId"
:data-scroll-target="getShareLinkId(cardId, BoardMenuScope.CARD)"
>
<template v-if="isLoadingCard">
<CardSkeleton :height="height" />
Expand All @@ -35,16 +36,17 @@

<div class="board-menu" :class="boardMenuClasses">
<BoardMenu
v-if="hasDeletePermission"
:scope="BoardMenuScope.CARD"
has-background
:data-testid="boardMenuTestId"
>
<KebabMenuActionEdit
v-if="!isEditMode"
v-if="hasDeletePermission && !isEditMode"
@click="onStartEditMode"
/>
<KebabMenuActionShareLink @click="onCopyShareLink" />
<KebabMenuActionDelete
v-if="hasDeletePermission"
:name="card.title"
:scope="BoardMenuScope.CARD"
@click="onDeleteCard"
Expand Down Expand Up @@ -99,14 +101,19 @@ import {
} from "@data-board";
import { mdiArrowExpand } from "@icons/material";
import { BoardMenu, BoardMenuScope } from "@ui-board";
import { KebabMenuActionDelete, KebabMenuActionEdit } from "@ui-kebab-menu";
import { useCourseBoardEditMode } from "@util-board";
import {
KebabMenuActionDelete,
KebabMenuActionEdit,
KebabMenuActionShareLink,
} from "@ui-kebab-menu";
import { useCourseBoardEditMode, useShareBoardLink } from "@util-board";
import { useDebounceFn, useElementHover, useElementSize } from "@vueuse/core";
import { computed, defineComponent, onMounted, ref, toRef } from "vue";
import { useAddElementDialog } from "../shared/AddElementDialog.composable";
import CardAddElementMenu from "./CardAddElementMenu.vue";
import CardHostDetailView from "./CardHostDetailView.vue";
import CardHostInteractionHandler from "./CardHostInteractionHandler.vue";

import CardSkeleton from "./CardSkeleton.vue";
import CardTitle from "./CardTitle.vue";
import ContentElementList from "./ContentElementList.vue";
Expand All @@ -128,6 +135,7 @@ export default defineComponent({
CardHostInteractionHandler,
KebabMenuActionDelete,
CardHostDetailView,
KebabMenuActionShareLink,
},
props: {
height: { type: Number, required: true },
Expand Down Expand Up @@ -240,6 +248,12 @@ export default defineComponent({
cardStore.addTextAfterTitle(props.cardId);
};

const { copyShareLink, getShareLinkId } = useShareBoardLink();

const onCopyShareLink = async () => {
await copyShareLink(props.cardId, BoardMenuScope.CARD);
};

const boardMenuClasses = computed(() => {
if (isFocusContained.value === true || isHovered.value === true) {
return "";
Expand Down Expand Up @@ -278,6 +292,8 @@ export default defineComponent({
onEnter,
onOpenDetailView,
onCloseDetailView,
onCopyShareLink,
getShareLinkId,
isDetailView,
mdiArrowExpand,
};
Expand Down
Loading
Loading