From 30ffb07db3c0e13b7f700483872cadb3827b64e3 Mon Sep 17 00:00:00 2001 From: Michael Linares <116801315+Michaellinaresxk@users.noreply.github.com> Date: Tue, 17 Oct 2023 18:07:04 +0200 Subject: [PATCH 1/2] BC-4623-Creation time and update time of status messages (#2786) * Added tests unit for formatDate. --- src/components/topbar/StatusAlerts.unit.ts | 25 +++++++++++---------- src/components/topbar/StatusAlerts.vue | 20 ++++++++--------- src/plugins/datetime.js | 26 ++++++++++++++++++++-- src/plugins/datetime.unit.js | 8 +++++++ 4 files changed, 54 insertions(+), 25 deletions(-) diff --git a/src/components/topbar/StatusAlerts.unit.ts b/src/components/topbar/StatusAlerts.unit.ts index b9087a413b..d1f2214a73 100644 --- a/src/components/topbar/StatusAlerts.unit.ts +++ b/src/components/topbar/StatusAlerts.unit.ts @@ -4,6 +4,8 @@ import setupStores from "@@/tests/test-utils/setupStores"; import { mockStatusAlerts } from "@@/tests/test-utils/mockStatusAlerts"; import Vue from "vue"; import createComponentMocks from "@@/tests/test-utils/componentMocks"; +import dayjs from "dayjs"; +import { formatDateForAlerts } from "@/plugins/datetime"; const testProps = { statusAlerts: mockStatusAlerts, @@ -59,18 +61,17 @@ describe("@/components/topbar/StatusAlerts", () => { }); }); -describe("getCreatedDate", () => { - it("should be getCreatedDate function on the template", () => { - const wrapper = getWrapper(testProps); - const expectedDate = "05.05.2023 12:34"; - const alertElement = wrapper.find(".alert-date"); - expect(alertElement.element.innerHTML).toContain(expectedDate); +describe("formatDate", () => { + const wrapper = getWrapper(testProps); + it("returns 'vor ein paar Sekunden' for seconds difference", () => { + const fewSecondsAgo = dayjs().subtract(30, "seconds"); + const expectedOutput = "vor ein paar Sekunden"; + const result = formatDateForAlerts(fewSecondsAgo); + expect(result).toBe(expectedOutput); }); - - it("should returns expected result", () => { - const wrapper = getWrapper(testProps); - const expectedDate = "05.05.2023 12:34"; - const dateTime = "May 5, 2023 12:34 PM"; - expect(wrapper.vm.getCreatedDate(dateTime)).toEqual(expectedDate); + it("returns European format after 7 days", () => { + const pastDate = dayjs.utc().subtract(8, "days").toISOString(); + const expectedDate = dayjs.utc(pastDate).format("DD.MM.YYYY"); + expect(wrapper.vm.formatDate(pastDate)).toEqual(expectedDate); }); }); diff --git a/src/components/topbar/StatusAlerts.vue b/src/components/topbar/StatusAlerts.vue index c9acbd6063..5573915396 100644 --- a/src/components/topbar/StatusAlerts.vue +++ b/src/components/topbar/StatusAlerts.vue @@ -34,10 +34,11 @@ class="text-left text-caption d-flex flex-row alert-date text--secondary mt-0 mt-2" :data-testid="`alert-date-${index}`" > - {{ $t("common.labels.updateAt") }} - {{ getDate(item.timestamp) }} | - {{ $t("common.labels.createAt") }} - {{ getCreatedDate(item.createdAt) }} + + {{ $t("common.labels.createAt") }} {{ formatDate(item.createdAt) }} @@ -46,7 +47,7 @@ diff --git a/src/plugins/datetime.js b/src/plugins/datetime.js index 44289abc1c..b8b54e1213 100644 --- a/src/plugins/datetime.js +++ b/src/plugins/datetime.js @@ -252,6 +252,29 @@ export const fromNow = (date, isLocalTimeZone) => { return fromUTC(date).fromNow(); }; +export const formatDateForAlerts = (date, isLocalTimeZone = false) => { + const time = isLocalTimeZone ? dayjs(date) : dayjs.tz(date, "UTC"); + const current = isLocalTimeZone ? dayjs() : dayjs.utc(); + + setDayjsLocale(); + + const totalDaysDiff = Math.abs(current.diff(time, "day")); + const MAX_DAYS_BEFORE_SHOWING_FULL_DATE = 7; + + if (totalDaysDiff < MAX_DAYS_BEFORE_SHOWING_FULL_DATE) { + // If it is less than 7 days, we use fromNow + return fromNow(date, isLocalTimeZone); + } else { + // If it is 7 days or more, we return in European format. + return time.format("DD.MM.YYYY"); + } +}; + +export const setDayjsLocale = () => { + const locale = authModule?.getLocale || "de"; + dayjs.locale(locale); +}; + /** * Returns future date difference to current local time * @param {String} date @@ -289,6 +312,5 @@ export default ({ app, store }) => { initDefaultTimezone(app, store); setDefaultFormats(app); - const locale = authModule.getLocale || "de"; - dayjs.locale(locale); + setDayjsLocale(); }; diff --git a/src/plugins/datetime.unit.js b/src/plugins/datetime.unit.js index 66e0060075..5a47baa896 100644 --- a/src/plugins/datetime.unit.js +++ b/src/plugins/datetime.unit.js @@ -3,6 +3,7 @@ import { currentDate, fromInputDateTime, fromNow, + formatDateForAlerts, fromNowToFuture, createInputDateTime, inputRangeDate, @@ -154,6 +155,13 @@ describe("@/plugins/datetime", () => { expect(result).toBe("in 7 days"); }); + it("formatDateForAlerts", () => { + const sevenDaysAgo = dayjs().subtract(7, "days"); + const expectedDate = sevenDaysAgo.format("DD.MM.YYYY"); + const result = formatDateForAlerts(sevenDaysAgo); + expect(result).toBe(expectedDate); + }); + it("fromNowToFuture", () => { const past = dateUTC.toISOString(); const future = dateNow.add(230, "minute").toISOString(); From c5373a95fac6e075f6782fad3c2569d722568827 Mon Sep 17 00:00:00 2001 From: Max Bischof <106820326+bischofmax@users.noreply.github.com> Date: Tue, 17 Oct 2023 18:31:45 +0200 Subject: [PATCH 2/2] BC-5182 - Display video element (#2855) --- .../FileContentElement.unit.ts | 65 +++-- .../FileContentElement.vue | 14 +- .../content/FileContent.unit.ts | 67 +++-- .../content/FileContent.vue | 30 +- .../content/alert/FileAlert.unit.ts | 218 -------------- .../content/alert/FileAlert.vue | 94 ------ .../content/alert/FileAlerts.unit.ts | 104 +++++++ .../content/alert/FileAlerts.vue | 56 ++++ .../content/alert/useFileAlerts.composable.ts | 47 +++ .../alert/useFileAlerts.composable.unit.ts | 105 +++++++ .../content/display/FileDisplay.unit.ts | 276 +++++++++++++----- .../content/display/FileDisplay.vue | 41 ++- .../image-display/ImageDisplay.unit.ts | 48 +-- .../display/image-display/ImageDisplay.vue | 20 +- .../video-display/VideoDisplay.unit.ts | 64 ++++ .../display/video-display/VideoDisplay.vue | 52 ++++ .../shared/types/FileAlert.enum.ts | 7 + .../shared/types/file-properties.ts | 1 + src/components/ui-alert/BaseAlert.unit.ts | 74 +++++ src/components/ui-alert/BaseAlert.vue | 20 ++ src/components/ui-alert/ErrorAlert.unit.ts | 46 +++ src/components/ui-alert/ErrorAlert.vue | 18 ++ src/components/ui-alert/InfoAlert.unit.ts | 46 +++ src/components/ui-alert/InfoAlert.vue | 17 ++ src/components/ui-alert/SuccessAlert.unit.ts | 46 +++ src/components/ui-alert/SuccessAlert.vue | 18 ++ src/components/ui-alert/WarningAlert.unit.ts | 46 +++ src/components/ui-alert/WarningAlert.vue | 18 ++ src/components/ui-alert/index.ts | 6 + src/locales/de.json | 1 + src/locales/en.json | 1 + src/locales/es.json | 1 + src/locales/uk.json | 1 + src/utils/fileHelper.ts | 9 + src/utils/fileHelper.unit.ts | 71 +++++ tsconfig.json | 2 + vue.config.js | 1 + 37 files changed, 1281 insertions(+), 470 deletions(-) delete mode 100644 src/components/feature-board-file-element/content/alert/FileAlert.unit.ts delete mode 100644 src/components/feature-board-file-element/content/alert/FileAlert.vue create mode 100644 src/components/feature-board-file-element/content/alert/FileAlerts.unit.ts create mode 100644 src/components/feature-board-file-element/content/alert/FileAlerts.vue create mode 100644 src/components/feature-board-file-element/content/alert/useFileAlerts.composable.ts create mode 100644 src/components/feature-board-file-element/content/alert/useFileAlerts.composable.unit.ts create mode 100644 src/components/feature-board-file-element/content/display/video-display/VideoDisplay.unit.ts create mode 100644 src/components/feature-board-file-element/content/display/video-display/VideoDisplay.vue create mode 100644 src/components/feature-board-file-element/shared/types/FileAlert.enum.ts create mode 100644 src/components/ui-alert/BaseAlert.unit.ts create mode 100644 src/components/ui-alert/BaseAlert.vue create mode 100644 src/components/ui-alert/ErrorAlert.unit.ts create mode 100644 src/components/ui-alert/ErrorAlert.vue create mode 100644 src/components/ui-alert/InfoAlert.unit.ts create mode 100644 src/components/ui-alert/InfoAlert.vue create mode 100644 src/components/ui-alert/SuccessAlert.unit.ts create mode 100644 src/components/ui-alert/SuccessAlert.vue create mode 100644 src/components/ui-alert/WarningAlert.unit.ts create mode 100644 src/components/ui-alert/WarningAlert.vue create mode 100644 src/components/ui-alert/index.ts diff --git a/src/components/feature-board-file-element/FileContentElement.unit.ts b/src/components/feature-board-file-element/FileContentElement.unit.ts index 50d7b4e953..10165a32ab 100644 --- a/src/components/feature-board-file-element/FileContentElement.unit.ts +++ b/src/components/feature-board-file-element/FileContentElement.unit.ts @@ -13,10 +13,12 @@ import createComponentMocks from "@@/tests/test-utils/componentMocks"; import { fileElementResponseFactory } from "@@/tests/test-utils/factory/fileElementResponseFactory"; import { fileRecordResponseFactory } from "@@/tests/test-utils/factory/filerecordResponse.factory"; import { MountOptions, shallowMount } from "@vue/test-utils"; -import Vue from "vue"; -import FileContentElement from "./FileContentElement.vue"; +import Vue, { computed } from "vue"; +import { useFileAlerts } from "./content/alert/useFileAlerts.composable"; import FileContent from "./content/FileContent.vue"; +import FileContentElement from "./FileContentElement.vue"; import { FileProperties } from "./shared/types/file-properties"; +import { FileAlert } from "./shared/types/FileAlert.enum"; import FileUpload from "./upload/FileUpload.vue"; jest.mock("@data-board", () => { @@ -27,6 +29,7 @@ jest.mock("@data-board", () => { }); jest.mock("@feature-board"); jest.mock("./shared/composables/FileStorageApi.composable"); +jest.mock("./content/alert/useFileAlerts.composable"); describe("FileContentElement", () => { const notifierModule = createModuleMocks(NotifierModule); @@ -35,6 +38,13 @@ describe("FileContentElement", () => { isEditMode: boolean; }) => { const menu = "slot-menu"; + + const addAlertMock = jest.fn(); + jest.mocked(useFileAlerts).mockReturnValue({ + addAlert: addAlertMock, + alerts: computed(() => []), + }); + const wrapper = shallowMount(FileContentElement as MountOptions, { ...createComponentMocks({ i18n: true }), provide: { @@ -47,7 +57,7 @@ describe("FileContentElement", () => { }, }); - return { wrapper, menu }; + return { wrapper, menu, addAlertMock }; }; describe("when component is not in edit mode", () => { @@ -142,9 +152,10 @@ describe("FileContentElement", () => { size: fileRecordResponse.size, previewStatus: fileRecordResponse.previewStatus, element, + mimeType: fileRecordResponse.mimeType, }; - const { wrapper, menu } = getWrapper({ + const { wrapper, menu, addAlertMock } = getWrapper({ element, isEditMode: false, }); @@ -156,6 +167,7 @@ describe("FileContentElement", () => { element, expectedFileProperties, menu, + addAlertMock, }; }; @@ -167,6 +179,35 @@ describe("FileContentElement", () => { expect(card.props("outlined")).toBe(false); }); + describe("when file content emits add:alert event", () => { + it("should add event payload to emittedAlerts", async () => { + const { wrapper, addAlertMock } = setup(); + + await wrapper.vm.$nextTick(); + + const fileContent = wrapper.findComponent(FileContent); + const alert = FileAlert.VIDEO_FORMAT_ERROR; + fileContent.vm.$emit("add:alert", alert); + + expect(addAlertMock).toHaveBeenCalledWith(alert); + }); + }); + + describe("when file content emits fetch:file event", () => { + it("should call fetchFile when FileContent emits fetch:file event", async () => { + const { wrapper, fetchFile } = setup(); + + await wrapper.vm.$nextTick(); + + expect(fetchFile).toHaveBeenCalledTimes(1); + + const fileContent = wrapper.findComponent(FileContent); + fileContent.vm.$emit("fetch:file"); + + expect(fetchFile).toHaveBeenCalledTimes(2); + }); + }); + describe("when v-card emits keydown.down event", () => { it("should emit move-keyboard:edit event", async () => { const { wrapper } = setup(); @@ -244,21 +285,6 @@ describe("FileContentElement", () => { expect(fileProperties).toEqual(expectedFileProperties); }); - it("should call fetchFile when FileContent emits fetch:file event", async () => { - const { wrapper, fetchFile } = setup(); - - await wrapper.vm.$nextTick(); - - expect(fetchFile).toHaveBeenCalledTimes(1); - - const fileContent = wrapper.findComponent(FileContent); - fileContent.vm.$emit("fetch:file"); - - await wrapper.vm.$nextTick(); - - expect(fetchFile).toHaveBeenCalledTimes(2); - }); - it("should not render File Upload component", async () => { const { wrapper } = setup(); await wrapper.vm.$nextTick(); @@ -455,6 +481,7 @@ describe("FileContentElement", () => { size: fileRecordResponse.size, previewStatus: fileRecordResponse.previewStatus, element, + mimeType: fileRecordResponse.mimeType, }; const { wrapper, menu } = getWrapper({ diff --git a/src/components/feature-board-file-element/FileContentElement.vue b/src/components/feature-board-file-element/FileContentElement.vue index bc76bf5b16..7fc570029d 100644 --- a/src/components/feature-board-file-element/FileContentElement.vue +++ b/src/components/feature-board-file-element/FileContentElement.vue @@ -13,10 +13,12 @@ @@ -61,8 +63,10 @@ import { ref, toRef, } from "vue"; +import { useFileAlerts } from "./content/alert/useFileAlerts.composable"; import FileContent from "./content/FileContent.vue"; import { useFileStorageApi } from "./shared/composables/FileStorageApi.composable"; +import { FileAlert } from "./shared/types/FileAlert.enum"; import FileUpload from "./upload/FileUpload.vue"; export default defineComponent({ @@ -97,6 +101,8 @@ export default defineComponent({ FileRecordParentType.BOARDNODES ); + const { alerts, addAlert } = useFileAlerts(fileRecord); + const fileProperties = computed(() => { if (fileRecord.value === undefined) { return; @@ -115,6 +121,7 @@ export default defineComponent({ isDownloadAllowed: isDownloadAllowed( fileRecord.value.securityCheckStatus ), + mimeType: fileRecord.value.mimeType, element: props.element, }; }); @@ -161,6 +168,9 @@ export default defineComponent({ modelValue.value.caption = value; }; + const onAddAlert = (alert: FileAlert) => { + addAlert(alert); + }; const onDelete = () => emit("delete:element", element.value.id); const onMoveUp = () => emit("move-up:edit"); const onMoveDown = () => emit("move-down:edit"); @@ -172,11 +182,13 @@ export default defineComponent({ hasFileRecord, isOutlined, modelValue, + alerts, onKeydownArrow, onUploadFile, onFetchFile, onUpdateAlternativeText, onUpdateCaption, + onAddAlert, onDelete, onMoveUp, onMoveDown, diff --git a/src/components/feature-board-file-element/content/FileContent.unit.ts b/src/components/feature-board-file-element/content/FileContent.unit.ts index 39d94f0f24..37332edd23 100644 --- a/src/components/feature-board-file-element/content/FileContent.unit.ts +++ b/src/components/feature-board-file-element/content/FileContent.unit.ts @@ -2,8 +2,10 @@ import { PreviewStatus } from "@/fileStorageApi/v3"; import { fileElementResponseFactory } from "@@/tests/test-utils"; import createComponentMocks from "@@/tests/test-utils/componentMocks"; import { shallowMount } from "@vue/test-utils"; +import { FileAlert } from "../shared/types/FileAlert.enum"; +import FileAlerts from "./alert/FileAlerts.vue"; +import FileDisplay from "./display/FileDisplay.vue"; import FileContent from "./FileContent.vue"; -import FileAlert from "./alert/FileAlert.vue"; import ContentElementFooter from "./footer/ContentElementFooter.vue"; import FileInputs from "./inputs/FileInputs.vue"; @@ -24,10 +26,13 @@ describe("FileContent", () => { isDownloadAllowed: true, element, }; + + const alerts = [FileAlert.AWAITING_SCAN_STATUS]; const wrapper = shallowMount(FileContent, { propsData: { fileProperties, isEditMode: true, + alerts, }, ...createComponentMocks({}), }); @@ -35,13 +40,14 @@ describe("FileContent", () => { return { wrapper, fileProperties, + alerts, }; }; - it("should pass props to FileContent", () => { + it("should pass props to FileDisplay", () => { const { wrapper, fileProperties } = setup(); - const fileContent = wrapper.findComponent(FileContent); + const fileContent = wrapper.findComponent(FileDisplay); expect(fileContent.props()).toEqual({ fileProperties, @@ -61,54 +67,73 @@ describe("FileContent", () => { }); it("should pass props to FileAlert", () => { - const { wrapper, fileProperties } = setup(); + const { wrapper, alerts } = setup(); - const fileAlert = wrapper.findComponent(FileAlert); + const fileAlert = wrapper.findComponent(FileAlerts); expect(fileAlert.props()).toEqual({ - previewStatus: fileProperties.previewStatus, + alerts, }); }); - it("should FileInputs component be in dom", () => { - const { wrapper } = setup(); + it("should pass props to FileInputs", () => { + const { wrapper, fileProperties } = setup(); const fileInputs = wrapper.findComponent(FileInputs); - expect(fileInputs.exists()).toBe(true); + expect(fileInputs.props()).toEqual({ + fileProperties, + isEditMode: true, + }); }); - it("should emit update:alternativeText event, when it receives update:text event from file inputs component", async () => { - const { wrapper } = setup(); + describe("when file inputs emit update:alternativeText", () => { + it("should emit update:alternativeText", async () => { + const { wrapper } = setup(); - const fileInputs = wrapper.findComponent(FileInputs); + const fileInputs = wrapper.findComponent(FileInputs); - fileInputs.vm.$emit("update:alternativeText"); + fileInputs.vm.$emit("update:alternativeText"); - expect(wrapper.emitted("update:alternativeText")).toHaveLength(1); + expect(wrapper.emitted("update:alternativeText")).toHaveLength(1); + }); }); - it("should emit update:caption event, when it receives update:caption event from file inputs component", async () => { - const { wrapper } = setup(); + describe("when file inputs emit update:caption", () => { + it("should emit update:caption event, when it receives update:caption event from file inputs component", async () => { + const { wrapper } = setup(); - const fileInputs = wrapper.findComponent(FileInputs); + const fileInputs = wrapper.findComponent(FileInputs); - fileInputs.vm.$emit("update:caption"); + fileInputs.vm.$emit("update:caption"); - expect(wrapper.emitted("update:caption")).toHaveLength(1); + expect(wrapper.emitted("update:caption")).toHaveLength(1); + }); }); - describe("when alert emits on-status-reload", () => { + describe("when file alerts emits on-status-reload", () => { it("should emit fetch:file event", async () => { const { wrapper } = setup(); - const fileAlert = wrapper.findComponent(FileAlert); + const fileAlert = wrapper.findComponent(FileAlerts); await fileAlert.vm.$emit("on-status-reload"); expect(wrapper.emitted("fetch:file")).toBeTruthy(); }); }); + + describe("when file display emits add:alert", () => { + it("should emit add:alert event", async () => { + const { wrapper } = setup(); + + const fileDisplay = wrapper.findComponent(FileDisplay); + + fileDisplay.vm.$emit("add:alert"); + + expect(wrapper.emitted("add:alert")).toHaveLength(1); + }); + }); }); }); }); diff --git a/src/components/feature-board-file-element/content/FileContent.vue b/src/components/feature-board-file-element/content/FileContent.vue index c0c22e623b..34e546110b 100644 --- a/src/components/feature-board-file-element/content/FileContent.vue +++ b/src/components/feature-board-file-element/content/FileContent.vue @@ -1,6 +1,10 @@ diff --git a/src/components/feature-board-file-element/content/alert/FileAlerts.unit.ts b/src/components/feature-board-file-element/content/alert/FileAlerts.unit.ts new file mode 100644 index 0000000000..abcd4d9161 --- /dev/null +++ b/src/components/feature-board-file-element/content/alert/FileAlerts.unit.ts @@ -0,0 +1,104 @@ +import { I18N_KEY } from "@/utils/inject"; +import { i18nMock } from "@@/tests/test-utils"; +import createComponentMocks from "@@/tests/test-utils/componentMocks"; +import { ErrorAlert, InfoAlert, WarningAlert } from "@ui-alert"; +import { shallowMount } from "@vue/test-utils"; +import { FileAlert } from "../../shared/types/FileAlert.enum"; +import FileAlerts from "./FileAlerts.vue"; + +describe("FileAlerts", () => { + const setup = (alerts: FileAlert[]) => { + document.body.setAttribute("data-app", "true"); + + const wrapper = shallowMount(FileAlerts, { + ...createComponentMocks({ i18n: true }), + provide: { + [I18N_KEY.valueOf()]: i18nMock, + }, + propsData: { alerts }, + }); + return { wrapper, alerts }; + }; + + describe("when alerts contains FileAlert.VIDEO_FORMAT_ERROR", () => { + it("should render FileAlert.VIDEO_FORMAT_ERROR", () => { + const { wrapper } = setup([FileAlert.VIDEO_FORMAT_ERROR]); + + const infoAlert = wrapper.findComponent(InfoAlert); + + expect(infoAlert.text()).toBe( + "components.cardElement.fileElement.videoFormatError" + ); + }); + }); + + describe("when alerts contains FileAlert.AWAITING_SCAN_STATUS", () => { + it("should render FileAlert.AWAITING_SCAN_STATUS", () => { + const { wrapper } = setup([FileAlert.AWAITING_SCAN_STATUS]); + + const infoAlert = wrapper.findComponent(InfoAlert); + + expect(infoAlert.text()).toContain( + "components.cardElement.fileElement.awaitingScan" + ); + expect(infoAlert.text()).toContain( + "components.cardElement.fileElement.reloadStatus" + ); + }); + }); + + describe("when alerts contains FileAlert.SCAN_STATUS_WONT_CHECK", () => { + it("should render FileAlert.SCAN_STATUS_WONT_CHECK", () => { + const { wrapper } = setup([FileAlert.SCAN_STATUS_WONT_CHECK]); + + const infoAlert = wrapper.findComponent(InfoAlert); + + expect(infoAlert.text()).toBe( + "components.cardElement.fileElement.scanWontCheck" + ); + }); + }); + + describe("when alerts contains FileAlert.SCAN_STATUS_ERROR", () => { + it("should render FileAlert.SCAN_STATUS_ERROR", () => { + const { wrapper } = setup([FileAlert.SCAN_STATUS_ERROR]); + + const warningAlert = wrapper.findComponent(WarningAlert); + + expect(warningAlert.text()).toBe( + "components.cardElement.fileElement.scanError" + ); + }); + }); + + describe("when alerts contains FileAlert.SCAN_STATUS_BLOCKED", () => { + it("should render FileAlert.SCAN_STATUS_BLOCKED", () => { + const { wrapper } = setup([FileAlert.SCAN_STATUS_BLOCKED]); + + const errorAlert = wrapper.findComponent(ErrorAlert); + + expect(errorAlert.text()).toBe( + "components.cardElement.fileElement.virusDetected" + ); + }); + }); + + describe("when alerts contains SCAN_STATUS_BLOCKED and VIDEO_FORMAT_ERROR", () => { + it("should render FileAlert.SCAN_STATUS_BLOCKED", () => { + const { wrapper } = setup([ + FileAlert.SCAN_STATUS_BLOCKED, + FileAlert.VIDEO_FORMAT_ERROR, + ]); + + const errorAlert = wrapper.findComponent(ErrorAlert); + expect(errorAlert.text()).toContain( + "components.cardElement.fileElement.virusDetected" + ); + + const infoAlert = wrapper.findComponent(InfoAlert); + expect(infoAlert.text()).toBe( + "components.cardElement.fileElement.videoFormatError" + ); + }); + }); +}); diff --git a/src/components/feature-board-file-element/content/alert/FileAlerts.vue b/src/components/feature-board-file-element/content/alert/FileAlerts.vue new file mode 100644 index 0000000000..bc14cbd193 --- /dev/null +++ b/src/components/feature-board-file-element/content/alert/FileAlerts.vue @@ -0,0 +1,56 @@ + + + diff --git a/src/components/feature-board-file-element/content/alert/useFileAlerts.composable.ts b/src/components/feature-board-file-element/content/alert/useFileAlerts.composable.ts new file mode 100644 index 0000000000..361eb94df5 --- /dev/null +++ b/src/components/feature-board-file-element/content/alert/useFileAlerts.composable.ts @@ -0,0 +1,47 @@ +import { FileRecordResponse, PreviewStatus } from "@/fileStorageApi/v3"; +import { computed, Ref, ref } from "vue"; +import { FileAlert } from "../../shared/types/FileAlert.enum"; + +export const useFileAlerts = ( + fileRecord: Ref +) => { + const emittedAlerts: Ref = ref([]); + + const previewStatusAlert = computed(() => { + return mapPreviewStatusToFileAlert(fileRecord?.value?.previewStatus); + }); + + const alerts = computed(() => { + const alerts = [...emittedAlerts.value]; + + if (previewStatusAlert.value !== undefined) + alerts.push(previewStatusAlert.value); + + return alerts; + }); + + const addAlert = (alert: FileAlert) => { + emittedAlerts.value.push(alert); + }; + + return { + alerts, + addAlert, + }; +}; + +function mapPreviewStatusToFileAlert( + previewStatus?: PreviewStatus +): FileAlert | undefined { + console.log(previewStatus); + if (previewStatus === PreviewStatus.AWAITING_SCAN_STATUS) + return FileAlert.AWAITING_SCAN_STATUS; + if (previewStatus === PreviewStatus.PREVIEW_NOT_POSSIBLE_SCAN_STATUS_BLOCKED) + return FileAlert.SCAN_STATUS_BLOCKED; + if (previewStatus === PreviewStatus.PREVIEW_NOT_POSSIBLE_SCAN_STATUS_ERROR) + return FileAlert.SCAN_STATUS_ERROR; + if ( + previewStatus === PreviewStatus.PREVIEW_NOT_POSSIBLE_SCAN_STATUS_WONT_CHECK + ) + return FileAlert.SCAN_STATUS_WONT_CHECK; +} diff --git a/src/components/feature-board-file-element/content/alert/useFileAlerts.composable.unit.ts b/src/components/feature-board-file-element/content/alert/useFileAlerts.composable.unit.ts new file mode 100644 index 0000000000..d0cb3fb0ec --- /dev/null +++ b/src/components/feature-board-file-element/content/alert/useFileAlerts.composable.unit.ts @@ -0,0 +1,105 @@ +import { PreviewStatus } from "@/fileStorageApi/v3"; +import { + fileRecordResponseFactory, + mountComposable, +} from "@@/tests/test-utils"; +import { ref } from "vue"; +import { FileAlert } from "../../shared/types/FileAlert.enum"; +import { useFileAlerts } from "./useFileAlerts.composable"; + +describe("useFileAlerts", () => { + describe("when filerecord is undefined", () => { + const setup = () => { + const fileRecord = ref(); + const { alerts } = mountComposable(() => useFileAlerts(fileRecord)); + + return { + alerts, + }; + }; + + it("should return alerts empty", () => { + const { alerts } = setup(); + expect(alerts.value).toEqual([]); + }); + }); + + describe("when filerecord is defined", () => { + const setup = (previewStatus: PreviewStatus) => { + const fileRecord = ref( + fileRecordResponseFactory.build({ previewStatus }) + ); + const { alerts, addAlert } = mountComposable(() => + useFileAlerts(fileRecord) + ); + + return { + alerts, + fileRecord, + addAlert, + }; + }; + + describe("when previewStatus is AWAITING_SCAN_STATUS", () => { + it("should return AWAITING_SCAN_STATUS alert", () => { + const { alerts } = setup(PreviewStatus.AWAITING_SCAN_STATUS); + expect(alerts.value).toEqual([FileAlert.AWAITING_SCAN_STATUS]); + }); + }); + + describe("when previewStatus is PREVIEW_NOT_POSSIBLE_SCAN_STATUS_BLOCKED", () => { + it("should return SCAN_STATUS_BLOCKED alert", () => { + const { alerts } = setup( + PreviewStatus.PREVIEW_NOT_POSSIBLE_SCAN_STATUS_BLOCKED + ); + expect(alerts.value).toEqual([FileAlert.SCAN_STATUS_BLOCKED]); + }); + }); + + describe("when previewStatus is PREVIEW_NOT_POSSIBLE_SCAN_STATUS_ERROR", () => { + it("should return SCAN_STATUS_ERROR alert", () => { + const { alerts } = setup( + PreviewStatus.PREVIEW_NOT_POSSIBLE_SCAN_STATUS_ERROR + ); + expect(alerts.value).toEqual([FileAlert.SCAN_STATUS_ERROR]); + }); + }); + + describe("when previewStatus is PREVIEW_NOT_POSSIBLE_SCAN_STATUS_WONT_CHECK", () => { + it("should return SCAN_STATUS_WONT_CHECK alert", () => { + const { alerts } = setup( + PreviewStatus.PREVIEW_NOT_POSSIBLE_SCAN_STATUS_WONT_CHECK + ); + + expect(alerts.value).toEqual([FileAlert.SCAN_STATUS_WONT_CHECK]); + }); + }); + + describe("when previewStatus changes from AWAITING_SCAN_STATUS to PREVIEW_POSSIBLE", () => { + it("should return SCAN_STATUS_WONT_CHECK alert", () => { + const { alerts, fileRecord } = setup( + PreviewStatus.AWAITING_SCAN_STATUS + ); + + fileRecord.value = fileRecordResponseFactory.build({ + previewStatus: PreviewStatus.PREVIEW_POSSIBLE, + }); + + expect(alerts.value).toEqual([]); + }); + }); + + describe("when alert is added", () => { + it("should return correct alerts array", () => { + const { alerts, addAlert } = setup(PreviewStatus.AWAITING_SCAN_STATUS); + + addAlert(FileAlert.VIDEO_FORMAT_ERROR); + + expect(alerts.value).toEqual([ + FileAlert.VIDEO_FORMAT_ERROR, + FileAlert.AWAITING_SCAN_STATUS, + ]); + }); + }); + }); +}); diff --git a/src/components/feature-board-file-element/content/display/FileDisplay.unit.ts b/src/components/feature-board-file-element/content/display/FileDisplay.unit.ts index 3cad2add66..385beeafbd 100644 --- a/src/components/feature-board-file-element/content/display/FileDisplay.unit.ts +++ b/src/components/feature-board-file-element/content/display/FileDisplay.unit.ts @@ -1,112 +1,244 @@ +import { isVideoMimeType } from "@/utils/fileHelper"; import { fileElementResponseFactory } from "@@/tests/test-utils"; import createComponentMocks from "@@/tests/test-utils/componentMocks"; import { shallowMount } from "@vue/test-utils"; -import FileDisplay from "./FileDisplay.vue"; import FileDescription from "./file-description/FileDescription.vue"; +import FileDisplay from "./FileDisplay.vue"; import ImageDisplay from "./image-display/ImageDisplay.vue"; +import VideoDisplay from "./video-display/VideoDisplay.vue"; + +jest.mock("@/utils/fileHelper"); +const isVideoMimeTypeMock = jest.mocked(isVideoMimeType); describe("FileDisplay", () => { describe("when previewUrl is defined", () => { - const setup = () => { - document.body.setAttribute("data-app", "true"); - - const element = fileElementResponseFactory.build(); - const propsData = { - fileProperties: { - name: "test", - size: 100, - url: "test", - previewUrl: "test", - previewStatus: "test", - isDownloadAllowed: true, - element, - }, - isEditMode: true, + describe("when mimeType is not a video type", () => { + const setup = () => { + document.body.setAttribute("data-app", "true"); + + const element = fileElementResponseFactory.build(); + const propsData = { + fileProperties: { + name: "test", + size: 100, + url: "test", + previewUrl: "test", + previewStatus: "test", + isDownloadAllowed: true, + element, + }, + isEditMode: true, + }; + + isVideoMimeTypeMock.mockReset(); + isVideoMimeTypeMock.mockReturnValueOnce(false); + + const wrapper = shallowMount(FileDisplay, { + propsData, + ...createComponentMocks({}), + }); + + return { + wrapper, + fileNameProp: propsData.fileProperties.name, + previewUrlProp: propsData.fileProperties.previewUrl, + srcProp: propsData.fileProperties.url, + }; }; - const wrapper = shallowMount(FileDisplay, { - propsData, - ...createComponentMocks({}), + it("should be found in dom", () => { + const { wrapper } = setup(); + + const fileDisplay = wrapper.findComponent(FileDisplay); + + expect(fileDisplay.exists()).toBe(true); }); - return { - wrapper, - fileNameProp: propsData.fileProperties.name, - previewUrlProp: propsData.fileProperties.previewUrl, - }; - }; + it("should pass correct props to image display component", async () => { + const { wrapper, fileNameProp, previewUrlProp, srcProp } = setup(); + + const props = wrapper.findComponent(ImageDisplay).attributes(); + expect(props.name).toBe(fileNameProp); + expect(props.previewsrc).toBe(previewUrlProp); + expect(props.src).toBe(srcProp); + expect(props.iseditmode).toBe("true"); + expect(props.element).toBeDefined(); + }); - it("should be found in dom", () => { - const { wrapper } = setup(); + it("should render file description display component", () => { + const { wrapper } = setup(); - const fileDisplay = wrapper.findComponent(FileDisplay); + const fileDescription = wrapper.findComponent(FileDescription); - expect(fileDisplay.exists()).toBe(true); + expect(fileDescription.exists()).toBe(true); + }); }); - it("should pass correct props to image display component", () => { - const { wrapper, fileNameProp, previewUrlProp } = setup(); + describe("when mimeType is a video type", () => { + const setup = () => { + document.body.setAttribute("data-app", "true"); + + const element = fileElementResponseFactory.build(); + const propsData = { + fileProperties: { + name: "test", + size: 100, + url: "test", + previewUrl: "test", + previewStatus: "test", + isDownloadAllowed: true, + element, + }, + isEditMode: true, + }; + + isVideoMimeTypeMock.mockReset(); + isVideoMimeTypeMock.mockReturnValueOnce(true); + + const wrapper = shallowMount(FileDisplay, { + propsData, + ...createComponentMocks({}), + }); + + return { + wrapper, + fileNameProp: propsData.fileProperties.name, + previewUrlProp: propsData.fileProperties.previewUrl, + srcProp: propsData.fileProperties.url, + }; + }; - const props = wrapper.findComponent(ImageDisplay).attributes(); + it("should be found in dom", () => { + const { wrapper } = setup(); - expect(props.name).toBe(fileNameProp); - expect(props.previewurl).toBe(previewUrlProp); - expect(props.iseditmode).toBe("true"); - }); + const fileDisplay = wrapper.findComponent(FileDisplay); + + expect(fileDisplay.exists()).toBe(true); + }); + + it("should pass correct props to image display component", () => { + const { wrapper, fileNameProp, previewUrlProp, srcProp } = setup(); - it("should render file description display component", () => { - const { wrapper } = setup(); + const props = wrapper.findComponent(ImageDisplay).attributes(); - const fileDescription = wrapper.findComponent(FileDescription); + expect(props.name).toBe(fileNameProp); + expect(props.previewsrc).toBe(previewUrlProp); + expect(props.src).toBe(srcProp); + expect(props.iseditmode).toBe("true"); + expect(props.element).toBeDefined(); + }); + + it("should render file description display component", () => { + const { wrapper } = setup(); - expect(fileDescription.exists()).toBe(true); + const fileDescription = wrapper.findComponent(FileDescription); + + expect(fileDescription.exists()).toBe(true); + }); }); }); describe("when previewUrl is undefined", () => { - const setup = () => { - document.body.setAttribute("data-app", "true"); - - const element = fileElementResponseFactory.build(); - const propsData = { - fileProperties: { - name: "test", - size: 100, - url: "test", - previewUrl: undefined, - previewStatus: "test", - isDownloadAllowed: true, - element, - }, - isEditMode: true, + describe("when mimeType is a video type", () => { + const setup = () => { + document.body.setAttribute("data-app", "true"); + + const element = fileElementResponseFactory.build(); + const propsData = { + fileProperties: { + name: "test", + size: 100, + url: "test", + previewUrl: undefined, + previewStatus: "test", + isDownloadAllowed: true, + element, + }, + isEditMode: true, + }; + + isVideoMimeTypeMock.mockReset(); + isVideoMimeTypeMock.mockReturnValueOnce(true); + + const wrapper = shallowMount(FileDisplay, { + propsData, + ...createComponentMocks({}), + }); + + return { + wrapper, + fileNameProp: propsData.fileProperties.name, + previewUrlProp: propsData.fileProperties.previewUrl, + url: propsData.fileProperties.url, + }; }; - const wrapper = shallowMount(FileDisplay, { - propsData, - ...createComponentMocks({}), + it("should render video display component", async () => { + const { wrapper } = setup(); + + const videoDisplay = wrapper.findComponent(VideoDisplay); + + expect(videoDisplay.exists()).toBe(true); + }); + + it("should pass correct props to video display component", () => { + const { wrapper, fileNameProp, url } = setup(); + + const props = wrapper.findComponent(VideoDisplay).attributes(); + + expect(props.src).toBe(url); + expect(props.name).toBe(fileNameProp); }); + }); - return { - wrapper, - fileNameProp: propsData.fileProperties.name, - previewUrlProp: propsData.fileProperties.previewUrl, + describe("when mimeType is not a video type", () => { + const setup = () => { + document.body.setAttribute("data-app", "true"); + + const element = fileElementResponseFactory.build(); + const propsData = { + fileProperties: { + name: "test", + size: 100, + url: "test", + previewUrl: undefined, + previewStatus: "test", + isDownloadAllowed: true, + element, + }, + isEditMode: true, + }; + + isVideoMimeTypeMock.mockReset(); + isVideoMimeTypeMock.mockReturnValueOnce(false); + + const wrapper = shallowMount(FileDisplay, { + propsData, + ...createComponentMocks({}), + }); + + return { + wrapper, + fileNameProp: propsData.fileProperties.name, + previewUrlProp: propsData.fileProperties.previewUrl, + }; }; - }; - it("should render file description display component", () => { - const { wrapper } = setup(); + it("should render file description display component", () => { + const { wrapper } = setup(); - const fileDescription = wrapper.findComponent(FileDescription); + const fileDescription = wrapper.findComponent(FileDescription); - expect(fileDescription.exists()).toBe(true); - }); + expect(fileDescription.exists()).toBe(true); + }); - it("should not render image display component", () => { - const { wrapper } = setup(); + it("should not render image display component", () => { + const { wrapper } = setup(); - const imageDisplay = wrapper.findComponent(ImageDisplay); + const imageDisplay = wrapper.findComponent(ImageDisplay); - expect(imageDisplay.exists()).toBe(false); + expect(imageDisplay.exists()).toBe(false); + }); }); }); }); diff --git a/src/components/feature-board-file-element/content/display/FileDisplay.vue b/src/components/feature-board-file-element/content/display/FileDisplay.vue index 6fbc88eef1..f48061188c 100644 --- a/src/components/feature-board-file-element/content/display/FileDisplay.vue +++ b/src/components/feature-board-file-element/content/display/FileDisplay.vue @@ -2,18 +2,26 @@
+ + + @@ -22,14 +30,17 @@ diff --git a/src/components/feature-board-file-element/content/display/image-display/ImageDisplay.unit.ts b/src/components/feature-board-file-element/content/display/image-display/ImageDisplay.unit.ts index cd1da23632..0fd54c92f0 100644 --- a/src/components/feature-board-file-element/content/display/image-display/ImageDisplay.unit.ts +++ b/src/components/feature-board-file-element/content/display/image-display/ImageDisplay.unit.ts @@ -23,8 +23,8 @@ describe("ImageDisplay", () => { content: { alternativeText: props.alternativeText }, }); const propsData = { - url: "url/1/file-record #1.txt", - previewUrl: "preview/1/file-record #1.txt", + src: "url/1/file-record #1.txt", + previewSrc: "preview/1/file-record #1.txt", name: "file-record #1.txt", isEditMode: props.isEditMode, element, @@ -49,9 +49,9 @@ describe("ImageDisplay", () => { return { wrapper, - urlProp: propsData.url, + src: propsData.src, nameProp: propsData.name, - previewUrlProp: propsData.previewUrl, + previewSrc: propsData.previewSrc, element, open, }; @@ -95,11 +95,11 @@ describe("ImageDisplay", () => { }); it("should have set src correctly", () => { - const { wrapper, previewUrlProp } = setup({ isEditMode: false }); + const { wrapper, previewSrc } = setup({ isEditMode: false }); const src = wrapper.find(imageSelektor).attributes("src"); - expect(src).toBe(previewUrlProp); + expect(src).toBe(previewSrc); }); it("should have set alt correctly", () => { @@ -140,13 +140,13 @@ describe("ImageDisplay", () => { it("should call open function, when the container is clicked", () => { const alternativeText = "alternative text"; - const { wrapper, urlProp, nameProp, open } = setup({ + const { wrapper, src, nameProp, open } = setup({ isEditMode: false, alternativeText, }); const options: LightBoxOptions = { - downloadUrl: urlProp, - previewUrl: urlProp, + downloadUrl: src, + previewUrl: src, alt: alternativeText, name: nameProp, }; @@ -160,13 +160,13 @@ describe("ImageDisplay", () => { it("should call open function, when the image is clicked", () => { const alternativeText = "alternative text"; - const { wrapper, urlProp, nameProp, open } = setup({ + const { wrapper, src, nameProp, open } = setup({ isEditMode: false, alternativeText, }); const options: LightBoxOptions = { - downloadUrl: urlProp, - previewUrl: urlProp, + downloadUrl: src, + previewUrl: src, alt: alternativeText, name: nameProp, }; @@ -180,13 +180,13 @@ describe("ImageDisplay", () => { it("should call open function, when the overlay is clicked", async () => { const alternativeText = "alternative text"; - const { wrapper, urlProp, nameProp, open } = setup({ + const { wrapper, src, nameProp, open } = setup({ isEditMode: false, alternativeText, }); const options: LightBoxOptions = { - downloadUrl: urlProp, - previewUrl: urlProp, + downloadUrl: src, + previewUrl: src, alt: alternativeText, name: nameProp, }; @@ -254,13 +254,13 @@ describe("ImageDisplay", () => { it("should call open function, when Enter key is pressed", async () => { const alternativeText = "alternative text"; - const { wrapper, urlProp, nameProp, open } = setup({ + const { wrapper, src, nameProp, open } = setup({ isEditMode: false, alternativeText, }); const options: LightBoxOptions = { - downloadUrl: urlProp, - previewUrl: urlProp, + downloadUrl: src, + previewUrl: src, alt: alternativeText, name: nameProp, }; @@ -275,13 +275,13 @@ describe("ImageDisplay", () => { it("should call open function, when Space key is pressed", async () => { const alternativeText = "alternative text"; - const { wrapper, urlProp, nameProp, open } = setup({ + const { wrapper, src, nameProp, open } = setup({ isEditMode: false, alternativeText, }); const options: LightBoxOptions = { - downloadUrl: urlProp, - previewUrl: urlProp, + downloadUrl: src, + previewUrl: src, alt: alternativeText, name: nameProp, }; @@ -332,11 +332,13 @@ describe("ImageDisplay", () => { }); it("should have set src correctly", () => { - const { wrapper, previewUrlProp } = setup({ isEditMode: true }); + const { wrapper, previewSrc } = setup({ + isEditMode: true, + }); const src = wrapper.find(imageSelektor).attributes("src"); - expect(src).toBe(previewUrlProp); + expect(src).toBe(previewSrc); }); describe("When alternative text is defined", () => { diff --git a/src/components/feature-board-file-element/content/display/image-display/ImageDisplay.vue b/src/components/feature-board-file-element/content/display/image-display/ImageDisplay.vue index 5dfd591f64..8f03224b90 100644 --- a/src/components/feature-board-file-element/content/display/image-display/ImageDisplay.vue +++ b/src/components/feature-board-file-element/content/display/image-display/ImageDisplay.vue @@ -16,14 +16,13 @@ - - - - + + +
@@ -33,16 +32,18 @@ import { convertDownloadToPreviewUrl } from "@/utils/fileHelper"; import { I18N_KEY, injectStrict } from "@/utils/inject"; import { LightBoxOptions, useLightBox } from "@ui-light-box"; import { PropType, computed, defineComponent, ref } from "vue"; +import { ContentElementBar } from "@ui-board"; export default defineComponent({ name: "ImageDisplay", props: { - url: { type: String, required: true }, - previewUrl: { type: String, required: true }, + src: { type: String, required: true }, + previewSrc: { type: String, required: true }, name: { type: String, required: true }, isEditMode: { type: Boolean, required: true }, element: { type: Object as PropType, required: true }, }, + components: { ContentElementBar }, setup(props) { const i18n = injectStrict(I18N_KEY); const containerRef = ref(); @@ -106,10 +107,10 @@ export default defineComponent({ }; const openLightBox = () => { - const previewUrl = convertDownloadToPreviewUrl(props.url); + const previewUrl = convertDownloadToPreviewUrl(props.src); const options: LightBoxOptions = { - downloadUrl: props.url, + downloadUrl: props.src, previewUrl: previewUrl, alt: alternativeText.value, name: props.name, @@ -164,5 +165,6 @@ export default defineComponent({ .menu { position: absolute; top: 0px; + right: 0px; } diff --git a/src/components/feature-board-file-element/content/display/video-display/VideoDisplay.unit.ts b/src/components/feature-board-file-element/content/display/video-display/VideoDisplay.unit.ts new file mode 100644 index 0000000000..388d78dd07 --- /dev/null +++ b/src/components/feature-board-file-element/content/display/video-display/VideoDisplay.unit.ts @@ -0,0 +1,64 @@ +import { mount } from "@vue/test-utils"; +import VideoDisplay from ".//VideoDisplay.vue"; + +describe("VideoDisplay", () => { + const setup = () => { + document.body.setAttribute("data-app", "true"); + + const src = "test-source"; + const name = "test-name"; + const slotContent = "test-slot-content"; + const propsData = { + src, + name, + }; + + const wrapper = mount(VideoDisplay, { + attachTo: document.body, + propsData, + slots: { + default: slotContent, + }, + }); + + return { + wrapper, + src, + name, + }; + }; + + it("should render video element", () => { + const { wrapper } = setup(); + + const video = wrapper.find("video"); + expect(video.exists()).toBe(true); + }); + + it("should render video element with correct attributes", () => { + const { wrapper, src, name } = setup(); + + const video = wrapper.find("video"); + expect(video.attributes("src")).toBe(src); + expect(video.attributes("alt")).toBe(name); + expect(video.attributes("controls")).toBe("controls"); + expect(video.attributes("loading")).toBe("lazy"); + }); + + it("should render with slot content", () => { + const { wrapper } = setup(); + + expect(wrapper.text()).toContain("test-slot-content"); + }); + + describe("when video dispatches error event", () => { + it("should emit error event", () => { + const { wrapper } = setup(); + + const video = wrapper.find("video"); + video.trigger("error"); + + expect(wrapper.emitted("error")).toBeTruthy(); + }); + }); +}); diff --git a/src/components/feature-board-file-element/content/display/video-display/VideoDisplay.vue b/src/components/feature-board-file-element/content/display/video-display/VideoDisplay.vue new file mode 100644 index 0000000000..a67b187e27 --- /dev/null +++ b/src/components/feature-board-file-element/content/display/video-display/VideoDisplay.vue @@ -0,0 +1,52 @@ + + + + diff --git a/src/components/feature-board-file-element/shared/types/FileAlert.enum.ts b/src/components/feature-board-file-element/shared/types/FileAlert.enum.ts new file mode 100644 index 0000000000..9f9aab29d5 --- /dev/null +++ b/src/components/feature-board-file-element/shared/types/FileAlert.enum.ts @@ -0,0 +1,7 @@ +export enum FileAlert { + VIDEO_FORMAT_ERROR, + AWAITING_SCAN_STATUS, + SCAN_STATUS_WONT_CHECK, + SCAN_STATUS_BLOCKED, + SCAN_STATUS_ERROR, +} diff --git a/src/components/feature-board-file-element/shared/types/file-properties.ts b/src/components/feature-board-file-element/shared/types/file-properties.ts index 98f903fc1f..4170a3b4bb 100644 --- a/src/components/feature-board-file-element/shared/types/file-properties.ts +++ b/src/components/feature-board-file-element/shared/types/file-properties.ts @@ -8,5 +8,6 @@ export interface FileProperties { previewUrl?: string; previewStatus: PreviewStatus; isDownloadAllowed: boolean; + mimeType: string; element: FileElementResponse; } diff --git a/src/components/ui-alert/BaseAlert.unit.ts b/src/components/ui-alert/BaseAlert.unit.ts new file mode 100644 index 0000000000..ba2b19b44f --- /dev/null +++ b/src/components/ui-alert/BaseAlert.unit.ts @@ -0,0 +1,74 @@ +import { shallowMount } from "@vue/test-utils"; +import BaseAlert from "./BaseAlert.vue"; + +describe("BaseAlert", () => { + describe("when default slot is defined", () => { + const setup = () => { + document.body.setAttribute("data-app", "true"); + + const slot = "TestSlot"; + const color = "TestColor"; + const icon = "TestIcon"; + const wrapper = shallowMount(BaseAlert, { + propsData: { + color, + icon, + }, + slots: { + default: slot, + }, + }); + + return { + wrapper, + slot, + color, + icon, + }; + }; + + it("should render default slot", () => { + const { wrapper, slot } = setup(); + + const text = wrapper.text(); + + expect(text).toContain(slot); + }); + + it("should pass color", () => { + const { wrapper, color } = setup(); + + const attribute = wrapper.attributes("color"); + + expect(attribute).toBe(color); + }); + + it("should pass icon", () => { + const { wrapper, icon } = setup(); + + const attribute = wrapper.attributes("icon"); + + expect(attribute).toBe(icon); + }); + }); + + describe("when default slot is not defined", () => { + const setup = () => { + document.body.setAttribute("data-app", "true"); + + const wrapper = shallowMount(BaseAlert, {}); + + return { + wrapper, + }; + }; + + it("should not render default slot", () => { + const { wrapper } = setup(); + + const text = wrapper.find("div.black--text"); + + expect(text.exists()).toBeFalsy(); + }); + }); +}); diff --git a/src/components/ui-alert/BaseAlert.vue b/src/components/ui-alert/BaseAlert.vue new file mode 100644 index 0000000000..37f7bf91c4 --- /dev/null +++ b/src/components/ui-alert/BaseAlert.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/ui-alert/ErrorAlert.unit.ts b/src/components/ui-alert/ErrorAlert.unit.ts new file mode 100644 index 0000000000..68cb7f59fd --- /dev/null +++ b/src/components/ui-alert/ErrorAlert.unit.ts @@ -0,0 +1,46 @@ +import { mdiAlertCircle } from "@mdi/js"; +import { shallowMount } from "@vue/test-utils"; +import BaseAlert from "./BaseAlert.vue"; +import ErrorAlert from "./ErrorAlert.vue"; + +describe("ErrorAlert", () => { + const setup = () => { + document.body.setAttribute("data-app", "true"); + + const slot = "TestSlot"; + const wrapper = shallowMount(ErrorAlert, { + slots: { + default: slot, + }, + }); + + return { + wrapper, + slot, + }; + }; + + it("should pass correct color to filealert", () => { + const { wrapper } = setup(); + + const color = wrapper.findComponent(BaseAlert).attributes("color"); + + expect(color).toBe("error"); + }); + + it("should pass correct icon to filealert", () => { + const { wrapper } = setup(); + + const icon = wrapper.findComponent(BaseAlert).attributes("icon"); + + expect(icon).toBe(mdiAlertCircle); + }); + + it("should pass slot to filealert", () => { + const { wrapper, slot } = setup(); + + const text = wrapper.findComponent(BaseAlert).text(); + + expect(text).toContain(slot); + }); +}); diff --git a/src/components/ui-alert/ErrorAlert.vue b/src/components/ui-alert/ErrorAlert.vue new file mode 100644 index 0000000000..ba71980be2 --- /dev/null +++ b/src/components/ui-alert/ErrorAlert.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/ui-alert/InfoAlert.unit.ts b/src/components/ui-alert/InfoAlert.unit.ts new file mode 100644 index 0000000000..f7e6cdce87 --- /dev/null +++ b/src/components/ui-alert/InfoAlert.unit.ts @@ -0,0 +1,46 @@ +import { mdiInformation } from "@mdi/js"; +import { shallowMount } from "@vue/test-utils"; +import BaseAlert from "./BaseAlert.vue"; +import InfoAlert from "./InfoAlert.vue"; + +describe("InfoAlert", () => { + const setup = () => { + document.body.setAttribute("data-app", "true"); + + const slot = "TestSlot"; + const wrapper = shallowMount(InfoAlert, { + slots: { + default: slot, + }, + }); + + return { + wrapper, + slot, + }; + }; + + it("should pass correct color to filealert", () => { + const { wrapper } = setup(); + + const color = wrapper.findComponent(BaseAlert).attributes("color"); + + expect(color).toBe("info"); + }); + + it("should pass correct icon to filealert", () => { + const { wrapper } = setup(); + + const icon = wrapper.findComponent(BaseAlert).attributes("icon"); + + expect(icon).toBe(mdiInformation); + }); + + it("should pass slot to filealert", () => { + const { wrapper, slot } = setup(); + + const text = wrapper.findComponent(BaseAlert).text(); + + expect(text).toContain(slot); + }); +}); diff --git a/src/components/ui-alert/InfoAlert.vue b/src/components/ui-alert/InfoAlert.vue new file mode 100644 index 0000000000..f7a423e281 --- /dev/null +++ b/src/components/ui-alert/InfoAlert.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/ui-alert/SuccessAlert.unit.ts b/src/components/ui-alert/SuccessAlert.unit.ts new file mode 100644 index 0000000000..eea5ef51d9 --- /dev/null +++ b/src/components/ui-alert/SuccessAlert.unit.ts @@ -0,0 +1,46 @@ +import { mdiCheckCircle } from "@mdi/js"; +import { shallowMount } from "@vue/test-utils"; +import BaseAlert from "./BaseAlert.vue"; +import SuccessAlert from "./SuccessAlert.vue"; + +describe("SuccessAlert", () => { + const setup = () => { + document.body.setAttribute("data-app", "true"); + + const slot = "TestSlot"; + const wrapper = shallowMount(SuccessAlert, { + slots: { + default: slot, + }, + }); + + return { + wrapper, + slot, + }; + }; + + it("should pass correct color to filealert", () => { + const { wrapper } = setup(); + console.log(wrapper.html()); + const color = wrapper.findComponent(BaseAlert).attributes("color"); + + expect(color).toBe("success"); + }); + + it("should pass correct icon to filealert", () => { + const { wrapper } = setup(); + + const icon = wrapper.findComponent(BaseAlert).attributes("icon"); + + expect(icon).toBe(mdiCheckCircle); + }); + + it("should pass slot to filealert", () => { + const { wrapper, slot } = setup(); + + const text = wrapper.findComponent(BaseAlert).text(); + + expect(text).toContain(slot); + }); +}); diff --git a/src/components/ui-alert/SuccessAlert.vue b/src/components/ui-alert/SuccessAlert.vue new file mode 100644 index 0000000000..fb52fd1157 --- /dev/null +++ b/src/components/ui-alert/SuccessAlert.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/ui-alert/WarningAlert.unit.ts b/src/components/ui-alert/WarningAlert.unit.ts new file mode 100644 index 0000000000..b3cb967aa8 --- /dev/null +++ b/src/components/ui-alert/WarningAlert.unit.ts @@ -0,0 +1,46 @@ +import { mdiAlert } from "@mdi/js"; +import { shallowMount } from "@vue/test-utils"; +import BaseAlert from "./BaseAlert.vue"; +import WarningAlert from "./WarningAlert.vue"; + +describe("WarningAlert", () => { + const setup = () => { + document.body.setAttribute("data-app", "true"); + + const slot = "TestSlot"; + const wrapper = shallowMount(WarningAlert, { + slots: { + default: slot, + }, + }); + + return { + wrapper, + slot, + }; + }; + + it("should pass correct color to filealert", () => { + const { wrapper } = setup(); + + const color = wrapper.findComponent(BaseAlert).attributes("color"); + + expect(color).toBe("warning"); + }); + + it("should pass correct icon to filealert", () => { + const { wrapper } = setup(); + + const icon = wrapper.findComponent(BaseAlert).attributes("icon"); + + expect(icon).toBe(mdiAlert); + }); + + it("should pass slot to filealert", () => { + const { wrapper, slot } = setup(); + + const text = wrapper.findComponent(BaseAlert).text(); + + expect(text).toContain(slot); + }); +}); diff --git a/src/components/ui-alert/WarningAlert.vue b/src/components/ui-alert/WarningAlert.vue new file mode 100644 index 0000000000..be4ff96e3d --- /dev/null +++ b/src/components/ui-alert/WarningAlert.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/ui-alert/index.ts b/src/components/ui-alert/index.ts new file mode 100644 index 0000000000..e55cac75c1 --- /dev/null +++ b/src/components/ui-alert/index.ts @@ -0,0 +1,6 @@ +import ErrorAlert from "./ErrorAlert.vue"; +import InfoAlert from "./InfoAlert.vue"; +import SuccessAlert from "./SuccessAlert.vue"; +import WarningAlert from "./WarningAlert.vue"; + +export { ErrorAlert, InfoAlert, WarningAlert, SuccessAlert }; diff --git a/src/locales/de.json b/src/locales/de.json index f6102bd1d8..663b924d36 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -816,6 +816,7 @@ "components.cardElement.fileElement.scanWontCheck": "Aufgrund der Größe kann keine Vorschau generiert werden.", "components.cardElement.fileElement.reloadStatus": "Status aktualisieren", "components.cardElement.fileElement.caption": "Beschreibung", + "components.cardElement.fileElement.videoFormatError": "Das Videoformat wird von diesem Browser / Betriebssystem nicht unterstützt.", "components.cardElement.richTextElement": "Textelement", "components.cardElement.richTextElement.placeholder": "Text hinzufügen", "components.cardElement.submissionElement": "Abgabe", diff --git a/src/locales/en.json b/src/locales/en.json index 7c93838d92..f20b7f0a42 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -814,6 +814,7 @@ "components.cardElement.fileElement.reloadStatus": "Update status", "components.cardElement.fileElement.scanWontCheck": "Due to the size, no preview can be generated.", "components.cardElement.fileElement.caption": "Caption", + "components.cardElement.fileElement.videoFormatError": "The video format is not supported by this browser/operating system.", "components.cardElement.richTextElement": "Text element", "components.cardElement.richTextElement.placeholder": "Add text", "components.cardElement.submissionElement": "Submission", diff --git a/src/locales/es.json b/src/locales/es.json index 7d941d9d95..e8602cace0 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -824,6 +824,7 @@ "components.cardElement.fileElement.scanWontCheck": "Debido al tamaño, no se puede generar una vista previa.", "components.cardElement.fileElement.reloadStatus": "Estado de actualización", "components.cardElement.fileElement.caption": "Descripción", + "components.cardElement.fileElement.videoFormatError": "El formato de vídeo no es compatible con este navegador / sistema operativo.", "components.cardElement.richTextElement": "Elemento texto", "components.cardElement.richTextElement.placeholder": "Añadir texto", "components.cardElement.submissionElement": "Envío", diff --git a/src/locales/uk.json b/src/locales/uk.json index e13e0ea5af..f3b6c10955 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -327,6 +327,7 @@ "components.cardElement.fileElement.scanWontCheck": "Через розмір не може бути створено прев'ю.", "components.cardElement.fileElement.reloadStatus": "Статус оновлення", "components.cardElement.fileElement.caption": "опис", + "components.cardElement.fileElement.videoFormatError": "Формат відео не підтримується цим браузером / операційною системою.", "components.cardElement.richTextElement": "Текстовий елемент", "components.cardElement.richTextElement.placeholder": "додати текст", "components.cardElement.submissionElement": "Подання", diff --git a/src/utils/fileHelper.ts b/src/utils/fileHelper.ts index b142f7e09e..a05f24f05f 100644 --- a/src/utils/fileHelper.ts +++ b/src/utils/fileHelper.ts @@ -71,3 +71,12 @@ export function isDownloadAllowed(scanStatus: FileRecordScanStatus): boolean { export function isPreviewPossible(previewStatus: PreviewStatus): boolean { return previewStatus === PreviewStatus.PREVIEW_POSSIBLE; } + +export function isVideoMimeType(mimeType: string): boolean { + return ( + mimeType.startsWith("video/") || + mimeType === "application/x-mpegURL" || + mimeType === "application/vnd.ms-asf" || + mimeType === "application/ogg" + ); +} diff --git a/src/utils/fileHelper.unit.ts b/src/utils/fileHelper.unit.ts index 0f5a2df4c5..02ca1683a1 100644 --- a/src/utils/fileHelper.unit.ts +++ b/src/utils/fileHelper.unit.ts @@ -10,6 +10,7 @@ import { getFileExtension, isDownloadAllowed, isPreviewPossible, + isVideoMimeType, } from "./fileHelper"; describe("@/utils/fileHelper", () => { @@ -307,4 +308,74 @@ describe("@/utils/fileHelper", () => { }); }); }); + + describe("isVideoMimeType", () => { + describe("when file has video mime type", () => { + describe("when file mime type has video/ prefix", () => { + it("should return true", () => { + const result = isVideoMimeType("video/mp4"); + + expect(result).toBe(true); + }); + + it("should return true", () => { + const result = isVideoMimeType("video/"); + + expect(result).toBe(true); + }); + + it("should return true", () => { + const result = isVideoMimeType("video/ "); + + expect(result).toBe(true); + }); + }); + + describe("when file mime type has application/ prefix", () => { + it("should return true", () => { + const result = isVideoMimeType("application/x-mpegURL"); + + expect(result).toBe(true); + }); + + it("should return true", () => { + const result = isVideoMimeType("application/vnd.ms-asf"); + + expect(result).toBe(true); + }); + + it("should return true", () => { + const result = isVideoMimeType("application/ogg"); + + expect(result).toBe(true); + }); + }); + }); + + describe("when file has no video mime type", () => { + it("should return false", () => { + const result = isVideoMimeType("image/png"); + + expect(result).toBe(false); + }); + + it("should return false", () => { + const result = isVideoMimeType("application/"); + + expect(result).toBe(false); + }); + + it("should return false", () => { + const result = isVideoMimeType(" "); + + expect(result).toBe(false); + }); + + it("should return false", () => { + const result = isVideoMimeType(""); + + expect(result).toBe(false); + }); + }); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 7d76622a08..cdb9f76f7a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,6 +37,8 @@ "@feature-date-time-picker": ["src/components/feature-date-time-picker"], "@feature-editor": ["src/components/feature-editor"], "@feature-render-html": ["src/components/feature-render-html"], + + "@ui-alert": ["src/components/ui-alert"], "@ui-board": ["src/components/ui-board"], "@ui-confirmation-dialog": ["src/components/ui-confirmation-dialog"], "@ui-light-box": ["src/components/ui-light-box"], diff --git a/vue.config.js b/vue.config.js index 29c3e6378b..eae7418f73 100644 --- a/vue.config.js +++ b/vue.config.js @@ -42,6 +42,7 @@ module.exports = defineConfig({ ), "@feature-editor": getDir("src/components/feature-editor"), "@feature-render-html": getDir("src/components/feature-render-html"), + "@ui-alert": getDir("src/components/ui-alert"), "@ui-board": getDir("src/components/ui-board"), "@ui-confirmation-dialog": getDir( "src/components/ui-confirmation-dialog"