Skip to content

Commit

Permalink
implmenetation of a coordinate picker
Browse files Browse the repository at this point in the history
  • Loading branch information
CommanderStorm committed Jun 25, 2023
1 parent 12c5daa commit 7d4834c
Show file tree
Hide file tree
Showing 11 changed files with 463 additions and 120 deletions.
4 changes: 1 addition & 3 deletions webclient/src/components/DetailsInfoSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ const state = useDetailsStore();
</div>
<div class="card-body">
<DetailsPropertyTable />
<div class="toast toast-warning" v-if="state.data?.coords.accuracy === 'building'">
{{ $t("view_view.msg.inaccurate_only_building.primary_msg") }}<br />
</div>
<div id="maybe-coordinate-inacurate-warning-toast" />
<div
class="toast toast-warning"
v-if="state.data?.type === 'room' && state.data?.maps?.overlays?.default === null"
Expand Down
14 changes: 0 additions & 14 deletions webclient/src/components/DetailsInteractiveMap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -296,20 +296,6 @@ function setOverlayImage(imgUrl: string | null, coords: Coordinates | undefined)
padding: 0 8px;
}
.toast.location-picker {
animation: fade-in 0.1s linear 0.05s;
animation-fill-mode: both;
& .btns {
margin: auto 0;
}
.toast {
// Mobile
margin-bottom: 9px;
font-size: 0.7rem;
}
}
/* --- Interactive map display --- */
#interactive-map-container {
margin-bottom: 10px;
Expand Down
121 changes: 121 additions & 0 deletions webclient/src/components/feedback/DetailsCoordinatePicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<script setup lang="ts">
import { selectedMap, useDetailsStore } from "@/stores/details";
import { Coord, useGlobalStore } from "@/stores/global";
import { ref } from "vue";
import { getLocalStorageWithExpiry, setLocalStorageWithExpiry } from "@/composables/storage";
const state = useDetailsStore();
const global = useGlobalStore();
// The coordinate picker keeps backups of the subject and body
// in case someone writes a text and then after that clicks
// the set coordinate button in the feedback form.
// If we no backup has been made then, this would be lost after clicking confirm there.
const coord_picker = ref({
backup_id: null as string | null,
subject_backup: null as string | null,
body_backup: null as string | null,
force_reopen: false,
});
const emit = defineEmits<{
(e: "openFeedbackForm", callback: EventListener): void;
}>();
function addLocationPicker() {
// If this is called from the feedback form using the edit coordinate
// button, we temporarily save the current subject and body, so it is
// not lost when being reopened
if (global.feedback.open) {
coord_picker.value.backup_id = state.data?.id || "undefined";
coord_picker.value.subject_backup = global.feedback.subject;
coord_picker.value.body_backup = global.feedback.body;
coord_picker.value.force_reopen = true; // reopen after confirm
global.temporarilyCloseFeedback();
}
state.map.selected = selectedMap.interactive;
// Verify that there isn't already a marker (could happen if you click 'assign
// a location' multiple times from the 'missing accurate location' toast)
if (marker2.value === null) {
// Coordinates are either taken from the entry, or if there are already
// some in the localStorage use them
const currentEdits = getLocalStorageWithExpiry<{ [index: string]: Coord }>("feedback-coords", {});
const { coords } = currentEdits[state.data?.id || "undefined"] || state.data;
marker2.value = new Marker({
draggable: true,
color: "#ff0000",
});
if (coords.lat !== undefined && coords.lon !== undefined)
marker2.value.setLngLat([coords.lon, coords.lat]).addTo(map.value as Map);
}
}
function confirmLocationPicker() {
// add the current edits to the feedback
const currentEdits = getLocalStorageWithExpiry<{ [index: string]: Coord }>("feedback-coords", {});
const location = marker2.value?.getLngLat();
currentEdits[state.data?.id || "undefined"] = {
coords: { lat: location?.lat, lon: location?.lng },
};
// save to local storage with ttl of 12h (garbage-collected on next read)
setLocalStorageWithExpiry("feedback-coords", currentEdits, 12);
marker2.value?.remove();
marker2.value = null;
// A feedback form is only opened when this is the only (and therefore
// first coordinate). If there are more coordinates we can assume
// someone is doing batch edits. They can then use the send button in
// the coordinate counter at the top of the page.
if (Object.keys(currentEdits).length === 1 || state.coord_picker.force_reopen) {
state.coord_picker.force_reopen = false;
emit("openFeedbackForm", () => addLocationPicker());
}
// The helptext (which says thet you can edit multiple coordinates in bulk)
// is also only shown if there is one edit.
if (Object.keys(currentEdits).length === 1) {
document.getElementById("feedback-coordinate-picker-helptext")?.classList.remove("d-none");
}
}
function cancelLocationPicker() {
marker2.value?.remove();
marker2.value = null;
if (state.coord_picker.force_reopen) {
state.coord_picker.force_reopen = false;
emit("openFeedbackForm", () => addLocationPicker());
}
}
</script>
<template>
<Teleport to="maybe-coordinate-inacurate-warning-toast">
<div class="toast toast-warning" v-if="state.data?.coords.accuracy === 'building'">
{{ $t("view_view.msg.inaccurate_only_building.primary_msg") }}<br />
<i>
{{ $t("view_view.msg.inaccurate_only_building.help_others_and") }}
<button class="btn btn-sm" @click="addLocationPicker">
{{ $t("view_view.msg.inaccurate_only_building.btn") }}
</button>
</i>
</div>
</Teleport>

