From 79a92fbefc6f1494aeb24f9dc56752fe15bdfe59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20P=C3=B6schl?= Date: Fri, 30 Aug 2024 11:43:56 +0200 Subject: [PATCH] :sparkles: Allow UI to download a file (empty) --- .../controller/ConfigRestController.kt | 22 ++++++++++ .../src/components/MapEditorExportBox.vue | 42 +++++++++++++++++++ frontend/src/services/ConfigService.ts | 5 +++ frontend/src/stores/ConfigStore.ts | 12 ++++++ frontend/src/views/MapEditorView.vue | 5 ++- 5 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/MapEditorExportBox.vue diff --git a/backend/src/main/kotlin/xyz/poeschl/roborush/controller/ConfigRestController.kt b/backend/src/main/kotlin/xyz/poeschl/roborush/controller/ConfigRestController.kt index 5308f4f..9b79d05 100644 --- a/backend/src/main/kotlin/xyz/poeschl/roborush/controller/ConfigRestController.kt +++ b/backend/src/main/kotlin/xyz/poeschl/roborush/controller/ConfigRestController.kt @@ -3,7 +3,10 @@ package xyz.poeschl.roborush.controller import io.swagger.v3.oas.annotations.security.SecurityRequirement import org.apache.commons.lang3.EnumUtils import org.slf4j.LoggerFactory +import org.springframework.core.io.ByteArrayResource +import org.springframework.core.io.Resource import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile @@ -130,6 +133,25 @@ class ConfigRestController(private val configService: ConfigService, private val } } + @SecurityRequirement(name = "Bearer Authentication") + @PreAuthorize("hasRole('${User.ROLE_ADMIN}')") + @GetMapping("/map/{id}/export", produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE]) + fun exportMap(@PathVariable id: Long): ResponseEntity { + val map = mapService.getMap(id) + + if (map != null) { + // TODO: Create image data + val resource = ByteArrayResource(ByteArray(4, { i -> i.toByte() })) + + return ResponseEntity.ok() + .contentLength(resource.contentLength()) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(resource) + } else { + throw MapNotFound("No matching map found for deletion") + } + } + @SecurityRequirement(name = "Bearer Authentication") @PreAuthorize("hasRole('${User.ROLE_ADMIN}')") @PostMapping("/client/globalNotificationText", consumes = [MediaType.APPLICATION_JSON_VALUE]) diff --git a/frontend/src/components/MapEditorExportBox.vue b/frontend/src/components/MapEditorExportBox.vue new file mode 100644 index 0000000..6fcd1ce --- /dev/null +++ b/frontend/src/components/MapEditorExportBox.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/frontend/src/services/ConfigService.ts b/frontend/src/services/ConfigService.ts index bb939f7..5ae3d25 100644 --- a/frontend/src/services/ConfigService.ts +++ b/frontend/src/services/ConfigService.ts @@ -45,6 +45,10 @@ export default function useConfigService() { return axiosWithAuth.delete(`${baseConfigUrl}/map/${mapId}`); }; + const exportMap = (mapId: number): Promise => { + return axiosWithAuth.get(`${baseConfigUrl}/map/${mapId}/export`, { responseType: "blob" }).then((response) => response.data); + }; + const setGlobalNotificationText = (notificationText: string): Promise => { return axiosWithAuth.post(`${baseConfigUrl}/client/globalNotificationText`, { text: notificationText }); }; @@ -64,5 +68,6 @@ export default function useConfigService() { setMapTile, setGlobalNotificationText, getClientSettings: getGlobalNotificationText, + exportMap, }; } diff --git a/frontend/src/stores/ConfigStore.ts b/frontend/src/stores/ConfigStore.ts index 840aafc..f2fae69 100644 --- a/frontend/src/stores/ConfigStore.ts +++ b/frontend/src/stores/ConfigStore.ts @@ -84,6 +84,17 @@ export const useConfigStore = defineStore("configStore", () => { }); }; + const exportMap = (map: PlaygroundMap): Promise => { + return configService.exportMap(map.id).then((response) => { + const blob = new Blob([response], { type: "image/png" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = `${map.mapName}.png`; + setTimeout(() => URL.revokeObjectURL(link.href), 5000); + return link; + }); + }; + return { currentConfig, availableMaps, @@ -95,6 +106,7 @@ export const useConfigStore = defineStore("configStore", () => { removeMap, setMapAttributes, setMapTile, + exportMap, clientSettings, updateClientConfig, initWebsocket, diff --git a/frontend/src/views/MapEditorView.vue b/frontend/src/views/MapEditorView.vue index 054a0e2..c7a4356 100644 --- a/frontend/src/views/MapEditorView.vue +++ b/frontend/src/views/MapEditorView.vue @@ -56,7 +56,9 @@ @update:pathDrawEnabled="(value) => (pathDrawEnabled = value)" /> -
EXPORT
+
+ +
@@ -71,6 +73,7 @@ import { useConfigStore } from "@/stores/ConfigStore"; import MapEditorAttributesBox from "@/components/MapEditorAttributesBox.vue"; import MapEditorMeasureBox from "@/components/MapEditorMeasureBox.vue"; import MapEditorLocationChangeBox from "@/components/MapEditorLocationChangeBox.vue"; +import MapEditorExportBox from "@/components/MapEditorExportBox.vue"; const route = useRoute(); const router = useRouter();