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

BC-5642 - internal links in LinkElements on boards #2907

Merged
merged 18 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ describe("LinkContentElement", () => {
const component = wrapper.getComponent(LinkContentElementCreate);
component.vm.$emit("create:url", "https://abc.de");

expect(useMetaTagExtractorApiMock.extractMetaTags).toHaveBeenCalled();
expect(useMetaTagExtractorApiMock.getMetaTags).toHaveBeenCalled();
});

describe("when no protocol was provided", () => {
Expand All @@ -135,7 +135,7 @@ describe("LinkContentElement", () => {
component.vm.$emit("create:url", url);

const expected = `https://${url}`;
expect(useMetaTagExtractorApiMock.extractMetaTags).toHaveBeenCalledWith(
expect(useMetaTagExtractorApiMock.getMetaTags).toHaveBeenCalledWith(
expected
);
});
Expand All @@ -151,9 +151,12 @@ describe("LinkContentElement", () => {
title: "my title",
description: "",
imageUrl: "https://abc.de/foto.png",
type: "unknown",
parentTitle: "",
parentType: "unknown",
};

useMetaTagExtractorApiMock.extractMetaTags.mockResolvedValue(
useMetaTagExtractorApiMock.getMetaTags.mockResolvedValue(
fakeMetaTags
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default defineComponent({
() => props.isEditMode && !computedElement.value.content.url
);

const { extractMetaTags } = useMetaTagExtractorApi();
const { getMetaTags } = useMetaTagExtractorApi();

const { createPreviewImage } = usePreviewGenerator(element.value.id);

Expand All @@ -101,8 +101,7 @@ export default defineComponent({
const validUrl = ensureProtocolIncluded(originalUrl);
modelValue.value.url = validUrl;

const { title, description, imageUrl } =
await extractMetaTags(validUrl);
const { title, description, imageUrl } = await getMetaTags(validUrl);
modelValue.value.title = title;
modelValue.value.description = description;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
rows="1"
@keydown.enter.prevent.stop="onKeydownEnter"
class="text"
>
<template v-slot:append>
<button type="submit" ref="submit">
<v-icon aria-hidden="true"> {{ mdiCheck }}</v-icon>
<span class="d-sr-only">{{ $t("common.actions.save") }}</span>
</button>
</template>
</v-textarea>
/>

<div class="align-self-center pl-2">
<button type="submit" ref="submit">
<v-icon aria-hidden="true"> {{ mdiCheck }}</v-icon>
<span class="d-sr-only">{{ $t("common.actions.save") }}</span>
</button>
</div>

<div class="align-self-center menu">
<slot />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { MetaTagExtractorApiFactory } from "@/serverApi/v3";
import {
MetaTagExtractorApiFactory,
MetaTagExtractorResponse,
} from "@/serverApi/v3";
import { $axios } from "@/utils/api";
import { useI18n } from "@/composables/i18n.composable";

type MetaTagResult = {
url: string;
Expand All @@ -9,15 +13,52 @@ type MetaTagResult = {
};

export const useMetaTagExtractorApi = () => {
const { t } = useI18n();
const metaTagApi = MetaTagExtractorApiFactory(undefined, "/v3", $axios);

const extractMetaTags = async (url: string): Promise<MetaTagResult> => {
const mapMetaTagResponse = (
response: MetaTagExtractorResponse
): MetaTagResult => {
const prefix = getPrefix(response.type);
const postfix = getPostfix(response.type, response.parentTitle);
let title = getTitle(response.type, response.title);
title = `${prefix}${title}${postfix}`;
return { ...response, title };
};

const getPrefix = (type: string) => {
const typeToLanguageKeyMap: Record<string, string> = {
course: "common.labels.course",
lesson: "common.words.topic",
task: "common.words.task",
board: "components.board",
};

const prefixKey = typeToLanguageKeyMap[type];
const prefix = prefixKey ? `${t(prefixKey)}: ` : "";
return prefix;
hoeppner-dataport marked this conversation as resolved.
Show resolved Hide resolved
};

const getTitle = (type: string, title: string) => {
if (type === "board" && title == "") {
return t("pages.room.boardCard.label.courseBoard");
}
return title;
};

const getPostfix = (type: string, parentTitle: string) => {
if (type === "board" && parentTitle.length > 0) {
hoeppner-dataport marked this conversation as resolved.
Show resolved Hide resolved
return ` (${parentTitle})`;
}
hoeppner-dataport marked this conversation as resolved.
Show resolved Hide resolved
return "";
};

const getMetaTags = async (url: string): Promise<MetaTagResult> => {
try {
const res = await metaTagApi.metaTagExtractorControllerGetData({
const res = await metaTagApi.metaTagExtractorControllerGetMetaTags({
url,
});

return res.data;
return mapMetaTagResponse(res.data);
} catch (e) {
return {
url,
Expand All @@ -28,6 +69,6 @@ export const useMetaTagExtractorApi = () => {
};

return {
extractMetaTags,
getMetaTags,
};
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as serverApi from "@/serverApi/v3/api";
import { MetaTagExtractorResponse } from "@/serverApi/v3/api";
import { createMock, DeepMocked } from "@golevelup/ts-jest";
import { mount } from "@vue/test-utils";
import { defineComponent } from "vue";
import { useMetaTagExtractorApi } from "./MetaTagExtractorApi.composable";
import { mockApiResponse } from "@@/tests/test-utils";
import {
i18nMock,
mockApiResponse,
mountComposable,
} from "@@/tests/test-utils";
import { I18N_KEY } from "@/utils/inject";

describe("useMetaTagExtractorApi", () => {
let api: DeepMocked<serverApi.MetaTagExtractorApi>;
Expand All @@ -19,56 +22,132 @@ describe("useMetaTagExtractorApi", () => {
jest.clearAllMocks();
});

const getWrapper = () => {
let composable: ReturnType<typeof useMetaTagExtractorApi> | undefined;

const TestComponent = defineComponent({
template: "<div/>",
setup() {
composable = useMetaTagExtractorApi();
},
});

const wrapper = mount(TestComponent, {});
return { wrapper, composable };
};

describe("extractMetaTags", () => {
describe("getMetaTags", () => {
describe("when meta tags could be extracted", () => {
const setup = () => {
const mockedResponse: MetaTagExtractorResponse = {
url: "",
title: "",
description: "",
imageUrl: "",
describe("when meta tags are of type unknown", () => {
const setup = () => {
const mockedResponse: MetaTagExtractorResponse = {
url: "",
title: "",
description: "",
imageUrl: "",
type: "unknown",
parentTitle: "",
parentType: "unknown",
};

api.metaTagExtractorControllerGetMetaTags.mockResolvedValue(
mockApiResponse({ data: mockedResponse })
);

const composable = mountComposable(() => useMetaTagExtractorApi(), {
[I18N_KEY.valueOf()]: i18nMock,
});

return {
mockedResponse,
composable,
};
};

api.metaTagExtractorControllerGetData.mockResolvedValue(
mockApiResponse({ data: mockedResponse })
);
it("should be defined", () => {
const { composable } = setup();

const { wrapper, composable } = getWrapper();
expect(composable?.getMetaTags).toBeDefined();
});

return {
wrapper,
mockedResponse,
composable,
};
};
it("should return the data", async () => {
const { composable, mockedResponse } = setup();

it("should be defined", () => {
const { composable } = setup();
const url = "https://test.de/my-article";
const data = await composable?.getMetaTags(url);

expect(composable?.extractMetaTags).toBeDefined();
expect(data).toEqual(mockedResponse);
});
});

it("should return the data", async () => {
const { composable, mockedResponse } = setup();

const url = "https://test.de/my-article";
const data = await composable?.extractMetaTags(url);

expect(data).toEqual(mockedResponse);
describe("when metatags are of type board", () => {
describe("when board has explicit title", () => {
const setup = () => {
const mockedResponse: MetaTagExtractorResponse = {
url: "https://test.de/my-article",
title: "Shakespear",
description: "",
imageUrl: "",
type: "board",
parentTitle: "English",
parentType: "course",
};

api.metaTagExtractorControllerGetMetaTags.mockResolvedValue(
mockApiResponse({ data: mockedResponse })
);

const composable = mountComposable(() => useMetaTagExtractorApi(), {
[I18N_KEY.valueOf()]: i18nMock,
});

return {
mockedResponse,
composable,
};
};

it("should be defined", () => {
const { composable } = setup();

expect(composable?.getMetaTags).toBeDefined();
});

it("should return the correct composed title", async () => {
const { composable } = setup();

const url = "https://test.de/my-article";
const data = await composable?.getMetaTags(url);

expect(data.title).toEqual(
"components.board: Shakespear (English)"
);
});
});

describe("when board has no explicit title", () => {
const setup = () => {
const mockedResponse: MetaTagExtractorResponse = {
url: "https://test.de/my-article",
title: "",
description: "",
imageUrl: "",
type: "board",
parentTitle: "English",
parentType: "course",
};

api.metaTagExtractorControllerGetMetaTags.mockResolvedValue(
mockApiResponse({ data: mockedResponse })
);

const composable = mountComposable(() => useMetaTagExtractorApi(), {
[I18N_KEY.valueOf()]: i18nMock,
});

return {
mockedResponse,
composable,
};
};

it("should use default fallback title", async () => {
const { composable } = setup();

const url = "https://test.de/my-article";
const data = await composable?.getMetaTags(url);

expect(data.title).toEqual(
"components.board: pages.room.boardCard.label.courseBoard (English)"
);
});
});
});
});

Expand All @@ -79,14 +158,18 @@ describe("useMetaTagExtractorApi", () => {
title: "",
description: "",
imageUrl: "",
type: "unknown",
parentTitle: "",
parentType: "unknown",
};

api.metaTagExtractorControllerGetData.mockRejectedValue(false);
api.metaTagExtractorControllerGetMetaTags.mockRejectedValue(false);

const { wrapper, composable } = getWrapper();
const composable = mountComposable(() => useMetaTagExtractorApi(), {
[I18N_KEY.valueOf()]: { t: (key: string) => key },
});

return {
wrapper,
mockedResponse,
composable,
};
Expand All @@ -96,7 +179,7 @@ describe("useMetaTagExtractorApi", () => {
const { composable } = setup();

const url = "https://test.de/my-article";
const data = await composable?.extractMetaTags(url);
const data = await composable?.getMetaTags(url);

expect(data).toEqual({ url, title: "", description: "" });
});
Expand Down
4 changes: 3 additions & 1 deletion src/components/util-validators/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export const isValidUrl: FormValidatorFn<string> = (errMsg) => (value) => {
if (!["http:", "https:"].includes(urlObject.protocol)) {
throw new Error("Wrong protocol");
}
if (!urlObject.hostname.includes(".")) {
if (
!(urlObject.hostname.includes(".") || urlObject.hostname === "localhost")
) {
throw new Error("TopLevelDomain missing");
}
if (/(^-)|(--)|(-$)/.test(urlObject.hostname)) {
Expand Down
Loading
Loading