mirror of https://github.com/nocodb/nocodb
61 changed files with 2208 additions and 149 deletions
@ -0,0 +1,155 @@
|
||||
<script lang="ts" setup> |
||||
import type { GeoLocationType } from 'nocodb-sdk' |
||||
import { Modal as AModal, latLongToJoinedString, useVModel } from '#imports' |
||||
|
||||
interface Props { |
||||
modelValue?: string | null |
||||
} |
||||
|
||||
interface Emits { |
||||
(event: 'update:modelValue', model: GeoLocationType): void |
||||
} |
||||
|
||||
const props = defineProps<Props>() |
||||
|
||||
const emits = defineEmits<Emits>() |
||||
|
||||
const vModel = useVModel(props, 'modelValue', emits) |
||||
|
||||
let isExpanded = $ref(false) |
||||
|
||||
let isLoading = $ref(false) |
||||
|
||||
let isLocationSet = $ref(false) |
||||
|
||||
const [latitude, longitude] = (vModel.value || '').split(';') |
||||
|
||||
const latLongStr = computed(() => { |
||||
const [latitude, longitude] = (vModel.value || '').split(';') |
||||
if (latitude) isLocationSet = true |
||||
return latitude && longitude ? `${latitude}; ${longitude}` : 'Set location' |
||||
}) |
||||
|
||||
const formState = reactive({ |
||||
latitude, |
||||
longitude, |
||||
}) |
||||
|
||||
const handleFinish = () => { |
||||
vModel.value = latLongToJoinedString(parseFloat(formState.latitude), parseFloat(formState.longitude)) |
||||
isExpanded = false |
||||
} |
||||
|
||||
const clear = () => { |
||||
isExpanded = false |
||||
|
||||
formState.latitude = latitude |
||||
formState.longitude = longitude |
||||
} |
||||
|
||||
const onClickSetCurrentLocation = () => { |
||||
isLoading = true |
||||
const onSuccess = (position) => { |
||||
const crd = position.coords |
||||
formState.latitude = crd.latitude |
||||
formState.longitude = crd.longitude |
||||
isLoading = false |
||||
} |
||||
|
||||
const onError = (err) => { |
||||
console.error(`ERROR(${err.code}): ${err.message}`) |
||||
isLoading = false |
||||
} |
||||
|
||||
const options = { |
||||
enableHighAccuracy: true, |
||||
timeout: 20000, |
||||
maximumAge: 2000, |
||||
} |
||||
navigator.geolocation.getCurrentPosition(onSuccess, onError, options) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-dropdown :is="isExpanded ? AModal : 'div'" v-model:visible="isExpanded" trigger="click"> |
||||
<div |
||||
v-if="!isLocationSet" |
||||
class="group cursor-pointer flex gap-1 items-center mx-auto max-w-32 justify-center active:(ring ring-accent ring-opacity-100) rounded border-1 p-1 shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)" |
||||
> |
||||
<div class="flex items-center gap-2" data-testid="nc-geo-data-set-location-button"> |
||||
<MdiMapMarker class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]" /> |
||||
<div class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs"> |
||||
{{ latLongStr }} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div data-testid="nc-geo-data-lat-long-set" v-else>{{ latLongStr }}</div> |
||||
<template #overlay> |
||||
<a-form :model="formState" class="flex flex-col" @finish="handleFinish"> |
||||
<a-form-item> |
||||
<div class="flex mt-4 items-center mx-2"> |
||||
<div class="mr-2">{{ $t('labels.lat') }}:</div> |
||||
<a-input |
||||
v-model:value="formState.latitude" |
||||
data-testid="nc-geo-data-latitude" |
||||
type="number" |
||||
step="0.0000001" |
||||
:min="-90" |
||||
required |
||||
:max="90" |
||||
@keydown.stop |
||||
@selectstart.capture.stop |
||||
@mousedown.stop |
||||
/> |
||||
</div> |
||||
</a-form-item> |
||||
|
||||
<a-form-item> |
||||
<div class="flex items-center mx-2"> |
||||
<div class="mr-2">{{ $t('labels.lng') }}:</div> |
||||
<a-input |
||||
v-model:value="formState.longitude" |
||||
data-testid="nc-geo-data-longitude" |
||||
type="number" |
||||
step="0.0000001" |
||||
required |
||||
:min="-180" |
||||
:max="180" |
||||
@keydown.stop |
||||
@selectstart.capture.stop |
||||
@mousedown.stop |
||||
/> |
||||
</div> |
||||
</a-form-item> |
||||
<a-form-item> |
||||
<div class="flex items-center mr-2"> |
||||
<MdiReload v-if="isLoading" :class="{ 'animate-infinite animate-spin text-gray-500': isLoading }" /> |
||||
<a-button class="ml-2" @click="onClickSetCurrentLocation">{{ $t('labels.yourLocation') }}</a-button> |
||||
</div> |
||||
</a-form-item> |
||||
<a-form-item> |
||||
<div class="ml-auto mr-2"> |
||||
<a-button type="text" @click="clear">{{ $t('general.cancel') }}</a-button> |
||||
<a-button type="primary" html-type="submit" data-testid="nc-geo-data-save">{{ $t('general.submit') }}</a-button> |
||||
</div> |
||||
</a-form-item> |
||||
</a-form> |
||||
</template> |
||||
</a-dropdown> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
input[type='number']:focus { |
||||
@apply ring-transparent; |
||||
} |
||||
|
||||
input[type='number'] { |
||||
width: 180px; |
||||
} |
||||
.ant-form-item { |
||||
margin-bottom: 1rem; |
||||
} |
||||
.ant-dropdown-menu { |
||||
align-items: flex-end; |
||||
} |
||||
</style> |
@ -0,0 +1,42 @@
|
||||
<script setup lang="ts"> |
||||
import { |
||||
ActiveViewInj, |
||||
FieldsInj, |
||||
IsPublicInj, |
||||
MetaInj, |
||||
ReadonlyInj, |
||||
ReloadViewDataHookInj, |
||||
useProvideMapViewStore, |
||||
} from '#imports' |
||||
|
||||
const { sharedView, meta, sorts, nestedFilters } = useSharedView() |
||||
|
||||
const reloadEventHook = createEventHook() |
||||
|
||||
provide(ReloadViewDataHookInj, reloadEventHook) |
||||
|
||||
provide(ReadonlyInj, ref(true)) |
||||
|
||||
provide(MetaInj, meta) |
||||
|
||||
provide(ActiveViewInj, sharedView) |
||||
|
||||
provide(FieldsInj, ref(meta.value?.columns || [])) |
||||
|
||||
provide(IsPublicInj, ref(true)) |
||||
|
||||
useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters) |
||||
|
||||
useProvideMapViewStore(meta, sharedView, true) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="nc-container h-full mt-1.5 px-12"> |
||||
<div class="flex flex-col h-full flex-1 min-w-0"> |
||||
<LazySmartsheetToolbar /> |
||||
<div class="h-full flex-1 min-w-0 min-h-0 bg-gray-50"> |
||||
<LazySmartsheetMap /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -0,0 +1,266 @@
|
||||
<script lang="ts" setup> |
||||
import 'leaflet/dist/leaflet.css' |
||||
import L, { LatLng } from 'leaflet' |
||||
import 'leaflet.markercluster' |
||||
import { ViewTypes } from 'nocodb-sdk' |
||||
import { OpenNewRecordFormHookInj, latLongToJoinedString, onMounted, provide, ref, IsPublicInj } from '#imports' |
||||
import type { Row as RowType } from '~/lib' |
||||
|
||||
const route = useRoute() |
||||
|
||||
const router = useRouter() |
||||
|
||||
const reloadViewDataHook = inject(ReloadViewDataHookInj) |
||||
|
||||
const reloadViewMetaHook = inject(ReloadViewMetaHookInj) |
||||
|
||||
const { formattedData, loadMapData, loadMapMeta, mapMetaData, geoDataFieldColumn, addEmptyRow, syncCount, paginationData } = |
||||
useMapViewStoreOrThrow() |
||||
|
||||
const markersClusterGroupRef = ref<L.MarkerClusterGroup>() |
||||
|
||||
const mapContainerRef = ref<HTMLElement>() |
||||
|
||||
const myMapRef = ref<L.Map>() |
||||
|
||||
const isPublic = inject(IsPublicInj, ref(false)) |
||||
|
||||
const meta = inject(MetaInj, ref()) |
||||
|
||||
const view = inject(ActiveViewInj, ref()) |
||||
|
||||
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook()) |
||||
|
||||
const expandedFormDlg = ref(false) |
||||
|
||||
const expandedFormRow = ref<RowType>() |
||||
|
||||
const expandedFormRowState = ref<Record<string, any>>() |
||||
|
||||
const fallBackCenterLocation = { |
||||
lat: 51, |
||||
lng: 0.0, |
||||
} |
||||
|
||||
const getMapZoomLocalStorageKey = (viewId: string) => { |
||||
return `mapView.${viewId}.zoom` |
||||
} |
||||
const getMapCenterLocalStorageKey = (viewId: string) => `mapView.${viewId}.center` |
||||
|
||||
const expandForm = (row: RowType, state?: Record<string, any>) => { |
||||
const rowId = extractPkFromRow(row.row, meta.value!.columns!) |
||||
if (rowId) { |
||||
router.push({ |
||||
query: { |
||||
...route.query, |
||||
rowId, |
||||
}, |
||||
}) |
||||
} else { |
||||
expandedFormRow.value = row |
||||
expandedFormRowState.value = state |
||||
expandedFormDlg.value = true |
||||
} |
||||
} |
||||
|
||||
openNewRecordFormHook?.on(async () => { |
||||
const newRow = await addEmptyRow() |
||||
expandForm(newRow) |
||||
}) |
||||
|
||||
const expandedFormOnRowIdDlg = computed({ |
||||
get() { |
||||
return !!route.query.rowId |
||||
}, |
||||
set(val) { |
||||
if (!val) |
||||
router.push({ |
||||
query: { |
||||
...route.query, |
||||
rowId: undefined, |
||||
}, |
||||
}) |
||||
}, |
||||
}) |
||||
|
||||
const addMarker = (lat: number, long: number, row: RowType) => { |
||||
if (markersClusterGroupRef.value == null) { |
||||
throw new Error('Marker cluster is null') |
||||
} |
||||
const newMarker = L.marker([lat, long], { |
||||
alt: `${lat}, ${long}` |
||||
}).on('click', () => { |
||||
expandForm(row) |
||||
}) |
||||
markersClusterGroupRef.value?.addLayer(newMarker) |
||||
} |
||||
|
||||
const resetZoomAndCenterBasedOnLocalStorage = () => { |
||||
if (mapMetaData?.value?.fk_view_id == null) { |
||||
return |
||||
} |
||||
const initialZoomLevel = parseInt(localStorage.getItem(getMapZoomLocalStorageKey(mapMetaData.value.fk_view_id)) || '10') |
||||
const initialCenterLocalStorageStr = localStorage.getItem(getMapCenterLocalStorageKey(mapMetaData.value.fk_view_id)) |
||||
const initialCenter = initialCenterLocalStorageStr ? JSON.parse(initialCenterLocalStorageStr) : fallBackCenterLocation |
||||
|
||||
myMapRef?.value?.setView([initialCenter.lat, initialCenter.lng], initialZoomLevel) |
||||
} |
||||
|
||||
onBeforeMount(async () => { |
||||
await loadMapMeta() |
||||
await loadMapData() |
||||
}) |
||||
|
||||
onMounted(async () => { |
||||
const myMap = L.map(mapContainerRef.value!, { |
||||
center: new LatLng(10, 10), |
||||
zoom: 2, |
||||
}) |
||||
|
||||
myMapRef.value = myMap |
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { |
||||
maxZoom: 19, |
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>', |
||||
}).addTo(myMap) |
||||
|
||||
markersClusterGroupRef.value = L.markerClusterGroup({ |
||||
iconCreateFunction(cluster: { getChildCount: () => number }) { |
||||
return L.divIcon({ |
||||
html: `${cluster.getChildCount()}`, |
||||
className: 'bg-pink rounded-full flex items-center justify-center geo-map-marker-cluster', |
||||
iconSize: new L.Point(40, 40), |
||||
}) |
||||
}, |
||||
}) |
||||
|
||||
myMap.addLayer(markersClusterGroupRef.value) |
||||
|
||||
myMap.on('zoomend', function () { |
||||
if (localStorage != null && mapMetaData?.value?.fk_view_id) { |
||||
localStorage.setItem(getMapZoomLocalStorageKey(mapMetaData.value.fk_view_id), myMap.getZoom().toString()) |
||||
} |
||||
}) |
||||
|
||||
myMap.on('moveend', function () { |
||||
if (localStorage != null && mapMetaData?.value?.fk_view_id) { |
||||
localStorage.setItem(getMapCenterLocalStorageKey(mapMetaData?.value?.fk_view_id), JSON.stringify(myMap.getCenter())) |
||||
} |
||||
}) |
||||
|
||||
myMap.on('contextmenu', async function (e) { |
||||
const { lat, lng } = e.latlng |
||||
const newRow = await addEmptyRow() |
||||
if (geoDataFieldColumn.value?.title) { |
||||
newRow.row[geoDataFieldColumn.value.title] = latLongToJoinedString(lat, lng) |
||||
} |
||||
expandForm(newRow) |
||||
}) |
||||
}) |
||||
|
||||
reloadViewMetaHook?.on(async () => { |
||||
await loadMapMeta() |
||||
}) |
||||
|
||||
reloadViewDataHook?.on(async () => { |
||||
await loadMapData() |
||||
}) |
||||
|
||||
provide(ReloadRowDataHookInj, reloadViewDataHook) |
||||
|
||||
watch([formattedData, mapMetaData, markersClusterGroupRef], () => { |
||||
if (formattedData.value == null || mapMetaData.value?.fk_view_id == null || markersClusterGroupRef.value == null) { |
||||
return |
||||
} |
||||
|
||||
resetZoomAndCenterBasedOnLocalStorage() |
||||
|
||||
markersClusterGroupRef.value?.clearLayers() |
||||
|
||||
formattedData.value?.forEach((row) => { |
||||
const primaryGeoDataColumnTitle = geoDataFieldColumn.value?.title |
||||
|
||||
if (primaryGeoDataColumnTitle == null) { |
||||
throw new Error('Cannot find primary geo data column title') |
||||
} |
||||
|
||||
const primaryGeoDataValue = row.row[primaryGeoDataColumnTitle] |
||||
if (primaryGeoDataValue == null) { |
||||
return |
||||
} |
||||
|
||||
const [lat, long] = primaryGeoDataValue.split(';').map(parseFloat) |
||||
|
||||
addMarker(lat, long, row) |
||||
}) |
||||
}) |
||||
|
||||
watch(view, async (nextView) => { |
||||
if (nextView?.type === ViewTypes.MAP) { |
||||
await loadMapMeta() |
||||
await loadMapData() |
||||
} |
||||
}) |
||||
|
||||
const count = computed(() => paginationData.value.totalRows) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex flex-col h-full w-full no-underline" data-testid="nc-map-wrapper"> |
||||
<div id="mapContainer" ref="mapContainerRef" class="w-full h-screen"> |
||||
<a-tooltip placement="bottom" class="h-2 w-auto max-w-fit-content absolute top-3 right-3 p-2 z-500 cursor-default"> |
||||
<template #title> |
||||
<span v-if="count > 1000"> {{ $t('msg.info.map.overLimit') }} </span> |
||||
<span v-else-if="count > 900"> {{ $t('msg.info.map.closeLimit') }} </span> |
||||
<span> {{ $t('msg.info.map.limitNumber') }} </span> |
||||
</template> |
||||
|
||||
<div v-if="count > 900" class="nc-warning-info flex min-w-32px h-32px items-center gap-1 px-2 bg-white"> |
||||
<div>{{ count }} {{ $t('objects.records') }}</div> |
||||
<mdi-map-marker-alert /> |
||||
</div> |
||||
</a-tooltip> |
||||
</div> |
||||
</div> |
||||
<Suspense v-if="!isPublic"> |
||||
<LazySmartsheetExpandedForm |
||||
v-if="expandedFormRow && expandedFormDlg" |
||||
v-model="expandedFormDlg" |
||||
:row="expandedFormRow" |
||||
:state="expandedFormRowState" |
||||
:meta="meta" |
||||
:view="view" |
||||
/> |
||||
</Suspense> |
||||
<Suspense v-if="!isPublic"> |
||||
<LazySmartsheetExpandedForm |
||||
v-if="expandedFormOnRowIdDlg" |
||||
:key="route.query.rowId" |
||||
v-model="expandedFormOnRowIdDlg" |
||||
:row="{ row: {}, oldRow: {}, rowMeta: {} }" |
||||
:meta="meta" |
||||
:row-id="route.query.rowId" |
||||
:view="view" |
||||
/> |
||||
</Suspense> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
:global(.geo-map-marker-cluster) { |
||||
background-color: pink; |
||||
border-radius: 50%; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
} |
||||
</style> |
||||
|
||||
<style> |
||||
.no-underline a { |
||||
text-decoration: none !important; |
||||
} |
||||
.leaflet-popup-content-wrapper { |
||||
max-height: 255px; |
||||
overflow: scroll; |
||||
} |
||||
</style> |
@ -0,0 +1,15 @@
|
||||
<script setup lang="ts"> |
||||
function toggleGeodataFeature() { |
||||
geodataToggleState.show = !geodataToggleState.show |
||||
localStorage.setItem('geodataToggleState', JSON.stringify(geodataToggleState.show)) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-tooltip placement="bottomRight"> |
||||
<template #title> |
||||
<span> Toggle GeoData </span> |
||||
</template> |
||||
<mdi-map-marker class="cursor-pointer" data-testid="toggle-geodata-feature-icon" @click="toggleGeodataFeature" /> |
||||
</a-tooltip> |
||||
</template> |
@ -0,0 +1,113 @@
|
||||
<script setup lang="ts"> |
||||
import type { MapType } from 'nocodb-sdk' |
||||
import { UITypes } from 'nocodb-sdk' |
||||
import type { SelectProps } from 'ant-design-vue' |
||||
import { |
||||
ActiveViewInj, |
||||
IsLockedInj, |
||||
MetaInj, |
||||
ReloadViewDataHookInj, |
||||
computed, |
||||
inject, |
||||
ref, |
||||
useViewColumns, |
||||
watch, |
||||
} from '#imports' |
||||
|
||||
const { eventBus } = useSmartsheetStoreOrThrow() |
||||
|
||||
const meta = inject(MetaInj, ref()) |
||||
|
||||
const activeView = inject(ActiveViewInj, ref()) |
||||
|
||||
const reloadDataHook = inject(ReloadViewDataHookInj)! |
||||
|
||||
const isLocked = inject(IsLockedInj, ref(false)) |
||||
|
||||
const { fields, loadViewColumns, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger()) |
||||
|
||||
const { loadMapData, loadMapMeta, updateMapMeta, mapMetaData, geoDataFieldColumn } = useMapViewStoreOrThrow() |
||||
|
||||
const mappedByDropdown = ref(false) |
||||
|
||||
watch( |
||||
() => activeView.value?.id, |
||||
async (newVal, oldVal) => { |
||||
if (newVal !== oldVal && meta.value) { |
||||
await loadViewColumns() |
||||
} |
||||
}, |
||||
{ immediate: true }, |
||||
) |
||||
|
||||
const geoDataMappingFieldColumnId = computed({ |
||||
get: () => mapMetaData.value.fk_geo_data_col_id, |
||||
set: async (val) => { |
||||
if (val) { |
||||
await updateMapMeta({ |
||||
fk_geo_data_col_id: val, |
||||
}) |
||||
await loadMapMeta() |
||||
await loadMapData() |
||||
;(activeView.value?.view as MapType).fk_geo_data_col_id = val |
||||
eventBus.emit(SmartsheetStoreEvents.MAPPED_BY_COLUMN_CHANGE) |
||||
} |
||||
}, |
||||
}) |
||||
|
||||
const geoDataFieldOptions = computed<SelectProps['options']>(() => { |
||||
return fields.value |
||||
?.filter((el) => el.fk_column_id && metaColumnById.value[el.fk_column_id].uidt === UITypes.GeoData) |
||||
.map((field) => { |
||||
return { |
||||
value: field.fk_column_id, |
||||
label: field.title, |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
const handleChange = () => { |
||||
mappedByDropdown.value = false |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-dropdown v-model:visible="mappedByDropdown" :trigger="['click']"> |
||||
<div class="nc-map-btn"> |
||||
<a-button v-e="['c:map:change-grouping-field']" class="nc-map-stacked-by-menu-btn nc-toolbar-btn" :disabled="isLocked"> |
||||
<div class="flex items-center gap-1"> |
||||
<mdi-arrow-down-drop-circle-outline /> |
||||
<span class="text-capitalize !text-sm font-weight-normal"> |
||||
{{ $t('activity.map.mappedBy') }} |
||||
<span class="font-bold">{{ geoDataFieldColumn?.title }}</span> |
||||
</span> |
||||
<MdiMenuDown class="text-grey" /> |
||||
</div> |
||||
</a-button> |
||||
</div> |
||||
<template #overlay> |
||||
<div |
||||
v-if="mappedByDropdown" |
||||
class="p-3 min-w-[280px] bg-gray-50 shadow-lg nc-table-toolbar-menu max-h-[max(80vh,500px)] overflow-auto !border" |
||||
@click.stop |
||||
> |
||||
<div> |
||||
<span class="font-bold"> {{ $t('activity.map.chooseMappingField') }}</span> |
||||
<a-divider class="!my-2" /> |
||||
</div> |
||||
<div class="nc-fields-list py-1"> |
||||
<div class="grouping-field"> |
||||
<a-select |
||||
v-model:value="geoDataMappingFieldColumnId" |
||||
class="w-full nc-msp-grouping-field-select" |
||||
:options="geoDataFieldOptions" |
||||
placeholder="Select a Mapping Field" |
||||
@change="handleChange" |
||||
@click.stop |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</a-dropdown> |
||||
</template> |
@ -0,0 +1,182 @@
|
||||
import { reactive } from 'vue' |
||||
import type { ComputedRef, Ref } from 'vue' |
||||
import type { ColumnType, MapType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' |
||||
import { IsPublicInj, ref, useInjectionState, useMetas, useProject } from '#imports' |
||||
import type { Row } from '~/lib' |
||||
|
||||
const storedValue = localStorage.getItem('geodataToggleState') |
||||
|
||||
const initialState = storedValue ? JSON.parse(storedValue) : false |
||||
|
||||
export const geodataToggleState = reactive({ show: initialState }) |
||||
|
||||
const formatData = (list: Record<string, any>[]) => |
||||
list.map( |
||||
(row) => |
||||
({ |
||||
row: { ...row }, |
||||
oldRow: { ...row }, |
||||
rowMeta: {}, |
||||
} as Row), |
||||
) |
||||
|
||||
const [useProvideMapViewStore, useMapViewStore] = useInjectionState( |
||||
( |
||||
meta: Ref<TableType | undefined>, |
||||
viewMeta: Ref<ViewType | MapType | undefined> | ComputedRef<(ViewType & { id: string }) | undefined>, |
||||
shared = false, |
||||
where?: ComputedRef<string | undefined>, |
||||
) => { |
||||
if (!meta) { |
||||
throw new Error('Table meta is not available') |
||||
} |
||||
|
||||
const formattedData = ref<Row[]>([]) |
||||
|
||||
const { api } = useApi() |
||||
|
||||
const { project } = useProject() |
||||
|
||||
const { $api } = useNuxtApp() |
||||
|
||||
const { isUIAllowed } = useUIPermission() |
||||
|
||||
const isPublic = ref(shared) || inject(IsPublicInj, ref(false)) |
||||
|
||||
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow() |
||||
|
||||
const { fetchSharedViewData } = useSharedView() |
||||
|
||||
const mapMetaData = ref<MapType>({}) |
||||
|
||||
const geoDataFieldColumn = ref<ColumnType | undefined>() |
||||
|
||||
const defaultPageSize = 1000 |
||||
|
||||
const paginationData = ref<PaginatedType>({ page: 1, pageSize: defaultPageSize }) |
||||
|
||||
const queryParams = computed(() => ({ |
||||
limit: paginationData.value.pageSize ?? defaultPageSize, |
||||
where: where?.value ?? '', |
||||
})) |
||||
|
||||
async function syncCount() { |
||||
const { count } = await $api.dbViewRow.count( |
||||
NOCO, |
||||
project?.value?.title as string, |
||||
meta?.value?.id as string, |
||||
viewMeta?.value?.id as string, |
||||
) |
||||
paginationData.value.totalRows = count |
||||
} |
||||
|
||||
async function loadMapMeta() { |
||||
if (!viewMeta?.value?.id || !meta?.value?.columns) return |
||||
mapMetaData.value = await $api.dbView.mapRead(viewMeta.value.id) |
||||
geoDataFieldColumn.value = |
||||
(meta.value.columns as ColumnType[]).filter((f) => f.id === mapMetaData.value.fk_geo_data_col_id)[0] || {} |
||||
} |
||||
|
||||
async function loadMapData() { |
||||
if ((!project?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic?.value) return |
||||
|
||||
const res = !isPublic.value |
||||
? await api.dbViewRow.list('noco', project.value.id!, meta.value!.id!, viewMeta.value!.id!, { |
||||
...queryParams.value, |
||||
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), |
||||
where: where?.value, |
||||
}) |
||||
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value }) |
||||
|
||||
formattedData.value = formatData(res.list) |
||||
} |
||||
|
||||
async function updateMapMeta(updateObj: Partial<MapType>) { |
||||
if (!viewMeta?.value?.id || !isUIAllowed('xcDatatableEditable')) return |
||||
await $api.dbView.mapUpdate(viewMeta.value.id, { |
||||
...mapMetaData.value, |
||||
...updateObj, |
||||
}) |
||||
} |
||||
|
||||
const { getMeta } = useMetas() |
||||
|
||||
async function insertRow( |
||||
currentRow: Row, |
||||
ltarState: Record<string, any> = {}, |
||||
{ |
||||
metaValue = meta.value, |
||||
viewMetaValue = viewMeta.value, |
||||
}: { metaValue?: MapType; viewMetaValue?: ViewType | MapType } = {}, |
||||
) { |
||||
const row = currentRow.row |
||||
if (currentRow.rowMeta) currentRow.rowMeta.saving = true |
||||
try { |
||||
const { missingRequiredColumns, insertObj } = await populateInsertObject({ |
||||
meta: metaValue!, |
||||
ltarState, |
||||
getMeta, |
||||
row, |
||||
}) |
||||
|
||||
if (missingRequiredColumns.size) return |
||||
|
||||
const insertedData = await $api.dbViewRow.create( |
||||
NOCO, |
||||
project?.value.id as string, |
||||
metaValue?.id as string, |
||||
viewMetaValue?.id as string, |
||||
insertObj, |
||||
) |
||||
|
||||
Object.assign(currentRow, { |
||||
row: { ...insertedData, ...row }, |
||||
rowMeta: { ...(currentRow.rowMeta || {}), new: undefined }, |
||||
oldRow: { ...insertedData }, |
||||
}) |
||||
|
||||
syncCount() |
||||
|
||||
return insertedData |
||||
} catch (error: any) { |
||||
message.error(await extractSdkResponseErrorMsg(error)) |
||||
} finally { |
||||
if (currentRow.rowMeta) currentRow.rowMeta.saving = false |
||||
} |
||||
} |
||||
|
||||
function addEmptyRow(addAfter = formattedData.value.length) { |
||||
formattedData.value.splice(addAfter, 0, { |
||||
row: {}, |
||||
oldRow: {}, |
||||
rowMeta: { new: true }, |
||||
}) |
||||
|
||||
return formattedData.value[addAfter] |
||||
} |
||||
|
||||
return { |
||||
formattedData, |
||||
loadMapData, |
||||
loadMapMeta, |
||||
updateMapMeta, |
||||
mapMetaData, |
||||
geoDataFieldColumn, |
||||
addEmptyRow, |
||||
insertRow, |
||||
geodataToggleState, |
||||
syncCount, |
||||
paginationData, |
||||
} |
||||
}, |
||||
) |
||||
|
||||
export { useProvideMapViewStore } |
||||
|
||||
export function useMapViewStoreOrThrow() { |
||||
const mapViewStore = useMapViewStore() |
||||
|
||||
if (mapViewStore == null) throw new Error('Please call `useProvideMapViewStore` on the appropriate parent component') |
||||
|
||||
return mapViewStore |
||||
} |
@ -0,0 +1,35 @@
|
||||
<script setup lang="ts"> |
||||
import { message } from 'ant-design-vue' |
||||
import { definePageMeta } from '#imports' |
||||
|
||||
definePageMeta({ |
||||
public: true, |
||||
requiresAuth: false, |
||||
layout: 'shared-view', |
||||
}) |
||||
|
||||
const route = useRoute() |
||||
|
||||
const { loadSharedView } = useSharedView() |
||||
|
||||
const showPassword = ref(false) |
||||
|
||||
try { |
||||
await loadSharedView(route.params.viewId as string) |
||||
} catch (e: any) { |
||||
if (e?.response?.status === 403) { |
||||
showPassword.value = true |
||||
} else { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<NuxtLayout class="flex" name="shared-view"> |
||||
<div v-if="showPassword"> |
||||
<LazySharedViewAskPassword v-model="showPassword" /> |
||||
</div> |
||||
<LazySharedViewMap v-else /> |
||||
</NuxtLayout> |
||||
</template> |
@ -0,0 +1,3 @@
|
||||
const latLongToJoinedString = (lat: number, long: number) => `${lat.toFixed(7)};${long.toFixed(7)}` |
||||
|
||||
export { latLongToJoinedString } |
@ -0,0 +1,46 @@
|
||||
import { Request, Response, Router } from 'express'; |
||||
import { MapType, ViewTypes } from 'nocodb-sdk'; |
||||
import View from '../../models/View'; |
||||
import ncMetaAclMw from '../helpers/ncMetaAclMw'; |
||||
import { Tele } from 'nc-help'; |
||||
import { metaApiMetrics } from '../helpers/apiMetrics'; |
||||
import MapView from '../../models/MapView'; |
||||
|
||||
export async function mapViewGet(req: Request, res: Response<MapType>) { |
||||
res.json(await MapView.get(req.params.mapViewId)); |
||||
} |
||||
|
||||
export async function mapViewCreate(req: Request<any, any>, res) { |
||||
Tele.emit('evt', { evt_type: 'vtable:created', show_as: 'map' }); |
||||
const view = await View.insert({ |
||||
...req.body, |
||||
// todo: sanitize
|
||||
fk_model_id: req.params.tableId, |
||||
type: ViewTypes.MAP, |
||||
}); |
||||
res.json(view); |
||||
} |
||||
|
||||
export async function mapViewUpdate(req, res) { |
||||
Tele.emit('evt', { evt_type: 'view:updated', type: 'map' }); |
||||
res.json(await MapView.update(req.params.mapViewId, req.body)); |
||||
} |
||||
|
||||
const router = Router({ mergeParams: true }); |
||||
|
||||
router.post( |
||||
'/api/v1/db/meta/tables/:tableId/maps', |
||||
metaApiMetrics, |
||||
ncMetaAclMw(mapViewCreate, 'mapViewCreate') |
||||
); |
||||
router.patch( |
||||
'/api/v1/db/meta/maps/:mapViewId', |
||||
metaApiMetrics, |
||||
ncMetaAclMw(mapViewUpdate, 'mapViewUpdate') |
||||
); |
||||
router.get( |
||||
'/api/v1/db/meta/maps/:mapViewId', |
||||
metaApiMetrics, |
||||
ncMetaAclMw(mapViewGet, 'mapViewGet') |
||||
); |
||||
export default router; |
@ -0,0 +1,53 @@
|
||||
import { MetaTable } from '../../utils/globals'; |
||||
|
||||
const up = async (knex) => { |
||||
await knex.schema.createTable(MetaTable.MAP_VIEW, (table) => { |
||||
table.string('fk_view_id', 20).primary(); |
||||
table.foreign('fk_view_id').references(`${MetaTable.VIEWS}.id`); |
||||
|
||||
table.string('base_id', 20); |
||||
table.foreign('base_id').references(`${MetaTable.BASES}.id`); |
||||
|
||||
table.string('project_id', 128); |
||||
table.foreign('project_id').references(`${MetaTable.PROJECT}.id`); |
||||
|
||||
table.string('uuid'); |
||||
table.string('title'); |
||||
|
||||
table.string('fk_geo_data_col_id', 20); |
||||
table.foreign('fk_geo_data_col_id').references(`${MetaTable.COLUMNS}.id`); |
||||
|
||||
table.text('meta'); |
||||
|
||||
table.dateTime('created_at'); |
||||
table.dateTime('updated_at'); |
||||
}); |
||||
await knex.schema.createTable(MetaTable.MAP_VIEW_COLUMNS, (table) => { |
||||
table.string('id', 20).primary().notNullable(); |
||||
|
||||
table.string('base_id', 20); |
||||
table.string('project_id', 128); |
||||
|
||||
table.string('fk_view_id', 20); |
||||
table.foreign('fk_view_id').references(`${MetaTable.MAP_VIEW}.fk_view_id`); |
||||
table.string('fk_column_id', 20); |
||||
table.foreign('fk_column_id').references(`${MetaTable.COLUMNS}.id`); |
||||
|
||||
table.string('uuid'); |
||||
|
||||
table.string('label'); |
||||
table.string('help'); |
||||
|
||||
table.boolean('show'); |
||||
table.float('order'); |
||||
|
||||
table.timestamps(true, true); |
||||
}); |
||||
}; |
||||
|
||||
const down = async (knex) => { |
||||
await knex.schema.dropTable(MetaTable.MAP_VIEW); |
||||
await knex.schema.dropTable(MetaTable.MAP_VIEW_COLUMNS); |
||||
}; |
||||
|
||||
export { up, down }; |
@ -0,0 +1,104 @@
|
||||
import Noco from '../Noco'; |
||||
import { MapType } from 'nocodb-sdk'; |
||||
import { CacheGetType, CacheScope, MetaTable } from '../utils/globals'; |
||||
import View from './View'; |
||||
import NocoCache from '../cache/NocoCache'; |
||||
import MapViewColumn from './MapViewColumn'; |
||||
|
||||
export default class MapView implements MapType { |
||||
fk_view_id: string; |
||||
title: string; |
||||
project_id?: string; |
||||
base_id?: string; |
||||
fk_geo_data_col_id?: string; |
||||
meta?: string | Record<string, unknown>; |
||||
|
||||
// below fields are not in use at this moment
|
||||
// keep them for time being
|
||||
show?: boolean; |
||||
uuid?: string; |
||||
public?: boolean; |
||||
password?: string; |
||||
show_all_fields?: boolean; |
||||
|
||||
constructor(data: MapView) { |
||||
Object.assign(this, data); |
||||
} |
||||
|
||||
public static async get(viewId: string, ncMeta = Noco.ncMeta) { |
||||
let view = |
||||
viewId && |
||||
(await NocoCache.get( |
||||
`${CacheScope.MAP_VIEW}:${viewId}`, |
||||
CacheGetType.TYPE_OBJECT |
||||
)); |
||||
if (!view) { |
||||
view = await ncMeta.metaGet2(null, null, MetaTable.MAP_VIEW, { |
||||
fk_view_id: viewId, |
||||
}); |
||||
await NocoCache.set(`${CacheScope.MAP_VIEW}:${viewId}`, view); |
||||
} |
||||
|
||||
return view && new MapView(view); |
||||
} |
||||
|
||||
static async insert(view: Partial<MapView>, ncMeta = Noco.ncMeta) { |
||||
const insertObj = { |
||||
project_id: view.project_id, |
||||
base_id: view.base_id, |
||||
fk_view_id: view.fk_view_id, |
||||
fk_geo_data_col_id: view.fk_geo_data_col_id, |
||||
meta: view.meta, |
||||
}; |
||||
|
||||
const viewRef = await View.get(view.fk_view_id); |
||||
|
||||
if (!(view.project_id && view.base_id)) { |
||||
insertObj.project_id = viewRef.project_id; |
||||
insertObj.base_id = viewRef.base_id; |
||||
} |
||||
|
||||
await ncMeta.metaInsert2(null, null, MetaTable.MAP_VIEW, insertObj, true); |
||||
|
||||
return this.get(view.fk_view_id, ncMeta); |
||||
} |
||||
|
||||
static async update( |
||||
mapId: string, |
||||
body: Partial<MapView>, |
||||
ncMeta = Noco.ncMeta |
||||
) { |
||||
// get existing cache
|
||||
const key = `${CacheScope.MAP_VIEW}:${mapId}`; |
||||
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT); |
||||
const updateObj = { |
||||
...body, |
||||
meta: |
||||
typeof body.meta === 'string' |
||||
? body.meta |
||||
: JSON.stringify(body.meta ?? {}), |
||||
}; |
||||
if (o) { |
||||
o = { ...o, ...updateObj }; |
||||
// set cache
|
||||
await NocoCache.set(key, o); |
||||
} |
||||
|
||||
if (body.fk_geo_data_col_id != null) { |
||||
const mapViewColumns = await MapViewColumn.list(mapId); |
||||
const mapViewMappedByColumn = mapViewColumns.find( |
||||
(mapViewColumn) => |
||||
mapViewColumn.fk_column_id === body.fk_geo_data_col_id |
||||
); |
||||
await View.updateColumn(body.fk_view_id, mapViewMappedByColumn.id, { |
||||
show: true, |
||||
}); |
||||
|
||||
} |
||||
|
||||
// update meta
|
||||
return await ncMeta.metaUpdate(null, null, MetaTable.MAP_VIEW, updateObj, { |
||||
fk_view_id: mapId, |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,106 @@
|
||||
import Noco from '../Noco'; |
||||
import { CacheGetType, CacheScope, MetaTable } from '../utils/globals'; |
||||
import View from './View'; |
||||
import NocoCache from '../cache/NocoCache'; |
||||
|
||||
export default class MapViewColumn { |
||||
id: string; |
||||
title?: string; |
||||
show?: boolean; |
||||
order?: number; |
||||
|
||||
fk_view_id: string; |
||||
fk_column_id: string; |
||||
project_id?: string; |
||||
base_id?: string; |
||||
|
||||
constructor(data: MapViewColumn) { |
||||
Object.assign(this, data); |
||||
} |
||||
|
||||
public static async get(mapViewColumnId: string, ncMeta = Noco.ncMeta) { |
||||
let view = |
||||
mapViewColumnId && |
||||
(await NocoCache.get( |
||||
`${CacheScope.MAP_VIEW_COLUMN}:${mapViewColumnId}`, |
||||
CacheGetType.TYPE_OBJECT |
||||
)); |
||||
if (!view) { |
||||
view = await ncMeta.metaGet2( |
||||
null, |
||||
null, |
||||
MetaTable.MAP_VIEW_COLUMNS, |
||||
mapViewColumnId |
||||
); |
||||
await NocoCache.set( |
||||
`${CacheScope.MAP_VIEW_COLUMN}:${mapViewColumnId}`, |
||||
view |
||||
); |
||||
} |
||||
return view && new MapViewColumn(view); |
||||
} |
||||
static async insert(column: Partial<MapViewColumn>, ncMeta = Noco.ncMeta) { |
||||
const insertObj = { |
||||
fk_view_id: column.fk_view_id, |
||||
fk_column_id: column.fk_column_id, |
||||
order: await ncMeta.metaGetNextOrder(MetaTable.MAP_VIEW_COLUMNS, { |
||||
fk_view_id: column.fk_view_id, |
||||
}), |
||||
show: column.show, |
||||
project_id: column.project_id, |
||||
base_id: column.base_id, |
||||
}; |
||||
|
||||
if (!(column.project_id && column.base_id)) { |
||||
const viewRef = await View.get(column.fk_view_id, ncMeta); |
||||
insertObj.project_id = viewRef.project_id; |
||||
insertObj.base_id = viewRef.base_id; |
||||
} |
||||
|
||||
const { id, fk_column_id } = await ncMeta.metaInsert2( |
||||
null, |
||||
null, |
||||
MetaTable.MAP_VIEW_COLUMNS, |
||||
insertObj |
||||
); |
||||
|
||||
await NocoCache.set(`${CacheScope.MAP_VIEW_COLUMN}:${fk_column_id}`, id); |
||||
|
||||
// if cache is not present skip pushing it into the list to avoid unexpected behaviour
|
||||
if ( |
||||
(await NocoCache.getList(CacheScope.MAP_VIEW_COLUMN, [column.fk_view_id])) |
||||
?.length |
||||
) |
||||
await NocoCache.appendToList( |
||||
CacheScope.MAP_VIEW_COLUMN, |
||||
[column.fk_view_id], |
||||
`${CacheScope.MAP_VIEW_COLUMN}:${id}` |
||||
); |
||||
|
||||
return this.get(id, ncMeta); |
||||
} |
||||
|
||||
public static async list( |
||||
viewId: string, |
||||
ncMeta = Noco.ncMeta |
||||
): Promise<MapViewColumn[]> { |
||||
let views = await NocoCache.getList(CacheScope.MAP_VIEW_COLUMN, [viewId]); |
||||
if (!views.length) { |
||||
views = await ncMeta.metaList2(null, null, MetaTable.MAP_VIEW_COLUMNS, { |
||||
condition: { |
||||
fk_view_id: viewId, |
||||
}, |
||||
orderBy: { |
||||
order: 'asc', |
||||
}, |
||||
}); |
||||
await NocoCache.setList(CacheScope.MAP_VIEW_COLUMN, [viewId], views); |
||||
} |
||||
views.sort( |
||||
(a, b) => |
||||
(a.order != null ? a.order : Infinity) - |
||||
(b.order != null ? b.order : Infinity) |
||||
); |
||||
return views?.map((v) => new MapViewColumn(v)); |
||||
} |
||||
} |
@ -0,0 +1,51 @@
|
||||
import { expect } from '@playwright/test'; |
||||
import { DashboardPage } from '..'; |
||||
import BasePage from '../../Base'; |
||||
import { ToolbarPage } from '../common/Toolbar'; |
||||
|
||||
export class MapPage extends BasePage { |
||||
readonly dashboard: DashboardPage; |
||||
readonly toolbar: ToolbarPage; |
||||
|
||||
constructor(dashboard: DashboardPage) { |
||||
super(dashboard.rootPage); |
||||
this.dashboard = dashboard; |
||||
this.toolbar = new ToolbarPage(this); |
||||
} |
||||
|
||||
get() { |
||||
return this.dashboard.get().locator('[data-testid="nc-map-wrapper"]'); |
||||
} |
||||
|
||||
async marker(lat: string, long: string) { |
||||
const latLongStr = `${lat}, ${long}`; |
||||
const marker = await this.get().locator(`.leaflet-marker-pane img[alt="${latLongStr}"]`); |
||||
return marker; |
||||
} |
||||
|
||||
async clickAddRowButton() { |
||||
await this.rootPage.locator('.nc-add-new-row-btn').click(); |
||||
} |
||||
|
||||
async clickMarker(lat: string, long: string) { |
||||
return (await this.marker(lat, long)).click(); |
||||
} |
||||
|
||||
async verifyMarkerCount(count: number) { |
||||
const markers = await this.get().locator('.leaflet-marker-pane img'); |
||||
await expect(markers).toHaveCount(count); |
||||
} |
||||
|
||||
async zoomOut(times = 10) { |
||||
const zoomOutButton = await this.get().locator('.leaflet-control-zoom-out'); |
||||
for (let i = 0; i < times; i++) { |
||||
await zoomOutButton.click(); |
||||
await this.rootPage.waitForTimeout(400); |
||||
} |
||||
} |
||||
|
||||
// todo: Wait for render to complete
|
||||
async waitLoading() { |
||||
await this.rootPage.waitForTimeout(1000); |
||||
} |
||||
} |
@ -0,0 +1,36 @@
|
||||
import { CellPageObject } from '.'; |
||||
import BasePage from '../../../Base'; |
||||
|
||||
export class GeoDataCellPageObject extends BasePage { |
||||
readonly cell: CellPageObject; |
||||
|
||||
constructor(cell: CellPageObject) { |
||||
super(cell.rootPage); |
||||
this.cell = cell; |
||||
} |
||||
|
||||
get({ index, columnHeader }: { index?: number; columnHeader: string }) { |
||||
return this.cell.get({ index, columnHeader }); |
||||
} |
||||
|
||||
async openSetLocation({ index, columnHeader }: { index: number; columnHeader: string }) { |
||||
await this.cell.get({ index, columnHeader }).locator(`[data-testid="nc-geo-data-set-location-button"]`).click(); |
||||
} |
||||
|
||||
async openLatLngSet({ index, columnHeader }: { index: number; columnHeader: string }) { |
||||
await this.cell.get({ index, columnHeader }).locator(`[data-testid="nc-geo-data-lat-long-set"]`).click(); |
||||
} |
||||
|
||||
async enterLatLong({ lat, long }: { lat: string; long: string }) { |
||||
await this.rootPage.locator(`[data-testid="nc-geo-data-latitude"]`).fill(lat); |
||||
await this.rootPage.locator(`[data-testid="nc-geo-data-longitude"]`).fill(long); |
||||
} |
||||
|
||||
async clickSave() { |
||||
await this.rootPage.locator(`[data-testid="nc-geo-data-save"]`).click(); |
||||
} |
||||
|
||||
async close() { |
||||
await this.rootPage.keyboard.press('Escape'); |
||||
} |
||||
} |
@ -0,0 +1,73 @@
|
||||
import { test } from '@playwright/test'; |
||||
import { DashboardPage } from '../pages/Dashboard'; |
||||
import setup from '../setup'; |
||||
import { GridPage } from '../pages/Dashboard/Grid'; |
||||
|
||||
test.describe('Geo Data column', () => { |
||||
let dashboard: DashboardPage; |
||||
let grid: GridPage; |
||||
let context: any; |
||||
|
||||
test.beforeEach(async ({ page }) => { |
||||
context = await setup({ page }); |
||||
dashboard = new DashboardPage(page, context.project); |
||||
grid = dashboard.grid; |
||||
}); |
||||
|
||||
test('creation, validation and deleting geo data column', async () => { |
||||
// close 'Team & Auth' tab
|
||||
await dashboard.closeTab({ title: 'Team & Auth' }); |
||||
|
||||
await dashboard.treeView.openTable({ title: 'City' }); |
||||
|
||||
await dashboard.viewSidebar.activateGeoDataEasterEgg(); |
||||
|
||||
await grid.column.create({ |
||||
title: 'GeoData1', |
||||
type: 'GeoData', |
||||
}); |
||||
|
||||
await grid.column.verify({ title: 'GeoData1', isVisible: true }); |
||||
|
||||
await grid.cell.geoData.openSetLocation({ |
||||
index: 0, |
||||
columnHeader: 'GeoData1', |
||||
}); |
||||
await grid.cell.geoData.enterLatLong({ |
||||
lat: '50.4501', |
||||
long: '30.5234', |
||||
}); |
||||
await grid.cell.geoData.clickSave(); |
||||
|
||||
await grid.cell.verifyGeoDataCell({ |
||||
index: 0, |
||||
columnHeader: 'GeoData1', |
||||
lat: '50.4501000', |
||||
long: '30.5234000', |
||||
}); |
||||
|
||||
// Trying to change to value that is not valid
|
||||
await grid.cell.geoData.openLatLngSet({ |
||||
index: 0, |
||||
columnHeader: 'GeoData1', |
||||
}); |
||||
await grid.cell.geoData.enterLatLong({ |
||||
lat: '543210.4501', |
||||
long: '30.5234', |
||||
}); |
||||
await grid.cell.geoData.clickSave(); |
||||
|
||||
// value should not be changed
|
||||
await grid.cell.verifyGeoDataCell({ |
||||
index: 0, |
||||
columnHeader: 'GeoData1', |
||||
lat: '50.4501000', |
||||
long: '30.5234000', |
||||
}); |
||||
|
||||
await grid.column.delete({ title: 'GeoData1' }); |
||||
await grid.column.verify({ title: 'GeoData1', isVisible: false }); |
||||
|
||||
await dashboard.closeTab({ title: 'City' }); |
||||
}); |
||||
}); |
@ -0,0 +1,92 @@
|
||||
import { test } from '@playwright/test'; |
||||
import { DashboardPage } from '../pages/Dashboard'; |
||||
import { ToolbarPage } from '../pages/Dashboard/common/Toolbar'; |
||||
|
||||
import setup from '../setup'; |
||||
|
||||
test.describe('Map View', () => { |
||||
let dashboard: DashboardPage, toolbar: ToolbarPage; |
||||
let context: any; |
||||
|
||||
const latitudeInFullDecimalLength = '50.4501000'; |
||||
const longitudeInFullDecimalLength = '30.5234000'; |
||||
|
||||
const latitudeInShortDecimalLength = '50.4501'; |
||||
const longitudeInShortDecimalLength = '30.5234'; |
||||
|
||||
test.beforeEach(async ({ page }) => { |
||||
context = await setup({ page }); |
||||
dashboard = new DashboardPage(page, context.project); |
||||
toolbar = dashboard.map.toolbar; |
||||
|
||||
await dashboard.viewSidebar.activateGeoDataEasterEgg(); |
||||
|
||||
// close 'Team & Auth' tab
|
||||
await dashboard.closeTab({ title: 'Team & Auth' }); |
||||
await dashboard.treeView.openTable({ title: 'Actor' }); |
||||
|
||||
const grid = dashboard.grid; |
||||
|
||||
await grid.column.create({ |
||||
title: 'Actors Birthplace', |
||||
type: 'GeoData', |
||||
}); |
||||
|
||||
await grid.column.verify({ title: 'Actors Birthplace', isVisible: true }); |
||||
|
||||
await grid.cell.geoData.openSetLocation({ |
||||
index: 0, |
||||
columnHeader: 'Actors Birthplace', |
||||
}); |
||||
await grid.cell.geoData.enterLatLong({ |
||||
lat: latitudeInShortDecimalLength, |
||||
long: longitudeInShortDecimalLength, |
||||
}); |
||||
await grid.cell.geoData.clickSave(); |
||||
|
||||
await grid.cell.verifyGeoDataCell({ |
||||
index: 0, |
||||
columnHeader: 'Actors Birthplace', |
||||
lat: latitudeInFullDecimalLength, |
||||
long: longitudeInFullDecimalLength, |
||||
}); |
||||
}); |
||||
|
||||
test('shows the marker and opens the expanded form view when clicking on it', async () => { |
||||
await dashboard.viewSidebar.createMapView({ |
||||
title: 'Map 1', |
||||
}); |
||||
// Zoom out
|
||||
await dashboard.map.zoomOut(8); |
||||
await dashboard.map.verifyMarkerCount(1); |
||||
|
||||
await dashboard.map.clickAddRowButton(); |
||||
|
||||
await dashboard.expandedForm.fillField({ |
||||
columnTitle: 'FirstName', |
||||
value: 'Mario', |
||||
type: 'text', |
||||
}); |
||||
|
||||
await dashboard.expandedForm.fillField({ |
||||
columnTitle: 'LastName', |
||||
value: 'Ali', |
||||
type: 'text', |
||||
}); |
||||
|
||||
await dashboard.expandedForm.fillField({ |
||||
columnTitle: 'Actors Birthplace', |
||||
value: '12, 34', |
||||
type: 'geodata', |
||||
}); |
||||
|
||||
await dashboard.expandedForm.save(); |
||||
|
||||
await dashboard.map.verifyMarkerCount(2); |
||||
|
||||
await dashboard.map.clickMarker('12', '34'); |
||||
|
||||
await dashboard.expandedForm.clickDeleteRow(); |
||||
await dashboard.map.verifyMarkerCount(1); |
||||
}); |
||||
}); |
Loading…
Reference in new issue