From 85d643d0ecd5e7504f32953b9ed1509697b915e2 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 7 Apr 2023 17:54:06 +0200 Subject: [PATCH] feat(addresses): Allow to enter manual addresses Signed-off-by: Thomas Citharel --- .../Event/FullAddressAutoComplete.vue | 297 ++++++++++++------ js/src/components/LeafletMap.vue | 32 +- js/src/graphql/group.ts | 55 +++- js/src/types/address.model.ts | 26 +- js/src/views/Event/EditView.vue | 55 +--- js/src/views/Group/GroupSettings.vue | 5 +- 6 files changed, 304 insertions(+), 166 deletions(-) diff --git a/js/src/components/Event/FullAddressAutoComplete.vue b/js/src/components/Event/FullAddressAutoComplete.vue index a803c8ad..d0b95b19 100644 --- a/js/src/components/Event/FullAddressAutoComplete.vue +++ b/js/src/components/Event/FullAddressAutoComplete.vue @@ -10,32 +10,24 @@ > -

- - -

+ {{ addressToPoiInfos(option).alternativeName }} +

+ + {{ t("Getting location") }} +

@@ -90,16 +86,80 @@
-
+ + +
+
+

{{ t("Manually enter address") }}

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + {{ t("Save") }} + + + {{ t("Clear") }} + +

+ {{ + t( + "You can drag and drop the marker below to the desired location" + ) + }} +