<div class="toast toast-primary location-picker mb-2" v-if="marker2">
<div class="columns">
<div class="column col col-sm-12">
{{ $t("view_view.msg.correct_location.msg") }}
</div>
<div class="column col-auto col-sm-12 btns">
<button class="btn btn-sm" @click="cancelLocationPicker">
{{ $t("view_view.msg.correct_location.btn-cancel") }}
</button>
<button class="btn btn-sm" @click="confirmLocationPicker">
<i class="icon icon-check" />
{{ $t("view_view.msg.correct_location.btn-done") }}
</button>
</div>
</div>
</div>
</template>
<style lang="scss"></style>
41 changes: 41 additions & 0 deletions webclient/src/components/feedback/EditRequestModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
import BasicFeedbackModal from "@/components/feedback/TokenBasedModal.vue";
import { useGlobalStore } from "@/stores/global";
const global = useGlobalStore();
</script>

<template>
<BasicFeedbackModal :data="global.propose_edits.data">
<template v-slot:modal>
<div class="form-group">
<label class="form-label" for="additional_context">{{ $t("feedback.message") }}</label>
<textarea
class="form-input"
id="additional_context"
:placeholder="$t('feedback.message')"
v-model="global.propose_edits.data.additional_context"
rows="6"
/>
</div>
<h2>{{ $t("feedback.coordinates") }}</h2>
coordinates can be previewed here
</template>
<template v-slot:success="{ successUrl }">
<p>{{ $t("feedback.success.thank_you") }}</p>
<p>
{{ $t("feedback.success.response_at") }}
<a id="feedback-success-url" class="btn-link" :href="successUrl">{{ $t("feedback.success.this_pull_request") }}</a>
</p>
</template>
</BasicFeedbackModal>
</template>

<style lang="scss" scoped>
@import "@/assets/variables";
.modal {
#additional_context {
min-width: 100%;
}
}
</style>
128 changes: 128 additions & 0 deletions webclient/src/components/feedback/FeedbackModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<script setup lang="ts">
import { useGlobalStore } from "@/stores/global";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import TokenBasedModal from "@/components/feedback/TokenBasedModal.vue";
const { t } = useI18n({ inheritLocale: true, useScope: "global" });
const global = useGlobalStore();
const deleteIssueRequested = ref(false);
</script>

<template>
<TokenBasedModal :data="global.feedback.data">
<template v-slot:modal>
<div class="form-group">
<div id="feedback-coordinate-picker-helptext" class="d-none toast toast-primary">
{{ $t("feedback.coordinatepicker.helptext.enter_serveral") }}<br />
{{ $t("feedback.coordinatepicker.helptext.saved_for_12h") }}<br />
</div>
<label class="form-label" for="feedback-subject"> {{ $t("feedback.subject") }}</label>
<div class="input-group">
<select
class="form-select"
id="feedback-category"
:aria-label="$t('feedback.category')"
v-model="global.feedback.data.category"
>
<option value="general">{{ $t("feedback.type.general") }}</option>
<option value="bug">{{ $t("feedback.type.bug") }}</option>
<option value="features">{{ $t("feedback.type.features") }}</option>
<option value="search">{{ $t("feedback.type.search") }}</option>
<option value="entry">{{ $t("feedback.type.entry") }}</option>
</select>
<input
class="form-input"
type="text"
:placeholder="$t('feedback.subject')"
v-model="global.feedback.data.subject"
id="feedback-subject"
/>
</div>
</div>

<div class="form-group">
<div>
<label class="form-label" for="feedback-body">
{{ $t("feedback.message") }}
</label>
<button
id="feedback-coordinate-picker"
v-if="global.feedback.data.category === 'entry'"
class="btn btn-sm btn-link"
>
{{ $t("feedback.coordinatepicker.title") }}
</button>
</div>
<textarea
class="form-input"
id="feedback-body"
:placeholder="$t('feedback.message')"
v-model="global.feedback.data.body"
rows="6"
>
</textarea>
<p class="text-gray text-tiny">
{{
{
general: t("feedback.helptext.general"),
bug: t("feedback.helptext.bug"),
feature: t("feedback.helptext.features"),
search: t("feedback.helptext.search"),
entry: t("feedback.helptext.entry"),
other: t("feedback.helptext.other"), // This is only here to make the linter happy, backend uses "other" as a fallback if the category is not known
}[global.feedback.data.category]
}}
</p>
</div>

<!-- only visible if called through a view, because then the context of the calling building is availible -->
<div>
<button id="feedback-coordinate-picker" class="btn btn-sm d-none">
{{ $t("feedback.coordinatepicker.title") }}
</button>
</div>
<div class="form-group">
<label class="form-checkbox" id="feedback-delete-label">
<input type="checkbox" id="feedback-delete" v-model="deleteIssueRequested" />
<i class="form-icon" /> {{ $t("feedback.delete") }}
</label>
</div>
</template>
<template v-slot:success="{ successUrl }">
<p>{{ $t("feedback.success.thank_you") }}</p>
<p>
{{ $t("feedback.success.response_at") }}
<a id="feedback-success-url" class="btn-link" :href="successUrl">{{ $t("feedback.success.this_issue") }}</a>
</p>
</template>
</TokenBasedModal>
</template>

<style lang="scss" scoped>
@import "@/assets/variables";
.modal {
label {
width: fit-content;
display: inline-block;
}
.form-select {
flex: none;
}
#feedback-body {
min-width: 100%;
}
#feedback-coordinate-picker {
float: right;
margin-top: 0.5em;
}
#feedback-coordinate-picker-helptext {
font-size: 14px;
}
}
</style>
Loading

0 comments on commit 7d4834c

Please sign in to comment.