diff --git a/package.json b/package.json index 22a56b0..2127be3 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "axios": "^1.2.3", "bootstrap": "^5.2.3", "bootstrap-icons": "^1.10.3", - "geovisio": "2.5.0", + "geovisio": "2.5.1", "moment": "^2.29.4", "pako": "^2.1.0", "pinia": "^2.1.4", diff --git a/src/App.vue b/src/App.vue index 898c338..3d3c881 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,11 +3,9 @@ import { ref, computed } from 'vue' import Header from '@/components/Header.vue' import Footer from '@/components/Footer.vue' import { RouterView } from 'vue-router' -import { useI18n } from 'vue-i18n' import { hasASessionCookieDecoded } from '@/utils/auth' import authConfig from './composables/auth' const { authConf } = authConfig() -const { t } = useI18n() let focusMap = ref('focus-map') diff --git a/src/assets/components/index.scss b/src/assets/components/index.scss new file mode 100644 index 0000000..fd8793f --- /dev/null +++ b/src/assets/components/index.scss @@ -0,0 +1,14 @@ +@mixin switch-button-view() { + position: fixed; + right: 0; + top: toRem(22); + z-index: 3; + height: toRem(5); + display: flex; + align-items: center; + justify-content: center; + border-top-left-radius: toRem(0.5); + border-bottom-left-radius: toRem(0.5); + background-color: var(--white); + border: toRem(0.1) solid var(--grey-pale); +} \ No newline at end of file diff --git a/src/assets/font-size.scss b/src/assets/font-size.scss index f8ca0f7..f826f34 100644 --- a/src/assets/font-size.scss +++ b/src/assets/font-size.scss @@ -28,7 +28,7 @@ } @if $size == h4 { font-weight: normal; - font-size: toRem(1.6); + font-size: toRem(1.8); } @if $size == xxl-regular { font-size: toRem(3.2); diff --git a/src/assets/images/car.svg b/src/assets/images/car.svg new file mode 100644 index 0000000..6066a20 --- /dev/null +++ b/src/assets/images/car.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/cursor.svg b/src/assets/images/cursor.svg new file mode 100644 index 0000000..012e293 --- /dev/null +++ b/src/assets/images/cursor.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icon/cursor-arrow.svg b/src/assets/images/icon/cursor-arrow.svg new file mode 100644 index 0000000..0c0814f --- /dev/null +++ b/src/assets/images/icon/cursor-arrow.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/road.svg b/src/assets/images/road.svg new file mode 100644 index 0000000..8e3b3b9 --- /dev/null +++ b/src/assets/images/road.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/main.scss b/src/assets/main.scss index 96b931a..ddd4c80 100644 --- a/src/assets/main.scss +++ b/src/assets/main.scss @@ -34,10 +34,11 @@ h5 { --grey-pale: #cfd2cf; --grey-semi-dark: #808080; --grey-dark: #3e3e3e; - --blue: #2954e9; --blue-dark: #0a1f69; - --blue-semi: #d7dffc; + --blue: #2954e9; + --blue-semi-pale: #f2f5ff; --blue-pale: #f2f5ff; + --blue-very-pale: #f9faff; --blue-geovisio: #34495e; --beige: #f5f3ec; --yellow: #fec868; diff --git a/src/components/Button.vue b/src/components/Button.vue index e0e60bf..0e21637 100644 --- a/src/components/Button.vue +++ b/src/components/Button.vue @@ -82,6 +82,7 @@ defineProps({ .button--blue { color: var(--white); background-color: var(--blue); + height: toRem(4); &.disabled { opacity: 0.6; color: var(--white); @@ -140,6 +141,7 @@ defineProps({ .no-text-white .icon { color: var(--white); margin-right: 0; + font-size: toRem(1.4); } .no-text-blue-dark .icon { color: var(--blue-dark); diff --git a/src/components/EditText.vue b/src/components/EditText.vue index c867dc3..8295fd8 100644 --- a/src/components/EditText.vue +++ b/src/components/EditText.vue @@ -36,7 +36,7 @@ {{ text }}

@@ -23,7 +21,6 @@ @@ -36,11 +33,11 @@ h3 { .information-block { position: relative; border-left: toRem(1.4) solid var(--blue); - padding: toRem(2) toRem(2) toRem(1.5); + padding: toRem(1.8) toRem(1.8) toRem(1.3); background-color: var(--white); border-radius: toRem(1.5); display: flex; - align-items: flex-end; + align-items: center; justify-content: space-between; flex-wrap: wrap; } @@ -61,10 +58,6 @@ h3 { margin-right: toRem(0.5); height: toRem(2.2); } -.subtitle { - @include text(h2); - color: var(--blue-dark); -} .information-text { margin-top: toRem(1); @include text(m-r-regular); diff --git a/src/components/Input.vue b/src/components/Input.vue index ebb7afc..0fc8f61 100644 --- a/src/components/Input.vue +++ b/src/components/Input.vue @@ -3,16 +3,21 @@ :value="text" :required="true" :placeholder="placeholder" - class="input" - type="text" + :type="type" + :min="min" + :max="max" @input="emitValue" + class="input" /> @@ -54,6 +59,8 @@ function updateValue(value: boolean): void { align-items: center; height: toRem(2); width: toRem(2); + background-color: var(--white); + border-radius: toRem(0.5); } .input { -webkit-appearance: none; @@ -68,7 +75,7 @@ function updateValue(value: boolean): void { .icon { font-size: toRem(2); position: absolute; - color: var(--grey-semi-dark); + color: var(--blue); } .wrapper-checkbox { display: flex; @@ -77,5 +84,14 @@ function updateValue(value: boolean): void { .label { cursor: pointer; margin-left: toRem(0.5); + @include text(s-r-regular); +} +.checked { + .input-checkbox { + background-color: var(--blue); + } + .icon { + color: var(--white); + } } diff --git a/src/components/InputRadio.vue b/src/components/InputRadio.vue new file mode 100644 index 0000000..4c1ed74 --- /dev/null +++ b/src/components/InputRadio.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/components/InputSwitch.vue b/src/components/InputSwitch.vue new file mode 100644 index 0000000..940b7de --- /dev/null +++ b/src/components/InputSwitch.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/src/components/Link.vue b/src/components/Link.vue index 9777c57..e797959 100644 --- a/src/components/Link.vue +++ b/src/components/Link.vue @@ -124,6 +124,17 @@ function triggerButton() { margin-right: toRem(0.5); } } +.link--grey-dark { + color: var(--grey-dark); + text-decoration: underline; + font-weight: inherit; + font-size: toRem(1.4); + .icon { + color: var(--grey-dark); + font-size: toRem(1.4); + margin-right: toRem(0.5); + } +} .link--blue-dark { color: var(--blue-dark); .icon { @@ -169,7 +180,7 @@ function triggerButton() { } } .button--blue-bleu { - background-color: var(--blue-semi); + background-color: var(--blue-semi-pale); color: var(--blue); } .disabled { diff --git a/src/components/TabPanel.vue b/src/components/TabPanel.vue new file mode 100644 index 0000000..7aa30fd --- /dev/null +++ b/src/components/TabPanel.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/components/Toast.vue b/src/components/Toast.vue index df8b99a..738c061 100644 --- a/src/components/Toast.vue +++ b/src/components/Toast.vue @@ -39,6 +39,7 @@ defineProps({ min-width: toRem(10); padding-right: toRem(1); padding-left: toRem(1); + z-index: 1; } .button-close { position: absolute; diff --git a/src/components/Viewer.vue b/src/components/Viewer.vue index a85a60e..b49ca34 100644 --- a/src/components/Viewer.vue +++ b/src/components/Viewer.vue @@ -1,5 +1,5 @@ + + diff --git a/src/components/sequence/PanelPhotosManagement.vue b/src/components/sequence/PanelPhotosManagement.vue new file mode 100644 index 0000000..d3ee1af --- /dev/null +++ b/src/components/sequence/PanelPhotosManagement.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/src/components/sequence/PanelSortManagement.vue b/src/components/sequence/PanelSortManagement.vue new file mode 100644 index 0000000..b622eb5 --- /dev/null +++ b/src/components/sequence/PanelSortManagement.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/src/components/sequence/WidgetOrientation.vue b/src/components/sequence/WidgetOrientation.vue new file mode 100644 index 0000000..f39dd15 --- /dev/null +++ b/src/components/sequence/WidgetOrientation.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/src/locales/en.json b/src/locales/en.json index 5454d94..a054d34 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -56,8 +56,25 @@ "delete_sequence_tooltip": "Permanently delete this sequence", "hide_photo_tooltip": "Hide selected pictures", "delete_photo_tooltip": "Permanently delete selected pictures", - "confirm_pictures_dialog": "⚠️ Selected photos will be permanently deleted", - "confirm_sequence_dialog": "⚠️ This sequence will be permanently deleted", + "conf_pic_msg": "⚠️ Selected photos will be permanently deleted", + "conf_sequence_msg": "⚠️ This sequence will be permanently deleted", + "button_panel_photos": "Manage pictures", + "button_panel_orientation": "Set orientation", + "button_panel_sort": "Sort sequence", + "orientation_panel_title": "Adjusting the orientation of all photos in the sequence", + "orientation_panel_tooltip": "Drag the blue box in the desired direction\"", + "orientation_input_label": "or change the angle here", + "orientation_input_placeholder": "Value between -180 and 180", + "orientation_input_error_value": "Value must be between -180 and 180", + "orientation_panel_button": "Validate position", + "sort_panel_title": "Sequence sort setting", + "sort_panel_settings": "Sort sequence by:", + "sort_panel_settings_order": "Order :", + "sort_panel_settings_order_increase": "Ascending", + "sort_panel_settings_order_decrease": "Decreasing", + "sort_panel_check_gps": "GPS Date", + "sort_panel_check_file": "File date", + "sort_panel_check_name": "File name", "created": "Uploaded :", "taken": "Shot on :", "duration": "Duration :", diff --git a/src/locales/fr.json b/src/locales/fr.json index bb0becc..717ebff 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1,7 +1,6 @@ { "general": { - "title": "DESC Panoramax {instanceName}: photo-cartographier les territoires", - "description": "DESC L'instance Panoramax {instanceName} permet la publication de photo de terrain pour cartographier le territoire. Panoramax favorise la réutilisation des photos pour de nombreux cas d'usages.", + "title": "Instance Panoramax", "meta": { "title": "Instance Panoramax", "description": "Panoramax, l’alternative libre pour photo-cartographier les territoires" @@ -57,8 +56,27 @@ "delete_sequence_tooltip": "Supprime définitivement la séquence", "hide_photo_tooltip": "Masque les photos sur la carte", "delete_photo_tooltip": "Supprime définitivement les photos", - "confirm_pictures_dialog": "⚠️ Les photos sélectionnées vont être définitivement supprimées", - "confirm_sequence_dialog": "⚠️ La séquence va être définitivement supprimée", + "conf_pic_msg": "⚠️ Les photos sélectionnées vont être définitivement supprimées", + "conf_sequence_msg": "⚠️ La séquence va être définitivement supprimée", + "button_panel_photos": "Gérer les photos", + "button_panel_orientation": "Régler l'orientation", + "button_panel_sort": "Trier la séquence", + "orientation_panel_title": "Définir l'orientation de la caméra sur le véhicule", + "orientation_panel_tooltip": "Faites glisser la zone bleu dans la direction souhaitée", + "orientation_input_label": "ou modifiez l'angle ici", + "orientation_input_placeholder": "Valeur entre -180 et 180", + "orientation_input_error_value": "La valeur doit être entre -180 et 180", + "orientation_panel_button": "Valider la position", + "orientation_updated": "L'orientation a bien été modifiée", + "sort_updated": "La séquence a bien triée", + "sort_panel_title": "Réglage du tri de la séquence", + "sort_panel_settings": "Trier la séquence par :", + "sort_panel_settings_order": "Ordre :", + "sort_panel_settings_order_increase": "Croissant", + "sort_panel_settings_order_decrease": "Décroissant", + "sort_panel_check_gps": "Date du GPS", + "sort_panel_check_file": "Date de la caméra", + "sort_panel_check_name": "Nom du fichier", "created": "Versement :", "taken": "Prise de vue :", "duration": "Durée :", diff --git a/src/locales/hu.json b/src/locales/hu.json index 4e2f679..3dafb95 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -56,8 +56,25 @@ "delete_sequence_tooltip": "A sorozat végleges törlése", "hide_photo_tooltip": "A kiválasztott fényképek elrejtése", "delete_photo_tooltip": "A kiválasztottt fényképek végleges törlése", - "confirm_pictures_dialog": "⚠️ A kiválasztott fényképek véglegesen elvesznek", - "confirm_sequence_dialog": "⚠️ A kiválasztott sorozat véglegesen elvész", + "conf_pic_msg": "⚠️ A kiválasztott fényképek véglegesen elvesznek", + "conf_sequence_msg": "⚠️ A kiválasztott sorozat véglegesen elvész", + "button_panel_photos": "Fényképek kezelése", + "button_panel_orientation": "Tájolás beállítása", + "button_panel_sort": "Rendezési sorrend", + "orientation_panel_title": "Az összes kép tájolásának beállítása a sorozatban", + "orientation_panel_tooltip": "Húzza a kék dobozt a kívánt irányba", + "orientation_input_label": "vagy módosítsa a szöget itt", + "orientation_input_placeholder": "-180 és 180 közötti érték", + "orientation_input_error_value": "Az értéknek -180 és 180 között kell lennie", + "orientation_panel_button": "Pozíció ellenőrzése", + "sort_panel_title": "Szekvenciás rendezés beállítása", + "sort_panel_settings": "Sorrend rendezése:", + "sort_panel_settings_order": "Rendelés :", + "sort_panel_settings_order_increase": "Növekvő", + "sort_panel_settings_order_decrease": "Csökkenő", + "sort_panel_check_gps": "GPS dátum", + "sort_panel_check_file": "Fájl dátuma", + "sort_panel_check_name": "Fájlnév", "created": "Feltöltés ideje:", "taken": "Elkészítés ideje:", "duration": "Hossz:", diff --git a/src/tests/unit/components/InformationCard.spec.js b/src/tests/unit/components/InformationCard.spec.js index 2a83bec..0ebffa6 100644 --- a/src/tests/unit/components/InformationCard.spec.js +++ b/src/tests/unit/components/InformationCard.spec.js @@ -11,7 +11,6 @@ describe('Template', () => { } }) expect(wrapper.vm.text).toBe(null) - expect(wrapper.vm.title).toBe(null) expect(wrapper.vm.look).toBe('') }) test('should have all the props filled', () => { @@ -21,11 +20,9 @@ describe('Template', () => { }, props: { text: 'my text', - title: 'my title', look: 'my-look' } }) - expect(wrapper.html()).contains('my title') expect(wrapper.html()).contains('my text

') expect(wrapper.html()).contains('class="information-block my-look"') }) diff --git a/src/tests/unit/components/sequence/PanelOrientationManagement.spec.js b/src/tests/unit/components/sequence/PanelOrientationManagement.spec.js new file mode 100644 index 0000000..684ca3d --- /dev/null +++ b/src/tests/unit/components/sequence/PanelOrientationManagement.spec.js @@ -0,0 +1,74 @@ +import { it, describe, expect } from 'vitest' +import { shallowMount } from '@vue/test-utils' +import PanelOrientationManagement from '../../../../components/sequence/PanelOrientationManagement.vue' +import i18n from '../../config' + +describe('Template', () => { + describe('Props', () => { + it('should have default props', () => { + const wrapper = shallowMount(PanelOrientationManagement, { + global: { + plugins: [i18n], + mocks: { + $t: (msg) => msg + } + } + }) + + expect(wrapper.vm.roadDegrees).toBe(0) + expect(wrapper.vm.seqBruteDeg).toBe(0) + }) + describe('When the component have props filled', () => { + it('should render the component all the element', () => { + const wrapper = shallowMount(PanelOrientationManagement, { + global: { + plugins: [i18n], + mocks: { + $t: (msg) => msg + } + }, + props: { + roadDegrees: 45, + seqBruteDeg: 22 + } + }) + expect(wrapper.html()).contains( + 'pages.sequence.orientation_panel_title' + ) + expect(wrapper.html()).contains( + 'pages.sequence.orientation_input_label' + ) + expect(wrapper.html()).contains( + 'pages.sequence.orientation_input_placeholder' + ) + expect(wrapper.html()).contains(' { + it('should emit triggerAngle event', async () => { + const wrapper = shallowMount(PanelOrientationManagement, { + global: { + plugins: [i18n], + mocks: { + $t: (msg) => msg + } + }, + props: { + roadDegrees: 45, + seqBruteDeg: 22 + } + }) + const valueToEmit = 22 - 45 + await wrapper.vm.triggerAngle(valueToEmit) + expect(wrapper.emitted('triggerAngle')).toHaveLength(1) + expect(wrapper.emitted('triggerAngle')[0]).toEqual([valueToEmit]) + }) + }) + }) +}) diff --git a/src/tests/unit/components/sequence/PanelPhotosManagement.spec.js b/src/tests/unit/components/sequence/PanelPhotosManagement.spec.js new file mode 100644 index 0000000..54fb5e4 --- /dev/null +++ b/src/tests/unit/components/sequence/PanelPhotosManagement.spec.js @@ -0,0 +1,347 @@ +import { it, describe, expect } from 'vitest' +import { shallowMount } from '@vue/test-utils' +import PanelPhotosManagement from '../../../../components/sequence/PanelPhotosManagement.vue' +import i18n from '../../config' +import { createPinia } from 'pinia' +const pinia = createPinia() + +describe('Template', () => { + describe('Props', () => { + it('should have default props', () => { + const wrapper = shallowMount(PanelPhotosManagement, { + global: { + plugins: [i18n, pinia], + mocks: { + $t: (msg) => msg + } + } + }) + + expect(wrapper.vm.isSequenceOwner).toBe(false) + expect(wrapper.vm.imagesSelectedHaveDifferentStatus).toBe(false) + expect(wrapper.vm.itemSelected).toBe('') + expect(wrapper.vm.menuHeight).toBe('0') + expect(wrapper.vm.fullImagesToDelete).toStrictEqual([]) + expect(wrapper.vm.selfLink).toStrictEqual([]) + expect(wrapper.vm.paginationLinks).toStrictEqual([]) + expect(wrapper.vm.pictures).toStrictEqual([]) + expect(wrapper.vm.picturesToDelete).toStrictEqual([]) + expect(wrapper.vm.sequence).toStrictEqual({}) + }) + describe('When the component have props filled', () => { + it('should render the component with images selected', () => { + const pictures = [ + { + assets: { thumb: { href: 'thumbHref' }, hd: { href: 'hdHref' } }, + properties: { datetime: new Date(), 'geovisio:status': 'ready' }, + id: 'pictureId', + bbox: [1, 2, 3, 4] + } + ] + const sequence = [ + { + id: 'seqId', + title: 'title', + description: 'descr', + license: 'license', + created: new Date(), + taken: 'taken date', + location: 'location', + imageCount: 4, + duration: 'duration', + camera: 'camera', + cameraModel: 'camera model', + status: 'ready', + providers: [{ name: 'provider', roles: ['role1'] }], + extent: { + temporal: { interval: ['date'] }, + spatial: { bbox: ['1', '2'] } + } + } + ] + const paginationLinks = [ + { + href: 'href', + rel: 'rel', + title: 'title', + type: 'type' + } + ] + const selfLink = [ + { + href: 'href', + rel: 'self', + title: 'title', + type: 'type' + } + ] + const fullImagesToDelete = [ + { + assets: { thumb: { href: 'thumbHref' }, hd: { href: 'hdHref' } }, + properties: { datetime: new Date(), 'geovisio:status': 'ready' }, + id: 'pictureId', + bbox: [1, 2, 3, 4] + } + ] + const wrapper = shallowMount(PanelPhotosManagement, { + global: { + plugins: [i18n, pinia], + mocks: { + $t: (msg) => msg + } + }, + props: { + isSequenceOwner: true, + imagesSelectedHaveDifferentStatus: true, + itemSelected: 'pictureId', + menuHeight: '10px', + pictures, + picturesToDelete: ['1', '2'], + sequence, + paginationLinks, + selfLink, + fullImagesToDelete + } + }) + expect(wrapper.html()).contains(' { + const pictures = [ + { + assets: { thumb: { href: 'thumbHref' }, hd: { href: 'hdHref' } }, + properties: { datetime: new Date(), 'geovisio:status': 'ready' }, + id: 'pictureId', + bbox: [1, 2, 3, 4] + } + ] + const sequence = [ + { + id: 'seqId', + title: 'title', + description: 'descr', + license: 'license', + created: new Date(), + taken: 'taken date', + location: 'location', + imageCount: 4, + duration: 'duration', + camera: 'camera', + cameraModel: 'camera model', + status: 'ready', + providers: [{ name: 'provider', roles: ['role1'] }], + extent: { + temporal: { interval: ['date'] }, + spatial: { bbox: ['1', '2'] } + } + } + ] + const wrapper = shallowMount(PanelPhotosManagement, { + global: { + plugins: [i18n, pinia], + mocks: { + $t: (msg) => msg + } + }, + props: { + isSequenceOwner: true, + pictures, + picturesToDelete: [1], + sequence + } + }) + expect(wrapper.html()).contains( + 'ischecked="true" isindeterminate="false"> { + const pictures = [ + { + assets: { thumb: { href: 'thumbHref' }, hd: { href: 'hdHref' } }, + properties: { datetime: new Date(), 'geovisio:status': 'hidden' }, + id: 'pictureId', + bbox: [1, 2, 3, 4] + } + ] + const sequence = [ + { + id: 'seqId', + title: 'title', + description: 'descr', + license: 'license', + created: new Date(), + taken: 'taken date', + location: 'location', + imageCount: 4, + duration: 'duration', + camera: 'camera', + cameraModel: 'camera model', + status: 'ready', + providers: [{ name: 'provider', roles: ['role1'] }], + extent: { + temporal: { interval: ['date'] }, + spatial: { bbox: ['1', '2'] } + } + } + ] + const wrapper = shallowMount(PanelPhotosManagement, { + global: { + plugins: [i18n, pinia], + mocks: { + $t: (msg) => msg + } + }, + props: { + isSequenceOwner: true, + pictures, + picturesToDelete: [], + sequence + } + }) + expect(wrapper.html()).contains('status="hidden"> { + it('should emit triggerInputCheck event', async () => { + const wrapper = shallowMount(PanelPhotosManagement, { + global: { + plugins: [i18n, pinia], + mocks: { + $t: (msg) => msg + } + } + }) + await wrapper.vm.triggerInputCheck({ + isChecked: true, + isIndeterminate: false + }) + expect(wrapper.emitted('triggerInputCheck')).toHaveLength(1) + expect(wrapper.emitted('triggerInputCheck')[0]).toEqual([ + { isChecked: true, isIndeterminate: false } + ]) + }) + it('should emit triggerGoToNextPage event', async () => { + const wrapper = shallowMount(PanelPhotosManagement, { + global: { + plugins: [i18n, pinia], + mocks: { + $t: (msg) => msg + } + } + }) + await wrapper.vm.triggerGoToNextPage('nextPage') + expect(wrapper.emitted('triggerGoToNextPage')).toHaveLength(1) + expect(wrapper.emitted('triggerGoToNextPage')[0]).toEqual(['nextPage']) + }) + + it('should emit triggerSelectImageAndMove event', async () => { + const wrapper = shallowMount(PanelPhotosManagement, { + global: { + plugins: [i18n, pinia], + mocks: { + $t: (msg) => msg + } + } + }) + const picture = { + assets: { thumb: { href: 'thumbHref' }, hd: { href: 'hdHref' } }, + properties: { datetime: new Date(), 'geovisio:status': 'ready' }, + id: 'pictureId', + bbox: [1, 2, 3, 4] + } + await wrapper.vm.triggerSelectImageAndMove(picture) + expect(wrapper.emitted('triggerSelectImageAndMove')).toHaveLength(1) + expect(wrapper.emitted('triggerSelectImageAndMove')[0]).toEqual([picture]) + }) + + it('should emit triggerPatchOrDeleteCollectionItems event', async () => { + const wrapper = shallowMount(PanelPhotosManagement, { + global: { + plugins: [i18n, pinia], + mocks: { + $t: (msg) => msg + } + } + }) + await wrapper.vm.triggerPatchOrDeleteCollectionItems('itemToDelete') + expect( + wrapper.emitted('triggerPatchOrDeleteCollectionItems') + ).toHaveLength(1) + expect(wrapper.emitted('triggerPatchOrDeleteCollectionItems')[0]).toEqual( + ['itemToDelete'] + ) + }) + }) +}) diff --git a/src/tests/unit/components/sequence/PanelSortManagement.spec.js b/src/tests/unit/components/sequence/PanelSortManagement.spec.js new file mode 100644 index 0000000..cd00456 --- /dev/null +++ b/src/tests/unit/components/sequence/PanelSortManagement.spec.js @@ -0,0 +1,83 @@ +import { it, describe, expect } from 'vitest' +import { shallowMount } from '@vue/test-utils' +import PanelSortManagement from '../../../../components/sequence/PanelSortManagement.vue' +import i18n from '../../config' + +describe('Template', () => { + describe('Props', () => { + it('should have default props', () => { + const wrapper = shallowMount(PanelSortManagement, { + global: { + plugins: [i18n], + mocks: { + $t: (msg) => msg + } + } + }) + + expect(wrapper.vm.sequenceSorted).toBe(null) + }) + describe('When the component have props filled', () => { + it('should render the component all the element', () => { + const wrapper = shallowMount(PanelSortManagement, { + global: { + plugins: [i18n], + mocks: { + $t: (msg) => msg + } + } + }) + expect(wrapper.html()).contains('pages.sequence.sort_panel_title') + expect(wrapper.html()).contains('pages.sequence.sort_panel_settings') + expect(wrapper.html()).contains( + 'name="sort" id="gpsdate" label="pages.sequence.sort_panel_check_gps" value=""' + ) + expect(wrapper.html()).contains( + 'name="sort" id="filedate" label="pages.sequence.sort_panel_check_file" value=""' + ) + expect(wrapper.html()).contains( + 'name="sort" id="filename" label="pages.sequence.sort_panel_check_name" value=""' + ) + expect(wrapper.html()).contains( + 'pages.sequence.sort_panel_settings_order' + ) + expect(wrapper.html()).contains('increased="true"> { + const wrapper = shallowMount(PanelSortManagement, { + global: { + plugins: [i18n], + mocks: { + $t: (msg) => msg + } + }, + props: { + sequenceSorted: '-gpsdate' + } + }) + expect(wrapper.html()).contains( + 'name="sort" id="gpsdate" label="pages.sequence.sort_panel_check_gps" value="gpsdate"' + ) + expect(wrapper.html()).contains('increased="false"> { + it('should emit triggerSort event', async () => { + const wrapper = shallowMount(PanelSortManagement, { + global: { + plugins: [i18n], + mocks: { + $t: (msg) => msg + } + }, + props: { + sequenceSorted: '-gpsdate' + } + }) + await wrapper.vm.triggerSort('-gpsdate') + expect(wrapper.emitted('triggerSort')).toHaveLength(1) + expect(wrapper.emitted('triggerSort')[0]).toEqual(['-gpsdate']) + }) + }) +}) diff --git a/src/tests/unit/components/sequence/WidgetOrientation.spec.js b/src/tests/unit/components/sequence/WidgetOrientation.spec.js new file mode 100644 index 0000000..dc88e59 --- /dev/null +++ b/src/tests/unit/components/sequence/WidgetOrientation.spec.js @@ -0,0 +1,53 @@ +import { it, describe, expect } from 'vitest' +import { shallowMount } from '@vue/test-utils' +import WidgetOrientation from '../../../../components/sequence/WidgetOrientation.vue' +import i18n from '../../config' + +describe('Template', () => { + describe('Props', () => { + it('should have default props', () => { + const wrapper = shallowMount(WidgetOrientation, { + global: { + plugins: [i18n], + mocks: { + $t: (msg) => msg + } + } + }) + + expect(wrapper.vm.roadDegrees).toBe(0) + expect(wrapper.vm.seqBruteDeg).toBe(0) + }) + describe('When the component have props filled', () => { + it('should render the component all the element', () => { + const wrapper = shallowMount(WidgetOrientation, { + global: { + plugins: [i18n], + mocks: { + $t: (msg) => msg + } + }, + props: { + roadDegrees: 45, + seqBruteDeg: 22 + } + }) + expect(wrapper.html()).contains( + 'class="wrapper-img-road" style="transform: rotate(45deg);"' + ) + expect(wrapper.html()).contains( + 'src="/assets/images/car.svg" alt="" style="transform: rotate(45deg);" class="car-img"' + ) + expect(wrapper.html()).contains( + 'style="transform: rotate(22deg);" id="rotateWrapper" class="rotate-wrapper"' + ) + expect(wrapper.html()).contains( + 'src="/assets/images/icon/cursor-arrow.svg"' + ) + expect(wrapper.html()).contains('class="arrow-img arrow-img-1"') + expect(wrapper.html()).contains('class="arrow-img arrow-img-2"') + }) + }) + }) + // TODO TEST -> All Events +}) diff --git a/src/utils/mapAndViewer.ts b/src/utils/mapAndViewer.ts new file mode 100644 index 0000000..db76efa --- /dev/null +++ b/src/utils/mapAndViewer.ts @@ -0,0 +1,29 @@ +import axios from 'axios' + +async function getIgnTiles(): Promise { + try { + const { data } = await axios.get( + 'https://wxs.ign.fr/essentiels/static/vectorTiles/styles/PLAN.IGN/attenue.json' + ) + data.sources.plan_ign.scheme = 'xyz' + data.sources.plan_ign.attribution = 'Données cartographiques : © IGN' + const objIndex = data.layers.findIndex( + (el: { id: string }) => el.id === 'toponyme - parcellaire - adresse' + ) + data.layers[objIndex].layout = { + ...data.layers[objIndex].layout, + 'text-field': [ + 'concat', + ['get', 'numero'], + ['get', 'indice_de_repetition'] + ] + } + // Patch tms scheme to xyz to make it compatible for Maplibre GL JS / Mapbox GL JS + // Patch num_repetition + return data + } catch (error) { + return 'https://tile-vect.openstreetmap.fr/styles/basic/style.json' + } +} + +export { getIgnTiles } diff --git a/src/views/MySequenceView.vue b/src/views/MySequenceView.vue index 6dc31d4..27bc896 100644 --- a/src/views/MySequenceView.vue +++ b/src/views/MySequenceView.vue @@ -20,16 +20,25 @@ icon="bi bi-arrow-left" :text="$t('pages.sequence.back_button')" :route="{ name: 'my-sequences' }" - look="link--grey" + look="link--grey-dark" /> +
+ +
+ +
+ @@ -209,11 +179,12 @@ import { useCookies } from 'vue3-cookies' import Button from '@/components/Button.vue' import Link from '@/components/Link.vue' import Toast from '@/components/Toast.vue' -import Pagination from '@/components/Pagination.vue' -import InputCheckbox from '@/components/InputCheckbox.vue' import Loader from '@/components/Loader.vue' -import ImageItem from '@/components/ImageItem.vue' import EditText from '@/components/EditText.vue' +import TabPanel from '@/components/TabPanel.vue' +import PanelPhotosManagement from '@/components/sequence/PanelPhotosManagement.vue' +import PanelOrientationManagement from '@/components/sequence/PanelOrientationManagement.vue' +import PanelSortManagement from '@/components/sequence/PanelSortManagement.vue' import Viewer from '@/components/Viewer.vue' import type ViewerType from '@/components/Viewer.vue' import { durationCalc, formatDate } from '@/utils/dates' @@ -228,9 +199,7 @@ import { } from '@/views/utils/sequence/request' import { - imageStatus, scrollIntoSelected, - photoToDeleteOrPatchSelected, spliceIntoChunks, formatPaginationItems } from '@/views/utils/sequence/index' @@ -258,10 +227,14 @@ let isShiftPressed = ref(false) let itemSelected = ref('') let isLoading = ref(false) let isLoadingTitle = ref(false) +let isLoadingOrient = ref(false) +let seqDegrees = ref(0) +let seqBruteDeg = ref(0) +let panelView = ref('photos') const collapseMenu = ref() const deleteAll = ref() -const menuHeight = ref() -const viewerRef = ref>() +const menuHeight = ref('0') +const viewerRef = ref | null>(null) onMounted(async () => { try { @@ -276,40 +249,41 @@ onMounted(async () => { fetchAllCollectionInfo[1].data.links ) formatSequenceFetched(fetchAllCollectionInfo[0].data) - const collectionItems = fetchAllCollectionInfo[1].data.features - const collectionItemsReady = collectionItems.filter( + const collItems = fetchAllCollectionInfo[1].data.features + const collItemsReady = collItems.filter( (el) => el.properties['geovisio:status'] === 'ready' ) - pictures.value = collectionItems + pictures.value = collItems setHeightValue() - if ( - itemSelected.value.length || - !getCurrentPicId(collectionItemsReady[0].id) - ) { + if (itemSelected.value.length || !getCurrentPicId(collItemsReady[0].id)) { return } if (!viewerRef.value) return - viewerRef.value.viewer._api.onceReady().then(() => { + viewerRef.value.viewer.addEventListener('picture-loaded', (): void => { if (!viewerRef.value) return + const seqRelativeDeg = viewerRef.value.viewer.getPictureRelativeHeading() + seqBruteDeg.value = + viewerRef.value.viewer.getPictureMetadata().properties['view:azimuth'] + seqDegrees.value = seqBruteDeg.value - seqRelativeDeg + }) + if (!viewerRef.value) return + viewerRef.value.viewer._api.onceReady().then(() => { + if (!sequence.value || !viewerRef.value) return viewerRef.value.viewer.goToPicture( - getCurrentPicId(collectionItemsReady[0].id), - sequence.value?.id + getCurrentPicId(collItemsReady[0].id), + sequence.value.id ) }) - itemSelected.value = getCurrentPicId(collectionItemsReady[0].id) - if (!pictureExistInList(getCurrentPicId(collectionItemsReady[0].id))) { - await goToTheGoodPage(getCurrentPicId(collectionItemsReady[0].id)) - } - scrollIntoSelected( - itemSelected.value, - pictures.value.map((e) => e.id) - ) + await goToThePageAndScroll() } catch (err) { console.log(err) } }) -watchEffect(async () => { +watchEffect(() => { + goToThePageAndScroll() +}) +async function goToThePageAndScroll() { if (!viewerRef || !viewerRef.value || !viewerRef.value.viewer) return viewerRef.value.viewer.addEventListener( 'picture-loaded', @@ -324,7 +298,12 @@ watchEffect(async () => { ) } ) -}) +} +function setPanelView(value: string): void { + panelView.value = value + if (value === 'orientation') window.location.hash = '#background=aerial' + else window.location.hash = '#background=streets' +} async function setNewSequenceTitle(value: string | null): Promise { isLoadingTitle.value = true if (value && value.length > 0) { @@ -357,16 +336,12 @@ const sequenceStatus = computed((): string => { return t('pages.sequence.sequence_hidden') return t('pages.sequence.sequence_waiting') }) - -const picturesToDeleteStatus = computed((): string => { - if (fullImagesToDelete().length) { - return fullImagesToDelete()[0].properties['geovisio:status'] - } - return 'hidden' +const fullImagesToDelete = computed((): ResponseUserPhotoInterface[] => { + return pictures.value.filter((el) => picturesToDelete.value.includes(el.id)) }) const imagesSelectedHaveDifferentStatus = computed((): boolean => { function filterByStatus(status: string): ResponseUserPhotoInterface[] { - return fullImagesToDelete().filter((el) => { + return fullImagesToDelete.value.filter((el) => { return el.properties['geovisio:status'] === status }) } @@ -374,20 +349,6 @@ const imagesSelectedHaveDifferentStatus = computed((): boolean => { filterByStatus('hidden').length > 0 && filterByStatus('ready').length > 0 ) }) - -const isIndeterminate = computed( - (): boolean => - !!picturesToDelete.value.length && - !!sequence.value && - pictures.value.length !== picturesToDelete.value.length -) - -const selectedText = computed((): string => - picturesToDelete.value.length === pictures.value.length - ? t('pages.sequence.unselect_text') - : t('pages.sequence.select_text') -) - async function goToTheGoodPage(id: string): Promise { const { data } = await fetchCollectionItems( route.params.id, @@ -440,7 +401,7 @@ function hiddeAllPictures(): void { } async function deleteCollection(): Promise { - if (confirm(t('pages.sequence.confirm_sequence_dialog'))) { + if (confirm(t('pages.sequence.conf_sequence_msg'))) { isLoading.value = true await deleteACollection(route.params.id) isLoading.value = false @@ -465,11 +426,6 @@ async function patchCollection(): Promise { if (viewerRef.value) viewerRef.value.viewer.reloadVectorTiles() isLoading.value = false } - -function fullImagesToDelete(): ResponseUserPhotoInterface[] { - return pictures.value.filter((el) => picturesToDelete.value.includes(el.id)) -} - async function goToNextPage(value: string): Promise { isLoading.value = true const { data } = await fetchCollectionItemsWithFullUrl(value) @@ -484,15 +440,31 @@ async function goToNextPage(value: string): Promise { isLoading.value = false setHeightValue() } - function triggerCheck(value: CheckboxInterface): void { - value.isChecked - ? (picturesToDelete.value = pictures.value - .filter( - (el) => el.properties['geovisio:status'] !== 'waiting-for-process' - ) - .map((el) => el.id)) - : (picturesToDelete.value = []) + if (value.isChecked) { + picturesToDelete.value = pictures.value + .filter((e) => e.properties['geovisio:status'] !== 'waiting-for-process') + .map((e) => e.id) + } else picturesToDelete.value = [] +} +async function orientSequence(value: number): Promise { + isLoadingOrient.value = true + await patchACollection(route.params.id, { + relative_heading: value + }) + const fetchCollectionInfo = await fetchCollection(route.params.id) + formatSequenceFetched(fetchCollectionInfo.data) + if (viewerRef.value) viewerRef.value.viewer.clearPictureMetadataCache() + sequenceStore.addToastText(t('pages.sequence.orientation_updated'), 'success') + isLoadingOrient.value = false +} +async function sortSequence(value: string): Promise { + await patchACollection(route.params.id, { + sortby: value + }) + const fetchCollectionInfo = await fetchCollection(route.params.id) + formatSequenceFetched(fetchCollectionInfo.data) + sequenceStore.addToastText(t('pages.sequence.sort_updated'), 'success') } function selectPhotoToDeleteOrPatch( item: ResponseUserPhotoInterface @@ -550,59 +522,36 @@ async function selectImageAndMove( ) } } - -async function patchOrDeleteCollectionItems( - requestType: string -): Promise { - if ( - requestType === 'DELETE' && - !confirm(t('pages.sequence.confirm_pictures_dialog')) - ) - return +function formatValuesToPatchOrDelete(ele: string): string { + if (imagesSelectedHaveDifferentStatus.value) return 'true' + const imageToDelete = pictures.value.find((elem) => elem.id === ele) + return imageToDelete?.properties['geovisio:status'] === 'ready' + ? 'false' + : 'true' +} +async function patchOrDeleteCollectionItems(reqType: string): Promise { + if (reqType === 'DELETE' && !confirm(t('pages.sequence.conf_pic_msg'))) return isLoading.value = true toastText.value = '' const chunksItems = spliceIntoChunks(picturesToDelete.value, 4) try { let items: unknown[] = [] - if (imagesSelectedHaveDifferentStatus.value) { - for (let el of chunksItems) { - items = [ - ...items, - ...(await Promise.all( - el.map(async (ele) => { - if (requestType === 'PATCH') { - return await patchACollectionItem('true', route.params.id, ele) - } - return await deleteACollectionItem(route.params.id, ele) - }) - )) - ] - } - } else { - for (let el of chunksItems) { - items = [ - ...items, - ...(await Promise.all( - el.map(async (ele) => { - if (requestType === 'PATCH') { - const imageToDelete = pictures.value.find( - (elem) => elem.id === ele - ) - const isVisible = - imageToDelete?.properties['geovisio:status'] === 'ready' - ? 'false' - : 'true' - return await patchACollectionItem( - isVisible, - route.params.id, - ele - ) - } - return await deleteACollectionItem(route.params.id, ele) - }) - )) - ] - } + for (let el of chunksItems) { + items = [ + ...items, + ...(await Promise.all( + el.map(async (ele) => { + if (reqType === 'PATCH') { + return await patchACollectionItem( + formatValuesToPatchOrDelete(ele), + route.params.id, + ele + ) + } + return await deleteACollectionItem(route.params.id, ele) + }) + )) + ] } const { data } = await fetchCollectionItems(route.params.id, '?limit=100') pictures.value = data.features @@ -633,27 +582,36 @@ async function patchOrDeleteCollectionItems( display: flex; } .entry-viewer { - width: 50vw; + width: 55vw; position: relative; height: calc(100vh - #{toRem(8)}); } .menu-right { - width: 50vw; + width: 45vw; height: calc(100vh - #{toRem(8)}); - overflow: auto; + overflow-y: auto; +} +.back-button, +.entry-tab-panel { + margin-left: toRem(2); + margin-right: toRem(2); + margin-top: toRem(2); } .back-button { width: fit-content; - margin-left: toRem(2); - margin-top: toRem(2); + margin-bottom: toRem(2); } .wrapper-loader { display: flex; justify-content: center; align-items: center; } +.entry-edit-text { + display: flex; + width: 100%; +} .wrapper-title { - min-width: 60%; + width: 100%; } .wrapper-button { display: flex; @@ -701,13 +659,27 @@ async function patchOrDeleteCollectionItems( .button-close { display: none; } +.entry-panel, +.menu-top { + padding: toRem(2) toRem(2) toRem(3); + border-bottom-right-radius: toRem(0.5); + border-bottom-left-radius: toRem(0.5); + border-top-right-radius: toRem(0.5); + background-color: var(--blue-semi-pale); + margin-right: toRem(2); + margin-left: toRem(2); +} +.wrapper-panel-photo { + margin: toRem(1); +} +.entry-panel-photo { + height: calc(100vh - v-bind(menuHeight)); +} .menu-top { position: relative; - margin: toRem(2) toRem(2) 0; - padding: toRem(2.5) toRem(2) toRem(1); - border: toRem(0.1) solid var(--grey); - border-radius: toRem(0.5); - background-color: var(--blue-semi); + margin-bottom: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } .header-menu { display: flex; @@ -715,14 +687,14 @@ async function patchOrDeleteCollectionItems( margin-bottom: toRem(1); } .sequence-status { - position: absolute; - top: 0; - left: 0; - z-index: 1; - @include text(xss-regular); + @include text(xs-r-regular); + height: 100%; border-radius: toRem(0.4); padding: toRem(0.3) toRem(0.8); color: var(--white); + margin-right: toRem(1); + display: flex; + align-items: center; &.ready { background-color: var(--green); border: toRem(0.1) solid var(--green); @@ -749,67 +721,13 @@ async function patchOrDeleteCollectionItems( color: var(--grey-dark); font-size: toRem(3); } - -.photos-wrapper { - padding: toRem(1) 0 toRem(2) toRem(1); - height: calc(100vh - v-bind(menuHeight)); -} -.delete-all { - display: flex; - justify-content: space-between; - align-items: center; - margin-left: toRem(1); - margin-right: toRem(2); - margin-bottom: toRem(1); - @include text(xs-r-regular); -} -.wrapper-select { - display: flex; - align-items: center; -} -.wrapper-photo-selected { - @include text(xs-regular); -} -.photo-selected-separator { - margin-right: toRem(0.5); - margin-left: toRem(0.5); -} -.button-hidde { - margin-right: toRem(1); - margin-left: toRem(1); -} -.action-buttons { - display: flex; - align-items: center; -} -.photo-list { - display: flex; - flex-wrap: wrap; - overflow-y: auto; - height: 100%; - width: 100%; - padding: 0; -} -.photo-item { - width: calc(33% - #{toRem(2)}); - height: fit-content; - margin: toRem(1); - border-radius: toRem(0.5); - background-color: var(--grey); -} -.no-photo { - @include text(s-regular); - text-align: center; - margin-top: toRem(10); - color: var(--grey-dark); -} -.entry-pagination { - margin-top: toRem(2); - width: 100%; - display: flex; - justify-content: center; -} @media (max-width: toRem(102.4)) { + .entry-viewer { + width: 45vw; + } + .menu-right { + width: 55vw; + } .wrapper-title { width: 90%; } @@ -827,9 +745,6 @@ async function patchOrDeleteCollectionItems( margin-top: 0; } } - .photo-item { - width: calc(50% - #{toRem(2)}); - } } @media (max-width: toRem(76.8)) { .entry-page { @@ -841,6 +756,11 @@ async function patchOrDeleteCollectionItems( .menu-right { height: calc(100vh - #{toRem(12)}); } + .back-button { + .link--grey { + @include text(xs-r-regular); + } + } .desktop { display: none; } @@ -865,10 +785,6 @@ async function patchOrDeleteCollectionItems( top: toRem(-1); right: 0; } - .photo-item { - width: 100%; - margin-right: 0; - } .wrapper-info-top { width: 100%; margin-right: 0; @@ -876,26 +792,6 @@ async function patchOrDeleteCollectionItems( .wrapper-button { margin-top: toRem(1); } - .photo-list { - padding-right: toRem(2); - } - .delete-all { - text-align: left; - } - .wrapper-select { - flex-direction: column; - align-items: initial; - } - .wrapper-photo-selected:nth-child(2) { - margin-top: toRem(0.5); - } - .photo-selected-separator { - display: none; - } - .entry-pagination { - margin-top: toRem(0.5); - margin-bottom: toRem(1); - } } @media (max-width: toRem(50)) { @@ -915,26 +811,16 @@ async function patchOrDeleteCollectionItems( top: 0; right: 0; z-index: 0; - width: 80vw; + width: 85vw; background-color: var(--white); + border-left: toRem(0.1) solid var(--grey-pale); } .menu-top { width: 0vw; padding: toRem(2.5) toRem(1); } .button-close { - position: absolute; - right: 0; - top: toRem(22); - z-index: 3; - height: toRem(5); - display: flex; - align-items: center; - justify-content: center; - border-top-left-radius: toRem(0.5); - border-bottom-left-radius: toRem(0.5); - background-color: var(--white); - border: toRem(0.1) solid var(--black); + @include switch-button-view(); } .menu-is-open { .menu-right { @@ -944,12 +830,20 @@ async function patchOrDeleteCollectionItems( width: auto; } .button-close { - left: calc(20vw - #{toRem(3)}); + left: calc(15vw - #{toRem(3)}); right: initial; } } - .entry-pagination { - margin-top: toRem(1); + .entry-edit-text { + flex-direction: column; + } + .sequence-status { + width: fit-content; + } + .entry-panel { + padding-right: toRem(1); + padding-left: toRem(1); + padding-top: toRem(0); } } diff --git a/src/views/MySequencesView.vue b/src/views/MySequencesView.vue index a61b2bc..80475b8 100644 --- a/src/views/MySequencesView.vue +++ b/src/views/MySequencesView.vue @@ -1,5 +1,12 @@