+
+
+
+
import("@/components/LeafletMap.vue") ); @@ -139,29 +209,38 @@ const props = withDefaults( hideSelected?: boolean; placeholder?: string; resultType?: AddressSearchType; + defaultCoords?: string; }>(), { + defaultCoords: "0;0", labelClass: "", - defaultText: "", disabled: false, hideMap: false, hideSelected: false, } ); -// const addressModalActive = ref(false); - -const componentId = 0; +const componentId = ref(0); const emit = defineEmits(["update:modelValue"]); const gettingLocationError = ref(null); const gettingLocation = ref(false); -const mapDefaultZoom = ref(15); +const mapDefaultZoom = computed(() => { + if (selected.description) { + return 15; + } + return 5; +}); const addressData = ref([]); -const selected = ref(null); +const defaultAddress = new Address(); +defaultAddress.geom = undefined; +defaultAddress.id = undefined; +const selected = reactive(defaultAddress); + +const detailsAddress = ref(false); const isFetching = ref(false); @@ -171,40 +250,45 @@ const placeholderWithDefault = computed( () => props.placeholder ?? t("e.g. 10 Rue Jangot") ); -// created(): void { -// componentId += 1; -// } +onBeforeMount(() => { + componentId.value += 1; +}); const id = computed((): string => { - return `full-address-autocomplete-${componentId}`; + return `full-address-autocomplete-${componentId.value}`; }); const modelValue = computed(() => props.modelValue); watch(modelValue, () => { - if (!modelValue.value) return; - selected.value = modelValue.value; + console.debug("modelValue changed"); + setSelected(modelValue.value); }); -const updateSelected = (option: IAddress): void => { - if (option == null) return; - selected.value = option; - emit("update:modelValue", selected.value); +onMounted(() => { + setSelected(modelValue.value); +}); + +const setSelected = (newValue: IAddress | null) => { + if (!newValue) return; + console.debug("setting selected to model value"); + Object.assign(selected, newValue); }; -// const resetPopup = (): void => { -// selected.value = new Address(); -// }; - -// const openNewAddressModal = (): void => { -// resetPopup(); -// addressModalActive.value = true; -// }; +const saveManualAddress = (): void => { + console.debug("saving address"); + selected.id = undefined; + selected.originId = undefined; + selected.url = undefined; + emit("update:modelValue", selected); + detailsAddress.value = false; +}; const checkCurrentPosition = (e: LatLng): boolean => { - if (!selected.value?.geom) return false; - const lat = parseFloat(selected.value?.geom.split(";")[1]); - const lon = parseFloat(selected.value?.geom.split(";")[0]); + console.debug("checkCurrentPosition"); + if (!selected?.geom || !e) return false; + const lat = parseFloat(selected?.geom.split(";")[1]); + const lon = parseFloat(selected?.geom.split(";")[0]); return e.lat === lat && e.lng === lon; }; @@ -238,12 +322,11 @@ onAddressSearchResult((result) => { isFetching.value = false; }); -const searchQuery = ref(""); - const asyncData = async (query: string): Promise => { + console.debug("Finding addresses"); if (!query.length) { addressData.value = []; - selected.value = new Address(); + Object.assign(selected, defaultAddress); return; } @@ -254,33 +337,39 @@ const asyncData = async (query: string): Promise => { isFetching.value = true; - searchQuery.value = query; - searchAddress(undefined, { - query: searchQuery.value, + query, locale: locale, type: props.resultType, }); }; -const queryText = computed({ +const selectedAddressText = computed(() => { + if (!selected) return undefined; + return addressFullName(selected); +}); + +const queryText = ref(); + +const queryTextWithDefault = computed({ get() { + console.log("queryTextWithDefault 1", queryText.value); + console.log("queryTextWithDefault 2", selectedAddressText.value); + console.log("queryTextWithDefault 3", props.defaultText); return ( - (selected.value ? addressFullName(selected.value) : props.defaultText) ?? - "" + queryText.value ?? selectedAddressText.value ?? props.defaultText ?? "" ); }, - set(text) { - if (text === "" && selected.value?.id) { - console.debug("doing reset"); - resetAddress(); - } + set(newValue: string) { + queryText.value = newValue; }, }); const resetAddress = (): void => { + console.debug("resetting address"); emit("update:modelValue", null); - selected.value = new Address(); + resetAddressAction(selected); + queryTextWithDefault.value = ""; }; const locateMe = async (): Promise => { @@ -288,7 +377,7 @@ const locateMe = async (): Promise => { gettingLocationError.value = null; try { const location = await getLocation(); - mapDefaultZoom.value = 12; + // mapDefaultZoom.value = 12; reverseGeoCode( new LatLng(location.coords.latitude, location.coords.longitude), 12 @@ -308,15 +397,26 @@ onReverseGeocodeResult((result) => { addressData.value = data.reverseGeocode; if (addressData.value.length > 0) { - const defaultAddress = addressData.value[0]; - selected.value = defaultAddress; - emit("update:modelValue", selected.value); + const foundAddress = addressData.value[0]; + Object.assign(selected, foundAddress); + console.debug("reverse geocode succeded, setting new address"); + queryTextWithDefault.value = addressFullName(foundAddress); + emit("update:modelValue", selected); } }); const reverseGeoCode = (e: LatLng, zoom: number) => { + console.debug("reverse geocode"); + + // If the details is opened, just update coords, don't reverse geocode + if (e && detailsAddress.value) { + selected.geom = `${e.lng};${e.lat}`; + console.debug("no reverse geocode, just setting new coords"); + return; + } + // If the position has been updated through autocomplete selection, no need to geocode it! - if (checkCurrentPosition(e)) return; + if (!e || checkCurrentPosition(e)) return; loadReverseGeocode(undefined, { latitude: e.lat, @@ -358,6 +458,17 @@ const getLocation = async (): Promise => { }); }; +const mapMarkerValue = computed(() => { + if (!selected.description) return undefined; + return { + text: [ + addressToPoiInfos(selected).name, + addressToPoiInfos(selected).alternativeName, + ], + icon: addressToPoiInfos(selected).poiIcon.icon, + }; +}); + const fieldErrors = computed(() => { return gettingLocationError.value; }); diff --git a/js/src/components/LeafletMap.vue b/js/src/components/LeafletMap.vue index 8aa613dd..286ab43e 100644 --- a/js/src/components/LeafletMap.vue +++ b/js/src/components/LeafletMap.vue @@ -21,10 +21,15 @@ - + @@ -63,6 +68,7 @@ import { useI18n } from "vue-i18n"; import Locatecontrol from "leaflet.locatecontrol"; import CrosshairsGps from "vue-material-design-icons/CrosshairsGps.vue"; import MapMarker from "vue-material-design-icons/MapMarker.vue"; +import { useDebounceFn } from "@vueuse/core"; const props = withDefaults( defineProps<{ @@ -159,22 +165,25 @@ const mergedOptions = computed((): Record => { }); const lat = computed((): number => { - return Number.parseFloat(props.coords?.split(";")[1]); + return Number.parseFloat(props.coords?.split(";")[1] || "0"); }); const lon = computed((): number => { - return Number.parseFloat(props.coords.split(";")[0]); + return Number.parseFloat(props.coords?.split(";")[0] || "0"); }); -const popupMultiLine = computed((): Array => { +const popupMultiLine = computed((): Array | undefined => { if (Array.isArray(props.marker?.text)) { return props.marker?.text as string[]; } - return [props.marker?.text]; + if (props.marker?.text) { + return [props.marker?.text]; + } + return undefined; }); const clickMap = (event: LeafletMouseEvent): void => { - updateDraggableMarkerPosition(event.latlng); + updateDraggableMarkerPositionDebounced(event.latlng); }; const updateDraggableMarkerPosition = (e: LatLng): void => { @@ -183,6 +192,10 @@ const updateDraggableMarkerPosition = (e: LatLng): void => { } }; +const updateDraggableMarkerPositionDebounced = useDebounceFn((e: LatLng) => { + updateDraggableMarkerPosition(e); +}, 1000); + const updateZoom = (newZoom: number): void => { zoom.value = newZoom; }; @@ -211,4 +224,9 @@ div.map-container { diff --git a/js/src/graphql/group.ts b/js/src/graphql/group.ts index ea188955..c68278b5 100644 --- a/js/src/graphql/group.ts +++ b/js/src/graphql/group.ts @@ -54,6 +54,50 @@ export const LIST_GROUPS = gql` ${ACTOR_FRAGMENT} `; +export const GROUP_VERY_BASIC_FIELDS_FRAGMENTS = gql` + fragment GroupVeryBasicFields on Group { + ...ActorFragment + suspended + visibility + openness + manuallyApprovesFollowers + physicalAddress { + description + street + locality + postalCode + region + country + geom + type + id + originId + url + } + avatar { + id + url + name + metadata { + width + height + blurhash + } + } + banner { + id + url + name + metadata { + width + height + blurhash + } + } + } + ${ACTOR_FRAGMENT} +`; + export const GROUP_BASIC_FIELDS_FRAGMENTS = gql` fragment GroupBasicFields on Group { ...ActorFragment @@ -296,17 +340,10 @@ export const UPDATE_GROUP = gql` physicalAddress: $physicalAddress manuallyApprovesFollowers: $manuallyApprovesFollowers ) { - ...ActorFragment - visibility - openness - manuallyApprovesFollowers - banner { - id - url - } + ...GroupVeryBasicFields } } - ${ACTOR_FRAGMENT} + ${GROUP_VERY_BASIC_FIELDS_FRAGMENTS} `; export const DELETE_GROUP = gql` diff --git a/js/src/types/address.model.ts b/js/src/types/address.model.ts index 4de385f1..580d7fda 100644 --- a/js/src/types/address.model.ts +++ b/js/src/types/address.model.ts @@ -93,7 +93,10 @@ export function addressToPoiInfos(address: IAddress): IPoiInfo { switch (addressType) { case "house": name = address.description; - alternativeName = [address.postalCode, address.locality, address.country] + alternativeName = ( + address.description !== address.street ? [address.street] : [] + ) + .concat([address.postalCode, address.locality, address.country]) .filter((zone) => zone) .join(", "); poiIcon = poiIcons.defaultAddress; @@ -123,8 +126,11 @@ export function addressToPoiInfos(address: IAddress): IPoiInfo { alternativeName = ""; if (address.street && address.street.trim()) { alternativeName = `${address.street}`; + if (address.postalCode) { + alternativeName += `, ${address.postalCode}`; + } if (address.locality) { - alternativeName += ` (${address.locality})`; + alternativeName += `, ${address.locality}`; } } else if (address.locality && address.locality.trim()) { alternativeName = `${address.locality}, ${address.region}, ${address.country}`; @@ -158,3 +164,19 @@ export function addressFullName(address: IAddress): string { } return ""; } + +export function resetAddress(address: IAddress): void { + address.id = undefined; + address.description = ""; + address.street = ""; + address.locality = ""; + address.postalCode = ""; + address.region = ""; + address.country = ""; + address.type = ""; + address.geom = undefined; + address.url = undefined; + address.originId = undefined; + address.timezone = undefined; + address.pictureInfo = undefined; +} diff --git a/js/src/views/Event/EditView.vue b/js/src/views/Event/EditView.vue index cbe8791e..7703d5c8 100644 --- a/js/src/views/Event/EditView.vue +++ b/js/src/views/Event/EditView.vue @@ -612,59 +612,6 @@ const FullAddressAutoComplete = defineAsyncComponent( () => import("@/components/Event/FullAddressAutoComplete.vue") ); -// apollo: { -// config: CONFIG_EDIT_EVENT, -// event: { -// query: FETCH_EVENT, -// variables() { -// return { -// uuid: this.eventId, -// }; -// }, -// update(data) { -// let event = data.event; -// if (this.isDuplicate) { -// event = { ...event, organizerActor: this.currentActor }; -// } -// return new EventModel(event); -// }, -// skip() { -// return !this.eventId; -// }, -// }, -// person: { -// query: PERSON_STATUS_GROUP, -// fetchPolicy: "cache-and-network", -// variables() { -// return { -// id: this.currentActor.id, -// group: usernameWithDomain(this.event?.attributedTo), -// }; -// }, -// skip() { -// return ( -// !this.event?.attributedTo || -// !this.event?.attributedTo?.preferredUsername -// ); -// }, -// }, -// group: { -// query: FETCH_GROUP_PUBLIC, -// fetchPolicy: "cache-and-network", -// variables() { -// return { -// name: this.event?.attributedTo?.preferredUsername, -// }; -// }, -// skip() { -// return ( -// !this.event?.attributedTo || -// !this.event?.attributedTo?.preferredUsername -// ); -// }, -// }, -// }, - const { t } = useI18n({ useScope: "global" }); useHead({ @@ -689,7 +636,6 @@ const unmodifiedEvent = ref(new EventModel()); const pictureFile = ref(null); -// const canPromote = ref(true); const limitedPlaces = ref(false); const showFixedNavbar = ref(true); @@ -1051,6 +997,7 @@ const buildVariables = async () => { res.picture = { mediaId: event.value?.picture.id }; } } + console.debug("builded variables", res); } catch (e) { console.error(e); } diff --git a/js/src/views/Group/GroupSettings.vue b/js/src/views/Group/GroupSettings.vue index 20fd776e..7d7a5702 100644 --- a/js/src/views/Group/GroupSettings.vue +++ b/js/src/views/Group/GroupSettings.vue @@ -369,7 +369,10 @@ const currentAddress = computed({ }, set(address: IAddress) { if (editableGroup.value) { - editableGroup.value.physicalAddress = address; + editableGroup.value = { + ...editableGroup.value, + physicalAddress: address, + }; } }, });