mirror of https://github.com/nocodb/nocodb
Wing-Kam Wong
2 years ago
645 changed files with 47328 additions and 27866 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 v-else data-testid="nc-geo-data-lat-long-set">{{ 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,57 @@
|
||||
<script lang="ts" setup> |
||||
import { isMac } from '#imports' |
||||
|
||||
const props = defineProps<{ |
||||
keys: string[] |
||||
}>() |
||||
|
||||
const isMacOs = isMac() |
||||
|
||||
const getLabel = (key: string) => { |
||||
if (isMacOs) { |
||||
switch (key.toLowerCase()) { |
||||
case 'alt': |
||||
return '⌥' |
||||
case 'shift': |
||||
return '⇧' |
||||
case 'meta': |
||||
return '⌘' |
||||
case 'control': |
||||
case 'ctrl': |
||||
return '⌃' |
||||
case 'enter': |
||||
return '↩' |
||||
} |
||||
} |
||||
switch (key.toLowerCase()) { |
||||
case 'arrowup': |
||||
return '↑' |
||||
case 'arrowdown': |
||||
return '↓' |
||||
case 'arrowleft': |
||||
return '←' |
||||
case 'arrowright': |
||||
return '→' |
||||
} |
||||
|
||||
return key |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="nc-shortcut-label-wrapper"> |
||||
<div v-for="(key, index) in props.keys" :key="index" class="nc-shortcut-label"> |
||||
<span>{{ getLabel(key) }}</span> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
.nc-shortcut-label-wrapper { |
||||
@apply flex gap-1; |
||||
} |
||||
|
||||
.nc-shortcut-label { |
||||
@apply text-[0.7rem] leading-6 min-w-5 min-h-5 text-center relative z-0 after:(content-[''] left-0 top-0 -z-1 bg-current opacity-10 absolute w-full h-full rounded) px-1; |
||||
} |
||||
</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 { IsPublicInj, OpenNewRecordFormHookInj, latLongToJoinedString, onMounted, provide, ref } 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, 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> |
@ -1,3 +1,3 @@
|
||||
<template> |
||||
<div class="mt-4 mb-2" /> |
||||
<div /> |
||||
</template> |
||||
|
@ -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,179 @@
|
||||
import { reactive } from 'vue' |
||||
import type { ComputedRef, Ref } from 'vue' |
||||
import type { ColumnType, MapType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' |
||||
import { IsPublicInj, ref, storeToRefs, 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) & { id: string }> | 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 } = storeToRefs(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, updateObj) |
||||
} |
||||
|
||||
const { getMeta } = useMetas() |
||||
|
||||
async function insertRow( |
||||
currentRow: Row, |
||||
ltarState: Record<string, any> = {}, |
||||
{ |
||||
metaValue = meta.value, |
||||
viewMetaValue = viewMeta.value, |
||||
}: { metaValue?: MapType & { id: string }; viewMetaValue?: (ViewType | MapType) & { id: string } } = {}, |
||||
) { |
||||
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 |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue