You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
682 lines
20 KiB
682 lines
20 KiB
<template>
|
|
<QuickNav selected="Projects"></QuickNav>
|
|
<!-- CONTENT -->
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
<!-- Breadcrumb -->
|
|
<div id="ViewBreadcrumb" class="mb-8">
|
|
<h1 class="text-lg text-center font-light relative px-7">
|
|
<!-- Cancel -->
|
|
<router-link
|
|
:to="{ name: 'project' }"
|
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
><fa icon="chevron-left" class="fa-fw"></fa
|
|
></router-link>
|
|
Edit Idea
|
|
</h1>
|
|
</div>
|
|
|
|
<!-- Project Details -->
|
|
<!-- Image - (see design model) Empty -->
|
|
|
|
<div>
|
|
{{ errorMessage }}
|
|
</div>
|
|
|
|
<input
|
|
type="text"
|
|
placeholder="Idea Name"
|
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
v-model="fullClaim.name"
|
|
/>
|
|
|
|
<div class="flex justify-center mt-4">
|
|
<span v-if="imageUrl" class="flex justify-between">
|
|
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
|
|
<img :src="imageUrl" class="h-24 rounded-xl" />
|
|
</a>
|
|
<fa
|
|
icon="trash-can"
|
|
@click="confirmDeleteImage"
|
|
class="text-red-500 fa-fw ml-8 mt-10"
|
|
/>
|
|
</span>
|
|
<span v-else>
|
|
<fa
|
|
icon="camera"
|
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
|
|
@click="openImageDialog"
|
|
/>
|
|
</span>
|
|
</div>
|
|
<ImageMethodDialog ref="imageDialog" />
|
|
|
|
<input
|
|
type="text"
|
|
placeholder="Other Authorized Representative"
|
|
class="mt-4 block w-full rounded border border-slate-400 px-3 py-2"
|
|
v-model="agentDid"
|
|
/>
|
|
<div class="mb-4">
|
|
<p v-if="activeDid != projectIssuerDid && agentDid != projectIssuerDid">
|
|
<span class="text-red-500">Beware!</span>
|
|
If you save this, the original project owner will no longer be able to
|
|
edit it.
|
|
<button @click="agentDid = projectIssuerDid" class="text-blue-500">
|
|
Click here to make the original owner an authorized representative.
|
|
</button>
|
|
</p>
|
|
</div>
|
|
|
|
<textarea
|
|
placeholder="Description"
|
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
rows="5"
|
|
v-model="fullClaim.description"
|
|
maxlength="5000"
|
|
></textarea>
|
|
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
|
|
If you want to be contacted, be sure to include your contact information.
|
|
</div>
|
|
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
|
|
{{ fullClaim.description?.length }}/5000 max. characters
|
|
</div>
|
|
|
|
<input
|
|
v-model="fullClaim.url"
|
|
placeholder="Website"
|
|
autocapitalize="none"
|
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
/>
|
|
|
|
<div class="flex mb-4 columns-3 w-full">
|
|
<input
|
|
v-model="startDateInput"
|
|
placeholder="Start Date"
|
|
type="date"
|
|
class="col-span-1 w-full rounded border border-slate-400 px-3 py-2"
|
|
/>
|
|
<input
|
|
:disabled="!startDateInput"
|
|
placeholder="Start Time"
|
|
v-model="startTimeInput"
|
|
type="time"
|
|
class="col-span-1 w-full rounded border border-slate-400 ml-2 px-3 py-2"
|
|
/>
|
|
<span class="col-span-1 w-full flex justify-center">{{ zoneName }}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center mb-4"
|
|
@click="includeLocation = !includeLocation"
|
|
>
|
|
<input type="checkbox" class="mr-2" v-model="includeLocation" />
|
|
<label for="includeLocation">Include Location</label>
|
|
</div>
|
|
<div v-if="includeLocation" class="mb-4 aspect-video">
|
|
<p class="text-sm mb-2 text-slate-500">
|
|
For your security, choose a location nearby but not exactly at the
|
|
place.
|
|
</p>
|
|
|
|
<l-map
|
|
ref="map"
|
|
v-model:zoom="zoom"
|
|
:center="[0, 0]"
|
|
class="!z-40 rounded-md"
|
|
@click="
|
|
(event) => {
|
|
latitude = event.latlng.lat;
|
|
longitude = event.latlng.lng;
|
|
}
|
|
"
|
|
>
|
|
<l-tile-layer
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
layer-type="base"
|
|
name="OpenStreetMap"
|
|
/>
|
|
<l-marker
|
|
v-if="latitude && longitude"
|
|
:lat-lng="[latitude, longitude]"
|
|
@click="confirmEraseLatLong()"
|
|
/>
|
|
</l-map>
|
|
</div>
|
|
|
|
<div
|
|
v-if="showGeneralAdvanced && includeLocation"
|
|
class="items-center mb-4"
|
|
>
|
|
<div class="flex" @click="sendToTrustroots = !sendToTrustroots">
|
|
<input type="checkbox" class="mr-2" v-model="sendToTrustroots" />
|
|
<label>Send to Trustroots</label>
|
|
</div>
|
|
<div class="flex" @click="sendToTripHopping = !sendToTripHopping">
|
|
<input type="checkbox" class="mr-2" v-model="sendToTripHopping" />
|
|
<label>Send to TripHopping</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-8">
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
<button
|
|
:disabled="isHiddenSave"
|
|
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
|
|
@click="onSaveProjectClick()"
|
|
>
|
|
<!-- SHOW if in idle state -->
|
|
<span :class="{ hidden: isHiddenSave }">Save Project</span>
|
|
|
|
<!-- SHOW if in saving state; DISABLE button while in saving state -->
|
|
<span :class="{ hidden: isHiddenSpinner }">
|
|
<!-- icon no worky? -->
|
|
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
|
|
Saving...</span
|
|
>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
|
@click="onCancelClick()"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import "leaflet/dist/leaflet.css";
|
|
import { AxiosError, AxiosRequestHeaders } from "axios";
|
|
import { DateTime } from "luxon";
|
|
import { hexToBytes } from "@noble/hashes/utils";
|
|
import type { EventTemplate, VerifiedEvent } from "nostr-tools/lib/types/core";
|
|
import { accountFromSeedWords } from "nostr-tools/nip06";
|
|
import { finalizeEvent, serializeEvent } from "nostr-tools/pure";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
|
|
|
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
|
|
import QuickNav from "@/components/QuickNav.vue";
|
|
import {
|
|
DEFAULT_IMAGE_API_SERVER,
|
|
DEFAULT_PARTNER_API_SERVER,
|
|
NotificationIface,
|
|
} from "@/constants/app";
|
|
import { accountsDB, retrieveSettingsForActiveAccount } from "@/db/index";
|
|
import {
|
|
createEndorserJwtVcFromClaim,
|
|
getHeaders,
|
|
PlanVerifiableCredential,
|
|
} from "@/libs/endorserServer";
|
|
import { getAccount } from "@/libs/util";
|
|
|
|
@Component({
|
|
components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav },
|
|
})
|
|
export default class NewEditProjectView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
errNote(message: string) {
|
|
this.$notify(
|
|
{ group: "alert", type: "danger", title: "Error", text: message },
|
|
5000,
|
|
);
|
|
}
|
|
|
|
activeDid = "";
|
|
agentDid = "";
|
|
apiServer = "";
|
|
errorMessage = "";
|
|
fullClaim: PlanVerifiableCredential = {
|
|
"@context": "https://schema.org",
|
|
"@type": "PlanAction",
|
|
name: "",
|
|
description: "",
|
|
}; // this default is only to avoid errors before plan is loaded
|
|
imageUrl = "";
|
|
includeLocation = false;
|
|
isHiddenSave = false;
|
|
isHiddenSpinner = true;
|
|
lastClaimJwtId = "";
|
|
latitude = 0;
|
|
longitude = 0;
|
|
numAccounts = 0;
|
|
projectId = "";
|
|
projectIssuerDid = "";
|
|
sendToTrustroots = false;
|
|
sendToTripHopping = false;
|
|
showGeneralAdvanced = false;
|
|
startDateInput?: string;
|
|
startTimeInput?: string;
|
|
zoneName = DateTime.local().zoneName;
|
|
zoom = 2;
|
|
|
|
async mounted() {
|
|
await accountsDB.open();
|
|
this.numAccounts = await accountsDB.accounts.count();
|
|
|
|
const settings = await retrieveSettingsForActiveAccount();
|
|
this.activeDid = settings.activeDid || "";
|
|
this.apiServer = settings.apiServer || "";
|
|
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
|
|
|
|
this.projectId =
|
|
(this.$route as RouteLocationNormalizedLoaded).query["projectId"] || "";
|
|
|
|
if (this.projectId) {
|
|
if (this.numAccounts === 0) {
|
|
this.errNote("There was a problem loading your account info.");
|
|
} else {
|
|
this.loadProject(this.activeDid);
|
|
}
|
|
}
|
|
}
|
|
|
|
async loadProject(userDid: string) {
|
|
const url =
|
|
this.apiServer +
|
|
"/api/claim/byHandle/" +
|
|
encodeURIComponent(this.projectId);
|
|
const headers = await getHeaders(userDid);
|
|
|
|
try {
|
|
const resp = await this.axios.get(url, { headers });
|
|
if (resp.status === 200) {
|
|
this.projectIssuerDid = resp.data.issuer;
|
|
this.fullClaim = resp.data.claim;
|
|
this.imageUrl = resp.data.claim.image || "";
|
|
this.lastClaimJwtId = resp.data.id;
|
|
if (this.fullClaim?.location) {
|
|
this.includeLocation = true;
|
|
this.latitude = this.fullClaim.location.geo.latitude;
|
|
this.longitude = this.fullClaim.location.geo.longitude;
|
|
}
|
|
if (this.fullClaim?.agent?.identifier) {
|
|
this.agentDid = this.fullClaim.agent.identifier;
|
|
}
|
|
if (this.fullClaim.startTime) {
|
|
const localDateTime = DateTime.fromISO(
|
|
this.fullClaim.startTime as string,
|
|
).toLocal();
|
|
this.startDateInput = localDateTime.toFormat("yyyy-MM-dd");
|
|
this.startTimeInput = localDateTime.toFormat("HH:mm");
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Got error retrieving that project", error);
|
|
this.errNote("There was an error retrieving that project.");
|
|
}
|
|
}
|
|
|
|
openImageDialog() {
|
|
(this.$refs.imageDialog as ImageMethodDialog).open((imgUrl) => {
|
|
this.imageUrl = imgUrl;
|
|
}, "PlanAction");
|
|
}
|
|
|
|
confirmDeleteImage() {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Are you sure you want to delete the image?",
|
|
text: "",
|
|
onYes: this.deleteImage,
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
async deleteImage() {
|
|
if (!this.imageUrl) {
|
|
return;
|
|
}
|
|
try {
|
|
const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders;
|
|
const response = await this.axios.delete(
|
|
DEFAULT_IMAGE_API_SERVER +
|
|
"/image/" +
|
|
encodeURIComponent(this.imageUrl),
|
|
{ headers },
|
|
);
|
|
if (response.status === 204) {
|
|
// don't bother with a notification
|
|
// (either they'll simply continue or they're canceling and going back)
|
|
} else {
|
|
console.error("Problem deleting image:", response);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "There was a problem deleting the image.",
|
|
},
|
|
5000,
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.imageUrl = "";
|
|
} catch (error) {
|
|
console.error("Error deleting image:", error);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if ((error as any).response.status === 404) {
|
|
console.log("The image was already deleted:", error);
|
|
|
|
this.imageUrl = "";
|
|
|
|
// it already doesn't exist so we won't say anything to the user
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "There was an error deleting the image.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async saveProject() {
|
|
// Make a claim
|
|
const vcClaim: PlanVerifiableCredential = this.fullClaim;
|
|
if (this.projectId) {
|
|
vcClaim.lastClaimId = this.lastClaimJwtId;
|
|
}
|
|
if (this.agentDid) {
|
|
vcClaim.agent = {
|
|
identifier: this.agentDid,
|
|
};
|
|
} else {
|
|
delete vcClaim.agent;
|
|
}
|
|
if (this.imageUrl) {
|
|
vcClaim.image = this.imageUrl;
|
|
} else {
|
|
delete vcClaim.image;
|
|
}
|
|
if (this.includeLocation) {
|
|
vcClaim.location = {
|
|
geo: {
|
|
"@type": "GeoCoordinates",
|
|
latitude: this.latitude,
|
|
longitude: this.longitude,
|
|
},
|
|
};
|
|
} else {
|
|
delete vcClaim.location;
|
|
}
|
|
if (this.startDateInput) {
|
|
try {
|
|
const startTimeFull = this.startTimeInput || "00:00:00";
|
|
const fullTimeString = this.startDateInput + " " + startTimeFull;
|
|
// throw an error on an invalid date or time string
|
|
vcClaim.startTime = new Date(fullTimeString).toISOString(); // ensure timezone is part of it
|
|
} catch {
|
|
// it's not a valid date so erase it and tell the user
|
|
delete vcClaim.startTime;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "The date was invalid so it was not set.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
} else {
|
|
delete vcClaim.startTime;
|
|
}
|
|
const vcJwt = await createEndorserJwtVcFromClaim(this.activeDid, vcClaim);
|
|
|
|
// Make the xhr request payload
|
|
|
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
|
const url = this.apiServer + "/api/v2/claim";
|
|
const headers = await getHeaders(this.activeDid);
|
|
|
|
try {
|
|
const resp = await this.axios.post(url, payload, { headers });
|
|
if (resp.data?.success?.handleId) {
|
|
this.errorMessage = "";
|
|
|
|
const projectPath = encodeURIComponent(resp.data.success.handleId);
|
|
|
|
let signedPayload: VerifiedEvent | undefined; // sign something to prove ownership of pubkey
|
|
if (this.sendToTrustroots) {
|
|
signedPayload = await this.signPayload();
|
|
this.sendToNostrPartner(
|
|
"NOSTR-EVENT-TRUSTROOTS",
|
|
"Trustroots",
|
|
resp.data.success.claimId,
|
|
signedPayload,
|
|
);
|
|
}
|
|
if (this.sendToTripHopping) {
|
|
if (!signedPayload) {
|
|
signedPayload = await this.signPayload();
|
|
}
|
|
this.sendToNostrPartner(
|
|
"NOSTR-EVENT-TRIPHOPPING",
|
|
"TripHopping",
|
|
resp.data.success.claimId,
|
|
signedPayload,
|
|
);
|
|
}
|
|
|
|
(this.$router as Router).push({ path: "/project/" + projectPath });
|
|
} else {
|
|
console.error(
|
|
"Got unexpected 'data' inside response from server",
|
|
resp,
|
|
);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Saving Idea",
|
|
text: "Server did not save the idea. Try again.",
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
let userMessage = "There was an error saving the project.";
|
|
const serverError = error as AxiosError<{
|
|
error?: { message?: string };
|
|
}>;
|
|
if (serverError) {
|
|
console.error("Got error from server", serverError);
|
|
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
|
userMessage =
|
|
(serverError.response?.data?.error?.message as string) ||
|
|
userMessage;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "User Message",
|
|
text: userMessage,
|
|
},
|
|
-1,
|
|
);
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Server Message",
|
|
text: JSON.stringify(serverError.toJSON()),
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
} else {
|
|
console.error("Here's the full error trying to save the claim:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Claim Error",
|
|
text: error as string,
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
// Now set that error for the user to see.
|
|
this.errorMessage = userMessage;
|
|
}
|
|
}
|
|
|
|
private async signPayload(): Promise<VerifiedEvent> {
|
|
const account = await getAccount(this.activeDid);
|
|
// get the last number of the derivationPath
|
|
const finalDerNum = account?.derivationPath?.split?.("/")?.reverse()[0];
|
|
// remove any trailing '
|
|
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
|
|
const accountNum = Number(finalDerNumNoApostrophe || 0);
|
|
const pubPri = accountFromSeedWords(
|
|
account?.mnemonic as string,
|
|
"",
|
|
accountNum,
|
|
);
|
|
const privateBytes = hexToBytes(pubPri?.privateKey);
|
|
// No real content is necessary, we just want something signed,
|
|
// so we might as well use nostr libs for nostr functions.
|
|
// Besides: someday we may create real content that we can relay.
|
|
const event: EventTemplate = {
|
|
kind: 30402,
|
|
tags: [[]],
|
|
content: "",
|
|
created_at: 0,
|
|
};
|
|
// Why does IntelliJ not see matching types?
|
|
const signedEvent = finalizeEvent(event, privateBytes);
|
|
return signedEvent;
|
|
}
|
|
|
|
private async sendToNostrPartner(
|
|
linkCode: string,
|
|
serviceName: string,
|
|
jwtId: string,
|
|
signedPayload: VerifiedEvent,
|
|
) {
|
|
// first, get the public key for nostr
|
|
const account = await getAccount(this.activeDid);
|
|
// get the last number of the derivationPath
|
|
const finalDerNum = account?.derivationPath?.split?.("/")?.reverse()[0];
|
|
// remove any trailing '
|
|
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
|
|
const accountNum = Number(finalDerNumNoApostrophe || 0);
|
|
const pubPri = accountFromSeedWords(
|
|
account?.mnemonic as string,
|
|
"",
|
|
accountNum,
|
|
);
|
|
const nostrPubKey = pubPri?.publicKey;
|
|
|
|
const trustrootsUrl = DEFAULT_PARTNER_API_SERVER + "/api/partner/link";
|
|
const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
|
|
const content = this.fullClaim.name + " - see " + timeSafariUrl;
|
|
// Why does IntelliJ not see matching types?
|
|
const payload = serializeEvent(signedPayload);
|
|
const trustrootsParams = {
|
|
jwtId: jwtId,
|
|
linkCode: linkCode,
|
|
inputJson: JSON.stringify(content),
|
|
pubKeyHex: nostrPubKey,
|
|
pubKeyImage: payload,
|
|
pubKeySigHex: signedPayload.sig,
|
|
};
|
|
const fullTrustrootsUrl = trustrootsUrl;
|
|
const headers = await getHeaders(this.activeDid);
|
|
try {
|
|
const linkResp = await this.axios.post(
|
|
fullTrustrootsUrl,
|
|
trustrootsParams,
|
|
{ headers },
|
|
);
|
|
if (linkResp.status === 201) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: `Sent to ${serviceName}`,
|
|
text: `The project info was sent to ${serviceName}.`,
|
|
},
|
|
5000,
|
|
);
|
|
} else {
|
|
// axios never gets here because it throws an error, but just in case
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: `Failed Sending to ${serviceName}`,
|
|
text: JSON.stringify(linkResp.data),
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
console.error(`Error sending to ${serviceName}`, error);
|
|
let errorMessage = `There was an error sending to ${serviceName}.`;
|
|
if (error.response?.data?.error?.message) {
|
|
errorMessage = error.response.data.error.message;
|
|
}
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: `Error Sending to ${serviceName}`,
|
|
text: errorMessage,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
public async onSaveProjectClick() {
|
|
this.isHiddenSave = true;
|
|
this.isHiddenSpinner = false;
|
|
|
|
if (this.numAccounts === 0) {
|
|
console.error("Error: there is no account.");
|
|
} else {
|
|
this.saveProject();
|
|
}
|
|
}
|
|
|
|
confirmEraseLatLong() {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Erase Marker",
|
|
text: "Are you sure you don't want to mark a location? This will erase the current location.",
|
|
onYes: async () => {
|
|
this.eraseLatLong();
|
|
},
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
public eraseLatLong() {
|
|
this.latitude = 0;
|
|
this.longitude = 0;
|
|
this.includeLocation = false;
|
|
}
|
|
|
|
public onCancelClick() {
|
|
(this.$router as Router).back();
|
|
}
|
|
}
|
|
</script>
|
|
|