Skip to content

Commit

Permalink
✨ Allow UI to download a file (empty)
Browse files Browse the repository at this point in the history
  • Loading branch information
Poeschl committed Aug 31, 2024
1 parent 3b435e1 commit 79a92fb
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Resource> {
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])
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/components/MapEditorExportBox.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<template>
<InfoBoxTemplate title="Export Map">
<div class="control">
<button class="button is-primary is-fullwidth" :class="{ 'is-loading': exporting }" @click.prevent="exportMap">
<div class="icon mr-1">
<FontAwesomeIcon icon="fa-solid fa-file-export" />
</div>
Export
</button>
</div>
</InfoBoxTemplate>
</template>

<script setup lang="ts">
import type { PlaygroundMap, PlaygroundMapAttributes } from "@/models/Map";
import { computed, onMounted, type Ref, ref, watch } from "vue";
import { useConfigStore } from "@/stores/ConfigStore";
import InfoBoxTemplate from "@/components/templates/InfoBoxTemplate.vue";
import log from "loglevel";
const configStore = useConfigStore();
const exporting = ref<boolean>(false);
const props = defineProps<{
map: PlaygroundMap;
}>();
const exportMap = () => {
exporting.value = true;
configStore
.exportMap(props.map)
.then((link) => {
link.click();
})
.finally(() => {
exporting.value = false;
});
};
</script>

<style scoped lang="scss"></style>
5 changes: 5 additions & 0 deletions frontend/src/services/ConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export default function useConfigService() {
return axiosWithAuth.delete(`${baseConfigUrl}/map/${mapId}`);
};

const exportMap = (mapId: number): Promise<Blob> => {
return axiosWithAuth.get(`${baseConfigUrl}/map/${mapId}/export`, { responseType: "blob" }).then((response) => response.data);
};

const setGlobalNotificationText = (notificationText: string): Promise<void> => {
return axiosWithAuth.post(`${baseConfigUrl}/client/globalNotificationText`, { text: notificationText });
};
Expand All @@ -64,5 +68,6 @@ export default function useConfigService() {
setMapTile,
setGlobalNotificationText,
getClientSettings: getGlobalNotificationText,
exportMap,
};
}
12 changes: 12 additions & 0 deletions frontend/src/stores/ConfigStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ export const useConfigStore = defineStore("configStore", () => {
});
};

const exportMap = (map: PlaygroundMap): Promise<HTMLAnchorElement> => {
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,
Expand All @@ -95,6 +106,7 @@ export const useConfigStore = defineStore("configStore", () => {
removeMap,
setMapAttributes,
setMapTile,
exportMap,
clientSettings,
updateClientConfig,
initWebsocket,
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/views/MapEditorView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@
@update:pathDrawEnabled="(value) => (pathDrawEnabled = value)"
/>
</div>
<div class="box" v-if="activeTab == EditorTab.EXPORT">EXPORT</div>
<div v-if="map != undefined && activeTab == EditorTab.EXPORT">
<MapEditorExportBox :map="map" />
</div>
</div>
</div>
</template>
Expand All @@ -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();
Expand Down

0 comments on commit 79a92fb

Please sign in to comment.