added new geoPoint field
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
// manually load the markers so that they can be embedded in the prod bundle
|
||||
import markerIconUrl from "leaflet/dist/images/marker-icon.png";
|
||||
import markerIconRetinaUrl from "leaflet/dist/images/marker-icon-2x.png";
|
||||
import markerShadowUrl from "leaflet/dist/images/marker-shadow.png";
|
||||
|
||||
export let height = 225;
|
||||
export let point = { lat: 0, lon: 0 };
|
||||
|
||||
let map;
|
||||
let mapEl;
|
||||
let marker;
|
||||
let isSearching = false;
|
||||
let searchTerm = "";
|
||||
let searchResults = [];
|
||||
let searchTimeoutId;
|
||||
let searchAbortController;
|
||||
let panTimeoutId;
|
||||
|
||||
const defaultZoomLevel = 8;
|
||||
|
||||
$: search(searchTerm);
|
||||
|
||||
$: if (point.lat && point.lon) {
|
||||
panInside();
|
||||
}
|
||||
|
||||
function normalizeCoordinate(coord) {
|
||||
return +(+coord).toFixed(6);
|
||||
}
|
||||
|
||||
function panInside(debounce = 200) {
|
||||
clearTimeout(panTimeoutId);
|
||||
panTimeoutId = setTimeout(() => {
|
||||
marker?.setLatLng([point.lat, point.lon]);
|
||||
map?.panInside([point.lat, point.lon], { padding: [20, 40] });
|
||||
}, debounce);
|
||||
}
|
||||
|
||||
function initMap() {
|
||||
const latlon = [normalizeCoordinate(point.lat), normalizeCoordinate(point.lon)];
|
||||
|
||||
map = L.map(mapEl, { zoomControl: false }).setView(latlon, defaultZoomLevel);
|
||||
|
||||
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
}).addTo(map);
|
||||
|
||||
// reassign the default marker images with the loaded ones
|
||||
// (https://leafletjs.com/reference.html#icon-default-option)
|
||||
L.Icon.Default.prototype.options.iconUrl = markerIconUrl;
|
||||
L.Icon.Default.prototype.options.iconRetinaUrl = markerIconRetinaUrl;
|
||||
L.Icon.Default.prototype.options.shadowUrl = markerShadowUrl;
|
||||
L.Icon.Default.imagePath = "";
|
||||
|
||||
marker = L.marker(latlon, {
|
||||
draggable: true,
|
||||
autoPan: true,
|
||||
}).addTo(map);
|
||||
|
||||
marker.bindTooltip("drag or right click anywhere on the map to move");
|
||||
|
||||
marker.on("moveend", (e) => {
|
||||
if (e.sourceTarget?._latlng) {
|
||||
select(e.sourceTarget._latlng.lat, e.sourceTarget._latlng.lng, false);
|
||||
}
|
||||
});
|
||||
|
||||
map.on("contextmenu", (e) => {
|
||||
select(e.latlng.lat, e.latlng.lng, false);
|
||||
});
|
||||
}
|
||||
|
||||
function destroyMap() {
|
||||
resetSearch();
|
||||
marker?.remove();
|
||||
map?.remove();
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
searchAbortController?.abort();
|
||||
clearTimeout(searchTimeoutId);
|
||||
isSearching = false;
|
||||
searchResults = [];
|
||||
searchTerm = "";
|
||||
}
|
||||
|
||||
// note: using debounce > 1s to minimize hitting the API rate limits
|
||||
// (see also https://operations.osmfoundation.org/policies/nominatim/)
|
||||
function search(q, debounce = 1100) {
|
||||
isSearching = true;
|
||||
searchResults = [];
|
||||
clearTimeout(searchTimeoutId);
|
||||
searchAbortController?.abort();
|
||||
|
||||
if (!q) {
|
||||
isSearching = false;
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimeoutId = setTimeout(async () => {
|
||||
searchAbortController = new AbortController();
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://nominatim.openstreetmap.org/search.php?format=jsonv2&q=" + encodeURIComponent(q),
|
||||
{ signal: searchAbortController.signal },
|
||||
);
|
||||
if (response.status != 200) {
|
||||
throw new Error("OpenStreetMap API error " + response.status);
|
||||
}
|
||||
|
||||
const addresses = await response.json();
|
||||
for (const item of addresses) {
|
||||
searchResults.push({
|
||||
lat: item.lat,
|
||||
lon: item.lon,
|
||||
name: item.display_name,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[address search failed]", err);
|
||||
}
|
||||
|
||||
searchResults = searchResults;
|
||||
isSearching = false;
|
||||
}, debounce);
|
||||
}
|
||||
|
||||
function select(lat, lon, centerMap = true) {
|
||||
point.lat = normalizeCoordinate(lat);
|
||||
point.lon = normalizeCoordinate(lon);
|
||||
|
||||
// center the map
|
||||
if (centerMap) {
|
||||
marker?.setLatLng([point.lat, point.lon]); // optimistic marker update
|
||||
map?.panTo([point.lat, point.lon], { animate: false });
|
||||
}
|
||||
|
||||
resetSearch();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
initMap();
|
||||
|
||||
return () => {
|
||||
destroyMap();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="map-wrapper" style="{height ? `height:${height}px` : null};">
|
||||
<div class="map-search">
|
||||
<div class="form-field m-0">
|
||||
{#if isSearching}
|
||||
<div class="form-field-addon">
|
||||
<span class="loader loader-xs"></span>
|
||||
</div>
|
||||
{:else if searchTerm.length}
|
||||
<div class="form-field-addon">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-circle btn-xs btn-transparent"
|
||||
on:click={resetSearch}
|
||||
>
|
||||
<i class="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<input type="text" placeholder="Search address..." bind:value={searchTerm} />
|
||||
</div>
|
||||
{#if searchTerm.length && searchResults.length}
|
||||
<div class="dropdown dropdown-sm dropdown-block">
|
||||
{#each searchResults as result, i}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
use:tooltip={"Select address coordinates"}
|
||||
on:click={() => select(result.lat, result.lon)}
|
||||
>
|
||||
{result.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div bind:this={mapEl} class="map-box"></div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.map-wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.map-box {
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.map-search {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
top: 10px;
|
||||
width: 70%;
|
||||
max-width: 400px;
|
||||
margin-left: 15%;
|
||||
height: auto;
|
||||
input {
|
||||
opacity: 0.7;
|
||||
background: var(--baseColor);
|
||||
border: 0;
|
||||
box-shadow: 0 0 3px 0 var(--shadowColor);
|
||||
transition: opacity var(--baseAnimationSpeed);
|
||||
}
|
||||
.dropdown {
|
||||
max-height: 150px;
|
||||
border: 0;
|
||||
box-shadow: 0 0 3px 0 var(--shadowColor);
|
||||
}
|
||||
&:focus-within {
|
||||
input {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -15,6 +15,7 @@
|
||||
import SchemaFieldSelect from "@/components/collections/schema/SchemaFieldSelect.svelte";
|
||||
import SchemaFieldText from "@/components/collections/schema/SchemaFieldText.svelte";
|
||||
import SchemaFieldUrl from "@/components/collections/schema/SchemaFieldUrl.svelte";
|
||||
import SchemaFieldGeoPoint from "@/components/collections/schema/SchemaFieldGeoPoint.svelte";
|
||||
import { scaffolds } from "@/stores/collections";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
@@ -37,6 +38,7 @@
|
||||
relation: SchemaFieldRelation,
|
||||
password: SchemaFieldPassword,
|
||||
autodate: SchemaFieldAutodate,
|
||||
geoPoint: SchemaFieldGeoPoint,
|
||||
};
|
||||
|
||||
$: if (!collection.id && oldCollectionType != collection.type) {
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<script>
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import { scale } from "svelte/transition";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { errors, removeError, setErrors } from "@/stores/errors";
|
||||
import { addSuccessToast, removeAllToasts } from "@/stores/toasts";
|
||||
import { addCollection, removeCollection, scaffolds, activeCollection } from "@/stores/collections";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
@@ -8,14 +16,6 @@
|
||||
import CollectionQueryTab from "@/components/collections/CollectionQueryTab.svelte";
|
||||
import CollectionRulesTab from "@/components/collections/CollectionRulesTab.svelte";
|
||||
import CollectionUpdateConfirm from "@/components/collections/CollectionUpdateConfirm.svelte";
|
||||
import { addCollection, removeCollection, scaffolds, activeCollection } from "@/stores/collections";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { errors, removeError, setErrors } from "@/stores/errors";
|
||||
import { addSuccessToast, removeAllToasts } from "@/stores/toasts";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import { scale } from "svelte/transition";
|
||||
|
||||
const TAB_SCHEMA = "schema";
|
||||
const TAB_RULES = "api_rules";
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
icon: CommonHelper.getFieldTypeIcon("url"),
|
||||
},
|
||||
{
|
||||
label: "DateTime",
|
||||
label: "Datetime",
|
||||
value: "date",
|
||||
icon: CommonHelper.getFieldTypeIcon("date"),
|
||||
},
|
||||
@@ -69,6 +69,11 @@
|
||||
value: "json",
|
||||
icon: CommonHelper.getFieldTypeIcon("json"),
|
||||
},
|
||||
{
|
||||
label: "Geo Point",
|
||||
value: "geoPoint",
|
||||
icon: CommonHelper.getFieldTypeIcon("geoPoint"),
|
||||
},
|
||||
// {
|
||||
// label: "Password",
|
||||
// value: "password",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<script>
|
||||
import SchemaField from "@/components/collections/schema/SchemaField.svelte";
|
||||
|
||||
export let field;
|
||||
export let key = "";
|
||||
</script>
|
||||
|
||||
<SchemaField bind:field {key} on:rename on:remove on:duplicate {...$$restProps} />
|
||||
@@ -5,6 +5,7 @@
|
||||
import TinyMCE from "@/components/base/TinyMCE.svelte";
|
||||
import RecordFileThumb from "@/components/records/RecordFileThumb.svelte";
|
||||
import RecordInfo from "@/components/records/RecordInfo.svelte";
|
||||
import GeoPointValue from "@/components/records/fields/GeoPointValue.svelte";
|
||||
import { superuser } from "@/stores/superuser";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
|
||||
@@ -120,6 +121,8 @@
|
||||
...
|
||||
{/if}
|
||||
</div>
|
||||
{:else if field.type === "geoPoint"}
|
||||
<div class="label"><GeoPointValue value={rawValue} /></div>
|
||||
{:else if short}
|
||||
<span class="txt txt-ellipsis" title={CommonHelper.truncate(rawValue)}>
|
||||
{CommonHelper.truncate(rawValue)}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import RecordFileThumb from "@/components/records/RecordFileThumb.svelte";
|
||||
import RecordInfoContent from "@/components/records/RecordInfoContent.svelte";
|
||||
import GeoPointValue from "@/components/records/fields/GeoPointValue.svelte";
|
||||
import { collections } from "@/stores/collections";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
|
||||
export let record;
|
||||
|
||||
@@ -53,6 +54,8 @@
|
||||
|
||||
{#if field.type == "relation" && record.expand?.[field.name]}
|
||||
<RecordInfoContent bind:record={record.expand[field.name]} />
|
||||
{:else if field.type == "geoPoint"}
|
||||
<GeoPointValue value={record[field.name]} />
|
||||
{:else}
|
||||
{CommonHelper.truncate(CommonHelper.displayValue(record, [field.name]), 70)}
|
||||
{/if}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import SelectField from "@/components/records/fields/SelectField.svelte";
|
||||
import TextField from "@/components/records/fields/TextField.svelte";
|
||||
import UrlField from "@/components/records/fields/UrlField.svelte";
|
||||
import GeoPointField from "@/components/records/fields/GeoPointField.svelte";
|
||||
import ImpersonatePopup from "@/components/records/ImpersonatePopup.svelte";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
@@ -730,6 +731,8 @@
|
||||
<RelationField {field} {original} {record} bind:value={record[field.name]} />
|
||||
{:else if field.type === "password"}
|
||||
<PasswordField {field} {original} {record} bind:value={record[field.name]} />
|
||||
{:else if field.type === "geoPoint"}
|
||||
<GeoPointField {field} {original} {record} bind:value={record[field.name]} />
|
||||
{/if}
|
||||
{/each}
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<script>
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import FieldLabel from "@/components/records/fields/FieldLabel.svelte";
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
export let original;
|
||||
export let field;
|
||||
export let value = undefined;
|
||||
|
||||
let mapComponent;
|
||||
let isMapComponentLoading = false;
|
||||
let isMapVisible = false;
|
||||
|
||||
$: if (typeof value === "undefined") {
|
||||
value = { lat: 0, lon: 0 };
|
||||
}
|
||||
|
||||
$: if (value) {
|
||||
normalize();
|
||||
}
|
||||
|
||||
function normalize() {
|
||||
if (value.lat > 90) {
|
||||
value.lat = 90;
|
||||
}
|
||||
|
||||
if (value.lat < -90) {
|
||||
value.lat = -90;
|
||||
}
|
||||
|
||||
if (value.lon > 180) {
|
||||
value.lon = 180;
|
||||
}
|
||||
|
||||
if (value.lon < -180) {
|
||||
value.lon = -180;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMapVisibility() {
|
||||
if (isMapVisible) {
|
||||
hideMap();
|
||||
} else {
|
||||
showMap();
|
||||
}
|
||||
}
|
||||
|
||||
function showMap() {
|
||||
loadMapComponent();
|
||||
isMapVisible = true;
|
||||
}
|
||||
|
||||
function hideMap() {
|
||||
isMapVisible = false;
|
||||
}
|
||||
|
||||
async function loadMapComponent() {
|
||||
if (mapComponent || isMapComponentLoading) {
|
||||
return; // already loaded or in the process
|
||||
}
|
||||
|
||||
isMapComponentLoading = true;
|
||||
|
||||
mapComponent = (await import("@/components/base/Leaflet.svelte")).default;
|
||||
|
||||
isMapComponentLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field class="form-field form-field-list {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<FieldLabel {uniqueId} {field} />
|
||||
|
||||
<div class="list">
|
||||
<div class="list-item">
|
||||
<Field class="form-field form-field-inline m-0" let:uniqueId>
|
||||
<label for={uniqueId}>Longitude:</label>
|
||||
<input
|
||||
type="number"
|
||||
id={uniqueId}
|
||||
required={field.required}
|
||||
placeholder="0"
|
||||
step="any"
|
||||
min="-180"
|
||||
max="180"
|
||||
bind:value={value.lon}
|
||||
/>
|
||||
</Field>
|
||||
<span class="separator"></span>
|
||||
<Field class="form-field form-field-inline m-0" let:uniqueId>
|
||||
<label for={uniqueId}>Latitude:</label>
|
||||
<input
|
||||
type="number"
|
||||
id={uniqueId}
|
||||
required={field.required}
|
||||
placeholder="0"
|
||||
step="any"
|
||||
min="-90"
|
||||
max="90"
|
||||
bind:value={value.lat}
|
||||
/>
|
||||
</Field>
|
||||
<span class="separator"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-circle btn-sm btn-circle {isMapVisible
|
||||
? 'btn-secondary'
|
||||
: 'btn-hint btn-transparent'}"
|
||||
aria-label="Toggle map"
|
||||
use:tooltip={"Toggle map"}
|
||||
on:click={toggleMapVisibility}
|
||||
>
|
||||
<i class="ri-map-2-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if isMapVisible}
|
||||
<div class="block" style="height:200px" transition:slide={{ duration: 150 }}>
|
||||
{#if isMapComponentLoading}
|
||||
<div class="block txt-center p-base">
|
||||
<span class="loader loader-sm"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<svelte:component this={mapComponent} height={200} bind:point={value} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<style lang="scss">
|
||||
.list-item {
|
||||
padding: 5px 10px;
|
||||
min-height: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
.separator {
|
||||
align-self: stretch;
|
||||
background: var(--baseAlt2Color);
|
||||
width: 1px;
|
||||
margin: -5px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script>
|
||||
export let value = {};
|
||||
</script>
|
||||
|
||||
<div class="txt">
|
||||
{value?.lon}
|
||||
<span class="txt-disabled txt-xs">|</span>
|
||||
{value.lat}
|
||||
</div>
|
||||
+35
-5
@@ -524,7 +524,7 @@ select {
|
||||
border-top-left-radius: var(--baseRadius);
|
||||
border-top-right-radius: var(--baseRadius);
|
||||
& ~ %input,
|
||||
& ~ div %input {
|
||||
& ~ div > %input {
|
||||
border-top: 0;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 8px;
|
||||
@@ -545,7 +545,7 @@ select {
|
||||
background var(--baseAnimationSpeed),
|
||||
box-shadow var(--baseAnimationSpeed);
|
||||
}
|
||||
&:focus-within {
|
||||
&:focus-within:not(.form-field-list) {
|
||||
%input, label {
|
||||
background: var(--baseAlt2Color);
|
||||
}
|
||||
@@ -661,7 +661,7 @@ select {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
background: none !important;
|
||||
display: inline-flex;
|
||||
vertical-align: top;
|
||||
align-items: center;
|
||||
@@ -883,6 +883,33 @@ select {
|
||||
}
|
||||
}
|
||||
|
||||
.form-field-inline {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
> label {
|
||||
height: auto;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 0 5px 0 10px;
|
||||
padding-bottom: 0;
|
||||
white-space: nowrap;
|
||||
border-top-left-radius: var(--baseRadius);
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-left-radius: var(--baseRadius);
|
||||
border-bottom-right-radius: 0;
|
||||
~ input {
|
||||
padding-left: 5px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: var(--baseRadius);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: var(--baseRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// select field
|
||||
.select {
|
||||
position: relative;
|
||||
@@ -1155,7 +1182,7 @@ select {
|
||||
.form-field-list {
|
||||
border-radius: var(--baseRadius);
|
||||
transition: box-shadow var(--baseAnimationSpeed);
|
||||
label {
|
||||
> label {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.list {
|
||||
@@ -1196,9 +1223,12 @@ select {
|
||||
}
|
||||
}
|
||||
&:focus-within {
|
||||
.list, label {
|
||||
.list, %input:not(:focus), > label {
|
||||
background: var(--baseAlt1Color);
|
||||
}
|
||||
> label {
|
||||
color: var(--txtPrimaryColor);
|
||||
}
|
||||
}
|
||||
&.dragover:not(:has(.dragging)) {
|
||||
box-shadow: 0px 0px 0px 2px var(--warningColor);
|
||||
|
||||
@@ -1161,7 +1161,7 @@ export default class CommonHelper {
|
||||
* @return {String}
|
||||
*/
|
||||
static getFieldTypeIcon(type) {
|
||||
switch (type?.toLowerCase()) {
|
||||
switch (type) {
|
||||
case "primary":
|
||||
return "ri-key-line";
|
||||
case "text":
|
||||
@@ -1190,6 +1190,8 @@ export default class CommonHelper {
|
||||
return "ri-lock-password-line";
|
||||
case "autodate":
|
||||
return "ri-calendar-check-line";
|
||||
case "geoPoint":
|
||||
return "ri-map-pin-2-line";
|
||||
default:
|
||||
return "ri-star-s-line";
|
||||
}
|
||||
@@ -1774,7 +1776,12 @@ export default class CommonHelper {
|
||||
|
||||
const fields = collection.fields || [];
|
||||
for (const field of fields) {
|
||||
CommonHelper.pushUnique(result, prefix + field.name);
|
||||
if (field.type == "geoPoint") {
|
||||
CommonHelper.pushUnique(result, prefix + field.name + ".lon");
|
||||
CommonHelper.pushUnique(result, prefix + field.name + ".lat");
|
||||
} else {
|
||||
CommonHelper.pushUnique(result, prefix + field.name);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user