Browse Source

Merge branch 'develop' into pr/5165

pull/5165/head
Wing-Kam Wong 2 years ago
parent
commit
2beeb1f6bc
  1. 9
      .github/workflows/release-timely-executables.yml
  2. 4
      packages/nc-gui/assets/style.scss
  3. 2
      packages/nc-gui/components.d.ts
  4. 155
      packages/nc-gui/components/cell/GeoData.vue
  5. 12
      packages/nc-gui/components/cell/SingleSelect.vue
  6. 9
      packages/nc-gui/components/dlg/QuickImport.vue
  7. 11
      packages/nc-gui/components/dlg/TableCreate.vue
  8. 66
      packages/nc-gui/components/dlg/ViewCreate.vue
  9. 42
      packages/nc-gui/components/shared-view/Map.vue
  10. 6
      packages/nc-gui/components/smartsheet/Cell.vue
  11. 266
      packages/nc-gui/components/smartsheet/Map.vue
  12. 12
      packages/nc-gui/components/smartsheet/Toolbar.vue
  13. 13
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  14. 9
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  15. 4
      packages/nc-gui/components/smartsheet/header/CellIcon.ts
  16. 28
      packages/nc-gui/components/smartsheet/sidebar/MenuBottom.vue
  17. 15
      packages/nc-gui/components/smartsheet/sidebar/toolbar/GeodataSwitcher.vue
  18. 4
      packages/nc-gui/components/smartsheet/sidebar/toolbar/index.vue
  19. 3
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  20. 113
      packages/nc-gui/components/smartsheet/toolbar/MappedBy.vue
  21. 9
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  22. 3
      packages/nc-gui/components/smartsheet/toolbar/SharedViewList.vue
  23. 39
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  24. 5
      packages/nc-gui/components/tabs/Smartsheet.vue
  25. 16
      packages/nc-gui/components/template/Editor.vue
  26. 7
      packages/nc-gui/composables/useAttachment.ts
  27. 182
      packages/nc-gui/composables/useMapViewDataStore.ts
  28. 2
      packages/nc-gui/composables/useMultiSelect/index.ts
  29. 3
      packages/nc-gui/composables/useSmartsheetStore.ts
  30. 15
      packages/nc-gui/composables/useViewColumns.ts
  31. 3
      packages/nc-gui/lang/de.json
  32. 20
      packages/nc-gui/lang/en.json
  33. 1
      packages/nc-gui/lib/enums.ts
  34. 1
      packages/nc-gui/lib/types.ts
  35. 114
      packages/nc-gui/package-lock.json
  36. 6
      packages/nc-gui/package.json
  37. 35
      packages/nc-gui/pages/[projectType]/map/[viewId]/index.vue
  38. 9
      packages/nc-gui/pages/index/index/create.vue
  39. 2
      packages/nc-gui/utils/cell.ts
  40. 6
      packages/nc-gui/utils/columnUtils.ts
  41. 3
      packages/nc-gui/utils/geoDataUtils.ts
  42. 1
      packages/nc-gui/utils/index.ts
  43. 693
      packages/nc-gui/utils/mimeTypeUtils.ts
  44. 3
      packages/nc-gui/utils/viewUtils.ts
  45. 55
      packages/noco-docs/content/en/setup-and-usages/column-types.md
  46. 81
      packages/nocodb-sdk/src/lib/Api.ts
  47. 1
      packages/nocodb-sdk/src/lib/UITypes.ts
  48. 1
      packages/nocodb-sdk/src/lib/globals.ts
  49. 6
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  50. 8
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/getAst.ts
  51. 2
      packages/nocodb/src/lib/meta/api/index.ts
  52. 46
      packages/nocodb/src/lib/meta/api/mapViewApis.ts
  53. 3
      packages/nocodb/src/lib/meta/api/publicApis/publicDataApis.ts
  54. 6
      packages/nocodb/src/lib/meta/api/publicApis/publicDataExportApis.ts
  55. 1
      packages/nocodb/src/lib/meta/api/viewApis.ts
  56. 4
      packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
  57. 55
      packages/nocodb/src/lib/migrations/v2/nc_026_map_view.ts
  58. 104
      packages/nocodb/src/lib/models/MapView.ts
  59. 106
      packages/nocodb/src/lib/models/MapViewColumn.ts
  60. 108
      packages/nocodb/src/lib/models/View.ts
  61. 7
      packages/nocodb/src/lib/utils/globals.ts
  62. 178
      scripts/sdk/swagger.json
  63. 8
      tests/playwright/pages/Dashboard/ExpandedForm/index.ts
  64. 51
      tests/playwright/pages/Dashboard/Map/index.ts
  65. 13
      tests/playwright/pages/Dashboard/ViewSidebar/index.ts
  66. 36
      tests/playwright/pages/Dashboard/common/Cell/GeoDataCell.ts
  67. 30
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  68. 5
      tests/playwright/pages/Dashboard/common/Toolbar/index.ts
  69. 3
      tests/playwright/pages/Dashboard/index.ts
  70. 73
      tests/playwright/tests/columnGeoData.spec.ts
  71. 345
      tests/playwright/tests/keyboardShortcuts.spec.ts
  72. 92
      tests/playwright/tests/viewMap.spec.ts

9
.github/workflows/release-timely-executables.yml

@ -62,6 +62,10 @@ jobs:
./make.sh
sudo cp ./ldid /usr/local/bin
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Update nocodb-timely
env:
TAG: ${{ github.event.inputs.tag || inputs.tag }}
@ -75,11 +79,6 @@ jobs:
git tag $TAG
git push --tags
- uses: actions/setup-node@v3
with:
node-version: 16
- name : Install dependencies and build executables
run: |
# install npm dependendencies

4
packages/nc-gui/assets/style.scss

@ -287,6 +287,10 @@ a {
@apply !shadow-none rounded hover:(ring-1 ring-primary ring-opacity-100) focus:(ring-1 ring-accent ring-opacity-100);
}
.nc-warning-info {
@apply !shadow-none rounded ring-1 ring-red-600
}
.ant-modal {
@apply !top-[50px];
}

2
packages/nc-gui/components.d.ts vendored

@ -203,6 +203,8 @@ declare module '@vue/runtime-core' {
MdiLogin: typeof import('~icons/mdi/login')['default']
MdiLogout: typeof import('~icons/mdi/logout')['default']
MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMapMarker: typeof import('~icons/mdi/map-marker')['default']
MdiMapMarkerAlert: typeof import('~icons/mdi/map-marker-alert')['default']
MdiMenu: typeof import('~icons/mdi/menu')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']

155
packages/nc-gui/components/cell/GeoData.vue

@ -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>

12
packages/nc-gui/components/cell/SingleSelect.vue

@ -247,12 +247,12 @@ useEventListener(document, 'click', handleClose, true)
<a-select
ref="aselect"
v-model:value="vModel"
class="w-full"
class="w-full overflow-hidden"
:class="{ 'caret-transparent': !hasEditRoles }"
:allow-clear="!column.rqd && editAllowed"
:bordered="false"
:open="isOpen && (active || editable)"
:disabled="readOnly || !(active || editable)"
:open="isOpen && editAllowed"
:disabled="readOnly || !editAllowed"
:show-arrow="hasEditRoles && !readOnly && (editable || (active && vModel === null))"
:dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen && (active || editable) ? 'active' : ''}`"
:show-search="isOpen && (active || editable)"
@ -328,6 +328,12 @@ useEventListener(document, 'click', handleClose, true)
@apply !px-0;
}
:deep(.ant-select-selection-search) {
// following a-select with mode = multiple | tags
// initial width will block @mouseover in Grid.vue
@apply !w-[5px];
}
:deep(.ant-select-selection-search-input) {
@apply !text-xs;
}

9
packages/nc-gui/components/dlg/QuickImport.vue

@ -31,7 +31,7 @@ interface Props {
importDataOnly?: boolean
}
const { importType, importDataOnly = false, ...rest } = defineProps<Props>()
const { importType, importDataOnly = false, baseId, ...rest } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
@ -61,7 +61,7 @@ const isParsingData = ref(false)
const useForm = Form.useForm
const importState = reactive({
const defaultImportState = {
fileList: [] as importFileList | streamImportFileList,
url: '',
jsonEditor: {},
@ -72,7 +72,8 @@ const importState = reactive({
firstRowAsHeaders: true,
shouldImportData: true,
},
})
}
const importState = reactive(defaultImportState)
const isImportTypeJson = computed(() => importType === 'json')
@ -176,6 +177,8 @@ async function handleImport() {
return message.error(await extractSdkResponseErrorMsg(e))
} finally {
importLoading.value = false
templateEditorModal.value = false
Object.assign(importState, defaultImportState)
}
dialogShow.value = false
}

11
packages/nc-gui/components/dlg/TableCreate.vue

@ -78,14 +78,19 @@ const systemColumnsCheckboxInfo = SYSTEM_COLUMNS.map((c, index) => ({
disabled: index === 0,
}))
const creating = ref(false)
const _createTable = async () => {
try {
creating.value = true
await validate()
await createTable()
} catch (e: any) {
e.errorFields.map((f: Record<string, any>) => message.error(f.errors.join(',')))
if (e.errorFields.length) return
} finally {
creating.value = false
}
await createTable()
}
onMounted(() => {
@ -109,7 +114,9 @@ onMounted(() => {
<template #footer>
<a-button key="back" size="large" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" size="large" type="primary" @click="_createTable">{{ $t('general.submit') }}</a-button>
<a-button key="submit" size="large" type="primary" :loading="creating" @click="_createTable"
>{{ $t('general.submit') }}
</a-button>
</template>
<div class="pl-10 pr-10 pt-5">

66
packages/nc-gui/components/dlg/ViewCreate.vue

@ -2,7 +2,7 @@
import type { ComponentPublicInstance } from '@vue/runtime-core'
import type { Form as AntForm, SelectProps } from 'ant-design-vue'
import { capitalize } from '@vue/runtime-core'
import type { FormType, GalleryType, GridType, KanbanType, TableType, ViewType } from 'nocodb-sdk'
import type { FormType, GalleryType, GridType, KanbanType, MapType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, ViewTypes } from 'nocodb-sdk'
import {
computed,
@ -25,13 +25,14 @@ interface Props {
title?: string
selectedViewId?: string
groupingFieldColumnId?: string
geoDataFieldColumnId?: string
views: ViewType[]
meta: TableType
}
interface Emits {
(event: 'update:modelValue', value: boolean): void
(event: 'created', value: GridType | KanbanType | GalleryType | FormType): void
(event: 'created', value: GridType | KanbanType | GalleryType | FormType | MapType): void
}
interface Form {
@ -40,9 +41,10 @@ interface Form {
copy_from_id: string | null
// for kanban view only
fk_grp_col_id: string | null
fk_geo_data_col_id: string | null
}
const { views = [], meta, selectedViewId, groupingFieldColumnId, ...props } = defineProps<Props>()
const { views = [], meta, selectedViewId, groupingFieldColumnId, geoDataFieldColumnId, ...props } = defineProps<Props>()
const emits = defineEmits<Emits>()
@ -61,9 +63,10 @@ const form = reactive<Form>({
type: props.type,
copy_from_id: null,
fk_grp_col_id: null,
fk_geo_data_col_id: null,
})
const singleSelectFieldOptions = ref<SelectProps['options']>([])
const viewSelectFieldOptions = ref<SelectProps['options']>([])
const viewNameRules = [
// name is required
@ -72,7 +75,7 @@ const viewNameRules = [
{
validator: (_: unknown, v: string) =>
new Promise((resolve, reject) => {
views.every((v1) => ((v1 as GridType | KanbanType | GalleryType).alias || v1.title) !== v)
views.every((v1) => ((v1 as GridType | KanbanType | GalleryType | MapType).alias || v1.title) !== v)
? resolve(true)
: reject(new Error(`View name should be unique`))
}),
@ -80,10 +83,9 @@ const viewNameRules = [
},
]
const groupingFieldColumnRules = [
// name is required
{ required: true, message: `${t('general.groupingField')} ${t('general.required')}` },
]
const groupingFieldColumnRules = [{ required: true, message: `${t('general.groupingField')} ${t('general.required')}` }]
const geoDataFieldColumnRules = [{ required: true, message: `${t('general.geoDataField')} ${t('general.required')}` }]
const typeAlias = computed(
() =>
@ -92,6 +94,7 @@ const typeAlias = computed(
[ViewTypes.GALLERY]: 'gallery',
[ViewTypes.FORM]: 'form',
[ViewTypes.KANBAN]: 'kanban',
[ViewTypes.MAP]: 'map',
}[props.type]),
)
@ -113,7 +116,7 @@ function init() {
// preset the grouping field column
if (props.type === ViewTypes.KANBAN) {
singleSelectFieldOptions.value = meta
viewSelectFieldOptions.value = meta
.columns!.filter((el) => el.uidt === UITypes.SingleSelect)
.map((field) => {
return {
@ -127,7 +130,26 @@ function init() {
form.fk_grp_col_id = groupingFieldColumnId
} else {
// take the first option
form.fk_grp_col_id = singleSelectFieldOptions.value?.[0]?.value as string
form.fk_grp_col_id = viewSelectFieldOptions.value?.[0]?.value as string
}
}
if (props.type === ViewTypes.MAP) {
viewSelectFieldOptions.value = meta
.columns!.filter((el) => el.uidt === UITypes.GeoData)
.map((field) => {
return {
value: field.id,
label: field.title,
}
})
if (geoDataFieldColumnId) {
// take from the one from copy view
form.fk_geo_data_col_id = geoDataFieldColumnId
} else {
// take the first option
form.fk_geo_data_col_id = viewSelectFieldOptions.value?.[0]?.value as string
}
}
@ -150,7 +172,7 @@ async function onSubmit() {
if (!_meta || !_meta.id) return
try {
let data: GridType | KanbanType | GalleryType | FormType | null = null
let data: GridType | KanbanType | GalleryType | FormType | MapType | null = null
switch (form.type) {
case ViewTypes.GRID:
@ -164,6 +186,9 @@ async function onSubmit() {
break
case ViewTypes.KANBAN:
data = await api.dbView.kanbanCreate(_meta.id, form)
break
case ViewTypes.MAP:
data = await api.dbView.mapCreate(_meta.id, form)
}
if (data) {
@ -207,12 +232,27 @@ async function onSubmit() {
<a-select
v-model:value="form.fk_grp_col_id"
class="w-full nc-kanban-grouping-field-select"
:options="singleSelectFieldOptions"
:options="viewSelectFieldOptions"
:disabled="groupingFieldColumnId"
placeholder="Select a Grouping Field"
not-found-content="No Single Select Field can be found. Please create one first."
/>
</a-form-item>
<a-form-item
v-if="form.type === ViewTypes.MAP"
:label="$t('general.geoDataField')"
name="fk_geo_data_col_id"
:rules="geoDataFieldColumnRules"
>
<a-select
v-model:value="form.fk_geo_data_col_id"
class="w-full"
:options="viewSelectFieldOptions"
:disabled="geoDataFieldColumnId"
placeholder="Select a GeoData Field"
not-found-content="No GeoData Field can be found. Please create one first."
/>
</a-form-item>
</a-form>
<template #footer>

42
packages/nc-gui/components/shared-view/Map.vue

@ -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>

6
packages/nc-gui/components/smartsheet/Cell.vue

@ -21,6 +21,7 @@ import {
isDuration,
isEmail,
isFloat,
isGeoData,
isInt,
isJSON,
isManualSaved,
@ -100,7 +101,9 @@ const syncValue = useDebounceFn(
)
const vModel = computed({
get: () => props.modelValue,
get: () => {
return props.modelValue
},
set: (val) => {
if (val !== props.modelValue) {
currentRow.value.rowMeta.changed = true
@ -151,6 +154,7 @@ const isNumericField = computed(() => {
>
<template v-if="column">
<LazyCellTextArea v-if="isTextArea(column)" v-model="vModel" />
<LazyCellGeoData v-else-if="isGeoData(column)" v-model="vModel" />
<LazyCellCheckbox v-else-if="isBoolean(column, abstractType)" v-model="vModel" />
<LazyCellAttachment v-else-if="isAttachment(column)" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellSingleSelect v-else-if="isSingleSelect(column)" v-model="vModel" :row-index="props.rowIndex" />

266
packages/nc-gui/components/smartsheet/Map.vue

@ -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: '&copy; <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>

12
packages/nc-gui/components/smartsheet/Toolbar.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import { IsPublicInj, inject, ref, useSharedView, useSidebar, useSmartsheetStoreOrThrow, useUIPermission } from '#imports'
const { isGrid, isForm, isGallery, isKanban, isSqlView } = useSmartsheetStoreOrThrow()
const { isGrid, isForm, isGallery, isKanban, isMap, isSqlView } = useSmartsheetStoreOrThrow()
const isPublic = inject(IsPublicInj, ref(false))
@ -18,7 +18,7 @@ const { allowCSVDownload } = useSharedView()
style="z-index: 7"
>
<LazySmartsheetToolbarViewActions
v-if="(isGrid || isGallery || isKanban) && !isPublic && isUIAllowed('dataInsert')"
v-if="(isGrid || isGallery || isKanban || isMap) && !isPublic && isUIAllowed('dataInsert')"
:show-system-fields="false"
class="ml-1"
/>
@ -29,15 +29,17 @@ const { allowCSVDownload } = useSharedView()
<LazySmartsheetToolbarKanbanStackEditOrAdd v-if="isKanban" />
<LazySmartsheetToolbarFieldsMenu v-if="isGrid || isGallery || isKanban" :show-system-fields="false" />
<LazySmartsheetToolbarMappedBy v-if="isMap" />
<LazySmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery || isKanban" />
<LazySmartsheetToolbarFieldsMenu v-if="isGrid || isGallery || isKanban || isMap" :show-system-fields="false" />
<LazySmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery || isKanban || isMap" />
<LazySmartsheetToolbarSortListMenu v-if="isGrid || isGallery || isKanban" />
<LazySmartsheetToolbarRowHeight v-if="isGrid" />
<LazySmartsheetToolbarShareView v-if="(isForm || isGrid || isKanban || isGallery) && !isPublic" />
<LazySmartsheetToolbarShareView v-if="(isForm || isGrid || isKanban || isGallery || isMap) && !isPublic" />
<LazySmartsheetToolbarExport v-if="(!isPublic && !isUIAllowed('dataInsert')) || (isPublic && allowCSVDownload)" />
<div class="flex-1" />

13
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -57,9 +57,13 @@ const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const onlyNameUpdateOnEditColumns = [UITypes.LinkToAnotherRecord, UITypes.Lookup, UITypes.Rollup]
const geoDataToggleCondition = (t) => {
return geodataToggleState.show ? geodataToggleState.show : !t.name.includes(UITypes.GeoData)
}
const uiTypesOptions = computed<typeof uiTypes>(() => {
return [
...uiTypes.filter((t) => !isEdit.value || !t.virtual),
...uiTypes.filter((t) => geoDataToggleCondition(t) && (!isEdit.value || !t.virtual)),
...(!isEdit.value && meta?.value?.columns?.every((c) => !c.pk)
? [
{
@ -80,8 +84,12 @@ const reloadMetaAndData = async () => {
}
}
const saving = ref(false)
async function onSubmit() {
saving.value = true
const saved = await addOrUpdate(reloadMetaAndData, props.columnPosition)
saving.value = false
if (!saved) return
@ -178,6 +186,7 @@ useEventListener('keydown', (e: KeyboardEvent) => {
<LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<LazySmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" />
<LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<LazySmartsheetColumnGeoDataOptions v-if="formState.uidt === UITypes.GeoData" v-model:value="formState" />
<LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
@ -234,7 +243,7 @@ useEventListener('keydown', (e: KeyboardEvent) => {
{{ $t('general.cancel') }}
</a-button>
<a-button html-type="submit" type="primary" @click.prevent="onSubmit">
<a-button html-type="submit" type="primary" :loading="saving" @click.prevent="onSubmit">
<!-- Save -->
{{ $t('general.save') }}
</a-button>

9
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -3,6 +3,7 @@ import type { TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import {
CellClickHookInj,
FieldsInj,
IsFormInj,
IsKanbanInj,
@ -49,6 +50,9 @@ const meta = toRef(props, 'meta')
const router = useRouter()
// override cell click hook to avoid unexpected behavior at form fields
provide(CellClickHookInj, null)
const fields = computedInject(FieldsInj, (_fields) => {
if (props.useMetaFields) {
return (meta.value.columns ?? []).filter((col) => !isSystemColumn(col))
@ -132,7 +136,6 @@ reloadHook.on(() => {
if (isNew.value) return
loadRow()
})
provide(ReloadRowDataHookInj, reloadHook)
if (isKanban.value) {
@ -147,9 +150,7 @@ if (isKanban.value) {
const cellWrapperEl = ref<HTMLElement>()
onMounted(() => {
setTimeout(() => {
;(cellWrapperEl.value?.querySelector('input,select,textarea') as HTMLInputElement)?.focus()
})
setTimeout(() => (cellWrapperEl.value?.querySelector('input,select,textarea') as HTMLInputElement)?.focus())
})
</script>

4
packages/nc-gui/components/smartsheet/header/CellIcon.ts

@ -15,6 +15,7 @@ import {
isDuration,
isEmail,
isFloat,
isGeoData,
isInt,
isJSON,
isPercent,
@ -44,6 +45,7 @@ import CalendarIcon from '~icons/mdi/calendar'
import SingleSelectIcon from '~icons/mdi/arrow-down-drop-circle'
import MultiSelectIcon from '~icons/mdi/format-list-bulleted-square'
import DatetimeIcon from '~icons/mdi/calendar-clock'
import GeoDataIcon from '~icons/mdi/map-marker'
import RatingIcon from '~icons/mdi/star'
import GenericIcon from '~icons/mdi/square-rounded'
import NumericIcon from '~icons/mdi/numeric'
@ -64,6 +66,8 @@ const renderIcon = (column: ColumnType, abstractType: any) => {
return CalendarIcon
} else if (isDateTime(column, abstractType)) {
return DatetimeIcon
} else if (isGeoData(column)) {
return GeoDataIcon
} else if (isSet(column)) {
return MultiSelectIcon
} else if (isSingleSelect(column)) {

28
packages/nc-gui/components/smartsheet/sidebar/MenuBottom.vue

@ -2,12 +2,12 @@
import { ViewTypes } from 'nocodb-sdk'
import { useNuxtApp, useSmartsheetStoreOrThrow, viewIcons } from '#imports'
const emits = defineEmits<Emits>()
interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string }): void
}
const emits = defineEmits<Emits>()
const { $e } = useNuxtApp()
const { isSqlView } = useSmartsheetStoreOrThrow()
@ -20,7 +20,6 @@ function onOpenModal(type: ViewTypes, title = '') {
<template>
<a-menu :selected-keys="[]" class="flex flex-col">
<div>
<h3 class="px-3 text-xs font-semibold flex items-center gap-4 text-gray-500">
{{ $t('activity.createView') }}
</h3>
@ -113,8 +112,29 @@ function onOpenModal(type: ViewTypes, title = '') {
</div>
</a-tooltip>
</a-menu-item>
<a-menu-item
v-if="geodataToggleState.show"
key="map"
class="group !flex !items-center !my-0 !h-2.5rem nc-create-map-view"
@click="onOpenModal(ViewTypes.MAP)"
>
<a-tooltip :mouse-enter-delay="1" placement="left">
<template #title>
{{ $t('msg.info.addView.map') }}
</template>
<div class="w-full h-4" />
<div class="nc-project-menu-item !py-0 text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.MAP].icon" :style="{ color: viewIcons[ViewTypes.MAP].color }" />
<div>{{ $t('objects.viewType.map') }}</div>
<div class="flex-1" />
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<div class="w-full h-4" />
</a-menu>
</template>

15
packages/nc-gui/components/smartsheet/sidebar/toolbar/GeodataSwitcher.vue

@ -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>

4
packages/nc-gui/components/smartsheet/sidebar/toolbar/index.vue

@ -29,6 +29,10 @@ const onClick = () => {
<LazySmartsheetSidebarToolbarDebugMeta />
<div class="dot" />
<LazySmartsheetSidebarToolbarGeodataSwitcher />
<div class="dot" />
</template>
<slot name="end" />

3
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -55,6 +55,8 @@ const { eventBus } = useSmartsheetStoreOrThrow()
eventBus.on((event) => {
if (event === SmartsheetStoreEvents.FIELD_RELOAD) {
loadViewColumns()
} else if (event === SmartsheetStoreEvents.MAPPED_BY_COLUMN_CHANGE) {
loadViewColumns()
}
})
@ -204,6 +206,7 @@ useMenuCloseOnEsc(open)
v-model:checked="field.show"
v-e="['a:fields:show-hide']"
class="shrink"
:disabled="field.isViewEssentialField"
@change="saveOrUpdate(field, index)"
>
<div class="flex items-center">

113
packages/nc-gui/components/smartsheet/toolbar/MappedBy.vue

@ -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>

9
packages/nc-gui/components/smartsheet/toolbar/ShareView.vue

@ -116,6 +116,9 @@ const sharedViewUrl = computed(() => {
case ViewTypes.GALLERY:
viewType = 'gallery'
break
case ViewTypes.MAP:
viewType = 'map'
break
default:
viewType = 'view'
}
@ -350,7 +353,11 @@ const copyIframeCode = async () => {
<div
v-if="
shared && (shared.type === ViewTypes.GRID || shared.type === ViewTypes.KANBAN || shared.type === ViewTypes.GALLERY)
shared &&
(shared.type === ViewTypes.GRID ||
shared.type === ViewTypes.KANBAN ||
shared.type === ViewTypes.GALLERY ||
shared.type === ViewTypes.MAP)
"
>
<!-- Allow Download -->

3
packages/nc-gui/components/smartsheet/toolbar/SharedViewList.vue

@ -56,6 +56,9 @@ const sharedViewUrl = (view: SharedViewType) => {
case ViewTypes.FORM:
viewType = 'form'
break
case ViewTypes.MAP:
viewType = 'map'
break
case ViewTypes.KANBAN:
viewType = 'kanban'
break

39
packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue

@ -1,8 +1,10 @@
<script lang="ts" setup>
import type { Ref } from '@vue/reactivity'
import {
ActiveViewInj,
IsLockedInj,
IsPublicInj,
MetaInj,
extractSdkResponseErrorMsg,
inject,
message,
@ -41,12 +43,27 @@ const showApiSnippetDrawer = ref(false)
const showErd = ref(false)
const quickImportDialog = ref(false)
type QuickImportDialogType = 'csv' | 'excel' | 'json'
// TODO: add 'json' when it's ready
const quickImportDialogTypes: QuickImportDialogType[] = ['csv', 'excel']
const quickImportDialogs: Record<typeof quickImportDialogTypes[number], Ref<boolean>> = quickImportDialogTypes.reduce(
(acc: any, curr) => {
acc[curr] = ref(false)
return acc
},
{},
) as Record<QuickImportDialogType, Ref<boolean>>
const { isUIAllowed } = useUIPermission()
const { isSharedBase } = useProject()
const meta = inject(MetaInj, ref())
const currentBaseId = computed(() => meta.value?.base_id)
const Icon = computed(() => {
switch (selectedView.value?.lock_type) {
case LockType.Personal:
@ -173,19 +190,20 @@ useMenuCloseOnEsc(open)
</template>
<template #expandIcon></template>
<a-menu-item v-if="isUIAllowed('csvImport') && !isView && !isPublicView">
<template v-for="(dialog, type) in quickImportDialogs">
<a-menu-item v-if="isUIAllowed(`${type}Import`) && !isView && !isPublicView" :key="type">
<div
v-e="['a:actions:upload-csv']"
v-e="[`a:actions:upload-${type}`]"
class="nc-project-menu-item"
:class="{ disabled: isLocked }"
@click="!isLocked ? (quickImportDialog = true) : {}"
@click="!isLocked ? (dialog.value = true) : {}"
>
<MdiUploadOutline class="text-gray-500" />
<!-- Upload CSV -->
{{ $t('activity.uploadCSV') }}
{{ `${$t('general.upload')} ${type.toUpperCase()}` }}
<div class="flex items-center text-gray-400"><MdiAlpha />version</div>
</div>
</a-menu-item>
</template>
</a-sub-menu>
</template>
@ -230,7 +248,14 @@ useMenuCloseOnEsc(open)
</template>
</a-dropdown>
<LazyDlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" import-type="csv" :import-data-only="true" />
<LazyDlgQuickImport
v-for="type in quickImportDialogTypes"
:key="type"
v-model="quickImportDialogs[type].value"
:import-type="type"
:base-id="currentBaseId"
:import-data-only="true"
/>
<LazyWebhookDrawer v-if="showWebhookDrawer" v-model="showWebhookDrawer" />

5
packages/nc-gui/components/tabs/Smartsheet.vue

@ -39,7 +39,7 @@ const fields = ref<ColumnType[]>([])
const meta = computed<TableType | undefined>(() => activeTab.value && metas.value[activeTab.value.id!])
const { isGallery, isGrid, isForm, isKanban, isLocked } = useProvideSmartsheetStore(activeView, meta)
const { isGallery, isGrid, isForm, isKanban, isLocked, isMap } = useProvideSmartsheetStore(activeView, meta)
const reloadEventHook = createEventHook<void | boolean>()
@ -48,6 +48,7 @@ const reloadViewMetaEventHook = createEventHook<void | boolean>()
const openNewRecordFormHook = createEventHook<void>()
useProvideKanbanViewStore(meta, activeView)
useProvideMapViewStore(meta, activeView)
// todo: move to store
provide(MetaInj, meta)
@ -81,6 +82,8 @@ provide(
<LazySmartsheetForm v-else-if="isForm && !$route.query.reload" />
<LazySmartsheetKanban v-else-if="isKanban" />
<LazySmartsheetMap v-else-if="isMap" />
</div>
</div>
</template>

16
packages/nc-gui/components/template/Editor.vue

@ -405,10 +405,14 @@ async function importTemplate() {
const tableId = meta.value?.id
const projectName = project.value.title!
const table_names = data.tables.map((t: Record<string, any>) => t.table_name)
await Promise.all(
Object.keys(importData).map((key: string) =>
(async (k) => {
if (!table_names.includes(k)) {
return
}
const data = importData[k]
const total = data.length
@ -458,7 +462,7 @@ async function importTemplate() {
// Successfully imported table data
message.success(t('msg.success.tableDataImported'))
} catch (e: any) {
message.error(e.message)
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isImporting.value = false
}
@ -633,6 +637,16 @@ function isSelectDisabled(uidt: string, disableSelect = false) {
</span>
</template>
<template #extra>
<a-tooltip bottom>
<template #title>
<!-- TODO: i18n -->
<span>Delete Table</span>
</template>
<mdi-delete-outline v-if="data.tables.length > 1" class="text-lg mr-8" @click.stop="deleteTable(tableIdx)" />
</a-tooltip>
</template>
<a-table
v-if="srcDestMapping"
class="template-form"

7
packages/nc-gui/composables/useAttachment.ts

@ -1,4 +1,4 @@
import { mimeTypes, openLink, useGlobal } from '#imports'
import { openLink, useGlobal } from '#imports'
const useAttachment = () => {
const { appInfo } = useGlobal()
@ -15,11 +15,10 @@ const useAttachment = () => {
return item.data
}
const sources = getPossibleAttachmentSrc(item)
const mimeType = mimeTypes[item?.mimetype?.split('/')?.pop() || 'txt']
for (const source of sources) {
// test if the source is accessible or not
const res = await fetch(source)
if (res.ok && res.headers.get('Content-Type') === mimeType) {
const res = await fetch(source, { method: 'HEAD' })
if (res.ok) {
return source
}
}

182
packages/nc-gui/composables/useMapViewDataStore.ts

@ -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
}

2
packages/nc-gui/composables/useMultiSelect/index.ts

@ -145,14 +145,12 @@ export function useMultiSelect(
return
}
editEnabled.value = false
isMouseDown = true
selectedRange.startRange({ row, col })
}
const handleCellClick = (event: MouseEvent, row: number, col: number) => {
isMouseDown = true
editEnabled.value = false
selectedRange.startRange({ row, col })
selectedRange.endRange({ row, col })
makeActive(row, col)

3
packages/nc-gui/composables/useSmartsheetStore.ts

@ -26,13 +26,13 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
const eventBus = useEventBus<SmartsheetStoreEvents>(Symbol('SmartsheetStore'))
// getters
const isLocked = computed(() => view.value?.lock_type === 'locked')
const isPkAvail = computed(() => (meta.value as TableType)?.columns?.some((c) => c.pk))
const isGrid = computed(() => view.value?.type === ViewTypes.GRID)
const isForm = computed(() => view.value?.type === ViewTypes.FORM)
const isGallery = computed(() => view.value?.type === ViewTypes.GALLERY)
const isKanban = computed(() => view.value?.type === ViewTypes.KANBAN)
const isMap = computed(() => view.value?.type === ViewTypes.MAP)
const isSharedForm = computed(() => isForm.value && shared)
const xWhere = computed(() => {
let where
@ -65,6 +65,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
isGrid,
isGallery,
isKanban,
isMap,
cellRefs,
isSharedForm,
sorts,

15
packages/nc-gui/composables/useViewColumns.ts

@ -1,5 +1,5 @@
import { isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import { ViewTypes, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, MapType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { IsPublicInj, computed, inject, ref, useNuxtApp, useProject, useUIPermission, watch } from '#imports'
import type { Field } from '~/lib'
@ -25,6 +25,13 @@ export function useViewColumns(
() => isPublic.value || !isUIAllowed('hideAllColumns') || !isUIAllowed('showAllColumns') || isSharedBase.value,
)
const isColumnViewEssential = (column: ColumnType) => {
// TODO: consider at some point ti delegate this via a cleaner design pattern to view specific check logic
// which could be inside of a view specific helper class (and generalized via an interface)
// (on the other hand, the logic complexity is still very low atm - might be overkill)
return view.value?.type === ViewTypes.MAP && (view.value?.view as MapType)?.fk_geo_data_col_id === column.id
}
const metaColumnById = computed<Record<string, ColumnType>>(() => {
if (!meta.value?.columns) return {}
@ -62,8 +69,10 @@ export function useViewColumns(
title: column.title,
fk_column_id: column.id,
...currentColumnField,
show: currentColumnField.show || isColumnViewEssential(currentColumnField),
order: currentColumnField.order || order++,
system: isSystemColumn(metaColumnById?.value?.[currentColumnField.fk_column_id!]),
isViewEssentialField: isColumnViewEssential(column),
}
})
.sort((a: Field, b: Field) => a.order - b.order)
@ -98,7 +107,7 @@ export function useViewColumns(
if (isLocalMode.value) {
fields.value = fields.value?.map((field: Field) => ({
...field,
show: false,
show: !!field.isViewEssentialField,
}))
reloadData?.()
return

3
packages/nc-gui/lang/de.json

@ -98,7 +98,8 @@
"gallery": "Galerie",
"form": "Formular",
"kanban": "Kanban",
"calendar": "Kalender"
"calendar": "Kalender",
"map": "Karte"
},
"user": "Nutzer",
"users": "Benutzer",

20
packages/nc-gui/lang/en.json

@ -74,7 +74,8 @@
"insertBefore": "Insert Before",
"hideField": "Hide Field",
"sortAsc": "Sort Ascending",
"sortDesc": "Sort Descending"
"sortDesc": "Sort Descending",
"geoDataField": "GeoData Field"
},
"objects": {
"project": "Project",
@ -98,7 +99,8 @@
"gallery": "Gallery",
"form": "Form",
"kanban": "Kanban",
"calendar": "Calendar"
"calendar": "Calendar",
"map": "Map"
},
"user": "User",
"users": "Users",
@ -136,6 +138,7 @@
"Currency": "Currency",
"Percent": "Percent",
"Duration": "Duration",
"GeoData": "GeoData",
"Rating": "Rating",
"Formula": "Formula",
"Rollup": "Rollup",
@ -253,6 +256,9 @@
"barcodeFormat": "Barcode format",
"qrCodeValueTooLong": "Too many characters for a QR code",
"barcodeValueTooLong": "Too many characters for a barcode",
"yourLocation": "Your Location",
"lng": "Lng",
"lat": "Lat",
"aggregateFunction": "Aggregate function",
"dbCreateIfNotExists": "Database : create if not exists",
"clientKey": "Client Key",
@ -452,6 +458,10 @@
"stackedBy": "Stacked By",
"chooseGroupingField": "Choose a Grouping Field",
"addOrEditStack": "Add / Edit Stack"
},
"map": {
"mappedBy": "Mapped By",
"chooseMappingField": "Choose a Mapping Field"
}
},
"tooltip": {
@ -518,6 +528,11 @@
"orgCreator": "Creator can create new projects and access any invited project.",
"orgViewer": "Viewer is not allowed to create new projects but they can access any invited project."
},
"map": {
"overLimit": "You're over the limit.",
"closeLimit": "You're getting close to the limit.",
"limitNumber": "The limit of markers shown in a Map View is 1000 records."
},
"footerInfo": "Rows per page",
"upload": "Select file to Upload",
"upload_sub": "or drag and drop file",
@ -600,6 +615,7 @@
"gallery": "Add Gallery View",
"form": "Add Form View",
"kanban": "Add Kanban View",
"map": "Add Map View",
"calendar": "Add Calendar View"
},
"tablesMetadataInSync": "Tables metadata is in Sync",

1
packages/nc-gui/lib/enums.ts

@ -88,6 +88,7 @@ export enum SmartsheetStoreEvents {
DATA_RELOAD = 'data-reload',
FIELD_RELOAD = 'field-reload',
FIELD_ADD = 'field-add',
MAPPED_BY_COLUMN_CHANGE = 'mapped-by-column-change',
}
export enum DataSourcesSubTab {

1
packages/nc-gui/lib/types.ts

@ -32,6 +32,7 @@ export interface Field {
title: string
fk_column_id?: string
system?: boolean
isViewEssentialField?: boolean
}
export type Roles<T extends Role | ProjectRole = Role | ProjectRole> = Record<T | string, boolean>

114
packages/nc-gui/package-lock.json generated

@ -27,6 +27,8 @@
"jsep": "^1.3.6",
"just-clone": "^6.1.1",
"jwt-decode": "^3.1.2",
"leaflet": "^1.9.2",
"leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk",
@ -41,6 +43,7 @@
"vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.0.3",
"vue-i18n": "^9.2.2",
"vue3-contextmenu": "^0.2.12",
"vue3-text-clamp": "^0.1.1",
"vuedraggable": "^4.1.0",
"xlsx": "^0.18.5"
@ -69,6 +72,9 @@
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0",
"@types/dagre": "^0.7.48",
"@types/file-saver": "^2.0.5",
"@types/leaflet": "^1.9.0",
"@types/leaflet.markercluster": "^1.5.1",
"@types/papaparse": "^5.3.2",
"@types/sortablejs": "^1.13.0",
"@types/tinycolor2": "^1.4.3",
@ -3131,7 +3137,8 @@
"node_modules/@types/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ=="
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==",
"dev": true
},
"node_modules/@types/form-data": {
"version": "0.0.33",
@ -3142,6 +3149,12 @@
"@types/node": "*"
}
},
"node_modules/@types/geojson": {
"version": "7946.0.10",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@ -3154,6 +3167,24 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"node_modules/@types/leaflet": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.0.tgz",
"integrity": "sha512-7LeOSj7EloC5UcyOMo+1kc3S1UT3MjJxwqsMT1d2PTyvQz53w0Y0oSSk9nwZnOZubCmBvpSNGceucxiq+ZPEUw==",
"dev": true,
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/leaflet.markercluster": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.1.tgz",
"integrity": "sha512-gzJzP10qO6Zkts5QNVmSAEDLYicQHTEBLT9HZpFrJiSww9eDAs5OWHvIskldf41MvDv1gbMukuEBQEawHn+wtA==",
"dev": true,
"dependencies": {
"@types/leaflet": "*"
}
},
"node_modules/@types/mdast": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
@ -10661,6 +10692,19 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/leaflet": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
},
"node_modules/leaflet.markercluster": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
"peerDependencies": {
"leaflet": "^1.3.1"
}
},
"node_modules/less": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz",
@ -11274,6 +11318,11 @@
"minipass": "^2.9.0"
}
},
"node_modules/mitt": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz",
"integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg=="
},
"node_modules/mkdir": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/mkdir/-/mkdir-0.0.2.tgz",
@ -17172,6 +17221,16 @@
"vue": "^3.0.0"
}
},
"node_modules/vue3-contextmenu": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/vue3-contextmenu/-/vue3-contextmenu-0.2.12.tgz",
"integrity": "sha512-lSA+Pq4wozbf9nDmMBvz2Z2DXwGwCEeXEDsmQi+cIxc5FzPa1Ia013hv5/DQZwr4dPilNxrOXPht1u0CMXcpVw==",
"dependencies": {
"core-js": "^3.6.5",
"mitt": "^2.1.0",
"vue": "^3.0.0"
}
},
"node_modules/vue3-text-clamp": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/vue3-text-clamp/-/vue3-text-clamp-0.1.1.tgz",
@ -20061,7 +20120,8 @@
"@types/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ=="
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==",
"dev": true
},
"@types/form-data": {
"version": "0.0.33",
@ -20072,6 +20132,12 @@
"@types/node": "*"
}
},
"@types/geojson": {
"version": "7946.0.10",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==",
"dev": true
},
"@types/json-schema": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
@ -20084,6 +20150,24 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"@types/leaflet": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.0.tgz",
"integrity": "sha512-7LeOSj7EloC5UcyOMo+1kc3S1UT3MjJxwqsMT1d2PTyvQz53w0Y0oSSk9nwZnOZubCmBvpSNGceucxiq+ZPEUw==",
"dev": true,
"requires": {
"@types/geojson": "*"
}
},
"@types/leaflet.markercluster": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.1.tgz",
"integrity": "sha512-gzJzP10qO6Zkts5QNVmSAEDLYicQHTEBLT9HZpFrJiSww9eDAs5OWHvIskldf41MvDv1gbMukuEBQEawHn+wtA==",
"dev": true,
"requires": {
"@types/leaflet": "*"
}
},
"@types/mdast": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
@ -25481,6 +25565,17 @@
}
}
},
"leaflet": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
"integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
},
"leaflet.markercluster": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
"requires": {}
},
"less": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz",
@ -25974,6 +26069,11 @@
"minipass": "^2.9.0"
}
},
"mitt": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz",
"integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg=="
},
"mkdir": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/mkdir/-/mkdir-0.0.2.tgz",
@ -30167,6 +30267,16 @@
"is-plain-object": "3.0.1"
}
},
"vue3-contextmenu": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/vue3-contextmenu/-/vue3-contextmenu-0.2.12.tgz",
"integrity": "sha512-lSA+Pq4wozbf9nDmMBvz2Z2DXwGwCEeXEDsmQi+cIxc5FzPa1Ia013hv5/DQZwr4dPilNxrOXPht1u0CMXcpVw==",
"requires": {
"core-js": "^3.6.5",
"mitt": "^2.1.0",
"vue": "^3.0.0"
}
},
"vue3-text-clamp": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/vue3-text-clamp/-/vue3-text-clamp-0.1.1.tgz",

6
packages/nc-gui/package.json

@ -50,6 +50,8 @@
"jsep": "^1.3.6",
"just-clone": "^6.1.1",
"jwt-decode": "^3.1.2",
"leaflet": "^1.9.2",
"leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk",
@ -64,6 +66,7 @@
"vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.0.3",
"vue-i18n": "^9.2.2",
"vue3-contextmenu": "^0.2.12",
"vue3-text-clamp": "^0.1.1",
"vuedraggable": "^4.1.0",
"xlsx": "^0.18.5"
@ -92,6 +95,9 @@
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0",
"@types/dagre": "^0.7.48",
"@types/file-saver": "^2.0.5",
"@types/leaflet": "^1.9.0",
"@types/leaflet.markercluster": "^1.5.1",
"@types/papaparse": "^5.3.2",
"@types/sortablejs": "^1.13.0",
"@types/tinycolor2": "^1.4.3",

35
packages/nc-gui/pages/[projectType]/map/[viewId]/index.vue

@ -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>

9
packages/nc-gui/pages/index/index/create.vue

@ -37,9 +37,13 @@ const formState = reactive({
title: '',
})
const creating = ref(false)
const createProject = async () => {
$e('a:project:create:xcdb')
try {
creating.value = true
const result = await api.project.create({
title: formState.title,
})
@ -47,6 +51,8 @@ const createProject = async () => {
await navigateTo(`/nc/${result.id}`)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
creating.value = false
}
}
@ -90,7 +96,8 @@ onMounted(async () => {
</a-form-item>
<div class="text-center">
<button class="scaling-btn bg-opacity-100" type="submit">
<a-spin v-if="creating" spinning />
<button v-else class="scaling-btn bg-opacity-100" type="submit">
<span class="flex items-center gap-2">
<MaterialSymbolsRocketLaunchOutline />
{{ $t('general.create') }}

2
packages/nc-gui/utils/cell.ts

@ -27,6 +27,7 @@ export const isCurrency = (column: ColumnType) => column.uidt === UITypes.Curren
export const isPhoneNumber = (column: ColumnType) => column.uidt === UITypes.PhoneNumber
export const isDecimal = (column: ColumnType) => column.uidt === UITypes.Decimal
export const isDuration = (column: ColumnType) => column.uidt === UITypes.Duration
export const isGeoData = (column: ColumnType) => column.uidt === UITypes.GeoData
export const isPercent = (column: ColumnType) => column.uidt === UITypes.Percent
export const isSpecificDBType = (column: ColumnType) => column.uidt === UITypes.SpecificDBType
export const isAutoSaved = (column: ColumnType) =>
@ -43,6 +44,7 @@ export const isAutoSaved = (column: ColumnType) =>
UITypes.AutoNumber,
UITypes.SpecificDBType,
UITypes.Geometry,
UITypes.GeoData,
UITypes.Duration,
].includes(column.uidt as UITypes)

6
packages/nc-gui/utils/columnUtils.ts

@ -8,6 +8,7 @@ import TextSubject from '~icons/mdi/text-subject'
import JSONIcon from '~icons/mdi/code-json'
import SpecificDBTypeIcon from '~icons/mdi/database-settings'
import Attachment from '~icons/mdi/attachment'
import Marker from '~icons/mdi/map-marker'
import CheckboxMarkedOutline from '~icons/mdi/checkbox-marked-outline'
import FormatListBulletedSquare from '~icons/mdi/format-list-bulleted-square'
import ArrowDownDropCircle from '~icons/mdi/arrow-down-drop-circle'
@ -141,6 +142,11 @@ const uiTypes = [
name: UITypes.Geometry,
icon: RulerSquareCompass,
},
{
name: UITypes.GeoData,
icon: Marker,
},
{
name: UITypes.JSON,
icon: JSONIcon,

3
packages/nc-gui/utils/geoDataUtils.ts

@ -0,0 +1,3 @@
const latLongToJoinedString = (lat: number, long: number) => `${lat.toFixed(7)};${long.toFixed(7)}`
export { latLongToJoinedString }

1
packages/nc-gui/utils/index.ts

@ -20,4 +20,5 @@ export * from './userUtils'
export * from './stringUtils'
export * from './memStorage'
export * from './browserUtils'
export * from './geoDataUtils'
export * from './mimeTypeUtils'

693
packages/nc-gui/utils/mimeTypeUtils.ts

@ -1,694 +1,3 @@
const mimeTypes: Record<string, string> = {
'123': 'application/vnd.lotus-1-2-3',
'x3d': 'application/vnd.hzn-3d-crossword',
'3gp': 'video/3gpp',
'3g2': 'video/3gpp2',
'mseq': 'application/vnd.mseq',
'pwn': 'application/vnd.3m.post-it-notes',
'plb': 'application/vnd.3gpp.pic-bw-large',
'psb': 'application/vnd.3gpp.pic-bw-small',
'pvb': 'application/vnd.3gpp.pic-bw-var',
'tcap': 'application/vnd.3gpp2.tcap',
'7z': 'application/x-7z-compressed',
'abw': 'application/x-abiword',
'ace': 'application/x-ace-compressed',
'acc': 'application/vnd.americandynamics.acc',
'acu': 'application/vnd.acucobol',
'atc': 'application/vnd.acucorp',
'adp': 'audio/adpcm',
'aab': 'application/x-authorware-bin',
'aam': 'application/x-authorware-map',
'aas': 'application/x-authorware-seg',
'air': 'application/vnd.adobe.air-application-installer-package+zip',
'swf': 'application/x-shockwave-flash',
'fxp': 'application/vnd.adobe.fxp',
'pdf': 'application/pdf',
'ppd': 'application/vnd.cups-ppd',
'dir': 'application/x-director',
'xdp': 'application/vnd.adobe.xdp+xml',
'xfdf': 'application/vnd.adobe.xfdf',
'aac': 'audio/x-aac',
'ahead': 'application/vnd.ahead.space',
'azf': 'application/vnd.airzip.filesecure.azf',
'azs': 'application/vnd.airzip.filesecure.azs',
'azw': 'application/vnd.amazon.ebook',
'ami': 'application/vnd.amiga.ami',
'/A': 'application/andrew-inset',
'apk': 'application/vnd.android.package-archive',
'cii': 'application/vnd.anser-web-certificate-issue-initiation',
'fti': 'application/vnd.anser-web-funds-transfer-initiation',
'atx': 'application/vnd.antix.game-component',
'dmg': 'application/x-apple-diskimage',
'mpkg': 'application/vnd.apple.installer+xml',
'aw': 'application/applixware',
'les': 'application/vnd.hhe.lesson-player',
'swi': 'application/vnd.aristanetworks.swi',
's': 'text/x-asm',
'atomcat': 'application/atomcat+xml',
'atomsvc': 'application/atomsvc+xml',
'atom, .xml': 'application/atom+xml',
'ac': 'application/pkix-attr-cert',
'aif': 'audio/x-aiff',
'avi': 'video/x-msvideo',
'aep': 'application/vnd.audiograph',
'dxf': 'image/vnd.dxf',
'dwf': 'model/vnd.dwf',
'par': 'text/plain-bas',
'bcpio': 'application/x-bcpio',
'bin': 'application/octet-stream',
'bmp': 'image/bmp',
'torrent': 'application/x-bittorrent',
'cod': 'application/vnd.rim.cod',
'mpm': 'application/vnd.blueice.multipass',
'bmi': 'application/vnd.bmi',
'sh': 'application/x-sh',
'btif': 'image/prs.btif',
'rep': 'application/vnd.businessobjects',
'bz': 'application/x-bzip',
'bz2': 'application/x-bzip2',
'csh': 'application/x-csh',
'c': 'text/x-c',
'cdxml': 'application/vnd.chemdraw+xml',
'css': 'text/css',
'cdx': 'chemical/x-cdx',
'cml': 'chemical/x-cml',
'csml': 'chemical/x-csml',
'cdbcmsg': 'application/vnd.contact.cmsg',
'cla': 'application/vnd.claymore',
'c4g': 'application/vnd.clonk.c4group',
'sub': 'image/vnd.dvb.subtitle',
'cdmia': 'application/cdmi-capability',
'cdmic': 'application/cdmi-container',
'cdmid': 'application/cdmi-domain',
'cdmio': 'application/cdmi-object',
'cdmiq': 'application/cdmi-queue',
'c11amc': 'application/vnd.cluetrust.cartomobile-config',
'c11amz': 'application/vnd.cluetrust.cartomobile-config-pkg',
'ras': 'image/x-cmu-raster',
'dae': 'model/vnd.collada+xml',
'csv': 'text/csv',
'cpt': 'application/mac-compactpro',
'wmlc': 'application/vnd.wap.wmlc',
'cgm': 'image/cgm',
'ice': 'x-conference/x-cooltalk',
'cmx': 'image/x-cmx',
'xar': 'application/vnd.xara',
'cmc': 'application/vnd.cosmocaller',
'cpio': 'application/x-cpio',
'clkx': 'application/vnd.crick.clicker',
'clkk': 'application/vnd.crick.clicker.keyboard',
'clkp': 'application/vnd.crick.clicker.palette',
'clkt': 'application/vnd.crick.clicker.template',
'clkw': 'application/vnd.crick.clicker.wordbank',
'wbs': 'application/vnd.criticaltools.wbs+xml',
'cryptonote': 'application/vnd.rig.cryptonote',
'cif': 'chemical/x-cif',
'cmdf': 'chemical/x-cmdf',
'cu': 'application/cu-seeme',
'cww': 'application/prs.cww',
'curl': 'text/vnd.curl',
'dcurl': 'text/vnd.curl.dcurl',
'mcurl': 'text/vnd.curl.mcurl',
'scurl': 'text/vnd.curl.scurl',
'car': 'application/vnd.curl.car',
'pcurl': 'application/vnd.curl.pcurl',
'cmp': 'application/vnd.yellowriver-custom-menu',
'dssc': 'application/dssc+der',
'xdssc': 'application/dssc+xml',
'deb': 'application/x-debian-package',
'uva': 'audio/vnd.dece.audio',
'uvi': 'image/vnd.dece.graphic',
'uvh': 'video/vnd.dece.hd',
'uvm': 'video/vnd.dece.mobile',
'uvu': 'video/vnd.uvvu.mp4',
'uvp': 'video/vnd.dece.pd',
'uvs': 'video/vnd.dece.sd',
'uvv': 'video/vnd.dece.video',
'dvi': 'application/x-dvi',
'seed': 'application/vnd.fdsn.seed',
'dtb': 'application/x-dtbook+xml',
'res': 'application/x-dtbresource+xml',
'ait': 'application/vnd.dvb.ait',
'svc': 'application/vnd.dvb.service',
'eol': 'audio/vnd.digital-winds',
'djvu': 'image/vnd.djvu',
'dtd': 'application/xml-dtd',
'mlp': 'application/vnd.dolby.mlp',
'wad': 'application/x-doom',
'dpg': 'application/vnd.dpgraph',
'dra': 'audio/vnd.dra',
'dfac': 'application/vnd.dreamfactory',
'dts': 'audio/vnd.dts',
'dtshd': 'audio/vnd.dts.hd',
'dwg': 'image/vnd.dwg',
'geo': 'application/vnd.dynageo',
'es': 'application/ecmascript',
'mag': 'application/vnd.ecowin.chart',
'mmr': 'image/vnd.fujixerox.edmics-mmr',
'rlc': 'image/vnd.fujixerox.edmics-rlc',
'exi': 'application/exi',
'mgz': 'application/vnd.proteus.magazine',
'epub': 'application/epub+zip',
'eml': 'message/rfc822',
'nml': 'application/vnd.enliven',
'xpr': 'application/vnd.is-xpr',
'xif': 'image/vnd.xiff',
'xfdl': 'application/vnd.xfdl',
'emma': 'application/emma+xml',
'ez2': 'application/vnd.ezpix-album',
'ez3': 'application/vnd.ezpix-package',
'fst': 'image/vnd.fst',
'fvt': 'video/vnd.fvt',
'fbs': 'image/vnd.fastbidsheet',
'fe_launch': 'application/vnd.denovo.fcselayout-link',
'f4v': 'video/x-f4v',
'flv': 'video/x-flv',
'fpx': 'image/vnd.fpx',
'npx': 'image/vnd.net-fpx',
'flx': 'text/vnd.fmi.flexstor',
'fli': 'video/x-fli',
'ftc': 'application/vnd.fluxtime.clip',
'fdf': 'application/vnd.fdf',
'f': 'text/x-fortran',
'mif': 'application/vnd.mif',
'fm': 'application/vnd.framemaker',
'fh': 'image/x-freehand',
'fsc': 'application/vnd.fsc.weblaunch',
'fnc': 'application/vnd.frogans.fnc',
'ltf': 'application/vnd.frogans.ltf',
'ddd': 'application/vnd.fujixerox.ddd',
'xdw': 'application/vnd.fujixerox.docuworks',
'xbd': 'application/vnd.fujixerox.docuworks.binder',
'oas': 'application/vnd.fujitsu.oasys',
'oa2': 'application/vnd.fujitsu.oasys2',
'oa3': 'application/vnd.fujitsu.oasys3',
'fg5': 'application/vnd.fujitsu.oasysgp',
'bh2': 'application/vnd.fujitsu.oasysprs',
'spl': 'application/x-futuresplash',
'fzs': 'application/vnd.fuzzysheet',
'g3': 'image/g3fax',
'gmx': 'application/vnd.gmx',
'gtw': 'model/vnd.gtw',
'txd': 'application/vnd.genomatix.tuxedo',
'ggb': 'application/vnd.geogebra.file',
'ggt': 'application/vnd.geogebra.tool',
'gdl': 'model/vnd.gdl',
'gex': 'application/vnd.geometry-explorer',
'gxt': 'application/vnd.geonext',
'g2w': 'application/vnd.geoplan',
'g3w': 'application/vnd.geospace',
'gsf': 'application/x-font-ghostscript',
'bdf': 'application/x-font-bdf',
'gtar': 'application/x-gtar',
'texinfo': 'application/x-texinfo',
'gnumeric': 'application/x-gnumeric',
'kml': 'application/vnd.google-earth.kml+xml',
'kmz': 'application/vnd.google-earth.kmz',
'gpx': 'application/gpx+xml',
'gqf': 'application/vnd.grafeq',
'gif': 'image/gif',
'gv': 'text/vnd.graphviz',
'gac': 'application/vnd.groove-account',
'ghf': 'application/vnd.groove-help',
'gim': 'application/vnd.groove-identity-message',
'grv': 'application/vnd.groove-injector',
'gtm': 'application/vnd.groove-tool-message',
'tpl': 'application/vnd.groove-tool-template',
'vcg': 'application/vnd.groove-vcard',
'h261': 'video/h261',
'h263': 'video/h263',
'h264': 'video/h264',
'hpid': 'application/vnd.hp-hpid',
'hps': 'application/vnd.hp-hps',
'hdf': 'application/x-hdf',
'rip': 'audio/vnd.rip',
'hbci': 'application/vnd.hbci',
'jlt': 'application/vnd.hp-jlyt',
'pcl': 'application/vnd.hp-pcl',
'hpgl': 'application/vnd.hp-hpgl',
'hvs': 'application/vnd.yamaha.hv-script',
'hvd': 'application/vnd.yamaha.hv-dic',
'hvp': 'application/vnd.yamaha.hv-voice',
'sfd-hdstx': 'application/vnd.hydrostatix.sof-data',
'stk': 'application/hyperstudio',
'hal': 'application/vnd.hal+xml',
'html': 'text/html',
'irm': 'application/vnd.ibm.rights-management',
'sc': 'application/vnd.ibm.secure-container',
'ics': 'text/calendar',
'icc': 'application/vnd.iccprofile',
'ico': 'image/x-icon',
'igl': 'application/vnd.igloader',
'ief': 'image/ief',
'ivp': 'application/vnd.immervision-ivp',
'ivu': 'application/vnd.immervision-ivu',
'rif': 'application/reginfo+xml',
'3dml': 'text/vnd.in3d.3dml',
'spot': 'text/vnd.in3d.spot',
'igs': 'model/iges',
'i2g': 'application/vnd.intergeo',
'cdy': 'application/vnd.cinderella',
'xpw': 'application/vnd.intercon.formnet',
'fcs': 'application/vnd.isac.fcs',
'ipfix': 'application/ipfix',
'cer': 'application/pkix-cert',
'pki': 'application/pkixcmp',
'crl': 'application/pkix-crl',
'pkipath': 'application/pkix-pkipath',
'igm': 'application/vnd.insors.igm',
'rcprofile': 'application/vnd.ipunplugged.rcprofile',
'irp': 'application/vnd.irepository.package+xml',
'jad': 'text/vnd.sun.j2me.app-descriptor',
'jar': 'application/java-archive',
'class': 'application/java-vm',
'jnlp': 'application/x-java-jnlp-file',
'ser': 'application/java-serialized-object',
'java': 'text/x-java-source,java',
'js': 'application/javascript',
'json': 'application/json',
'joda': 'application/vnd.joost.joda-archive',
'jpm': 'video/jpm',
'jpeg': 'image/x-citrix-jpeg',
'jpg': 'image/x-citrix-jpeg',
'pjpeg': 'image/pjpeg',
'jpgv': 'video/jpeg',
'ktz': 'application/vnd.kahootz',
'mmd': 'application/vnd.chipnuts.karaoke-mmd',
'karbon': 'application/vnd.kde.karbon',
'chrt': 'application/vnd.kde.kchart',
'kfo': 'application/vnd.kde.kformula',
'flw': 'application/vnd.kde.kivio',
'kon': 'application/vnd.kde.kontour',
'kpr': 'application/vnd.kde.kpresenter',
'ksp': 'application/vnd.kde.kspread',
'kwd': 'application/vnd.kde.kword',
'htke': 'application/vnd.kenameaapp',
'kia': 'application/vnd.kidspiration',
'kne': 'application/vnd.kinar',
'sse': 'application/vnd.kodak-descriptor',
'lasxml': 'application/vnd.las.las+xml',
'latex': 'application/x-latex',
'lbd': 'application/vnd.llamagraphics.life-balance.desktop',
'lbe': 'application/vnd.llamagraphics.life-balance.exchange+xml',
'jam': 'application/vnd.jam',
'apr': 'application/vnd.lotus-approach',
'pre': 'application/vnd.lotus-freelance',
'nsf': 'application/vnd.lotus-notes',
'org': 'application/vnd.lotus-organizer',
'scm': 'application/vnd.lotus-screencam',
'lwp': 'application/vnd.lotus-wordpro',
'lvp': 'audio/vnd.lucent.voice',
'm3u': 'audio/x-mpegurl',
'm4v': 'video/x-m4v',
'hqx': 'application/mac-binhex40',
'portpkg': 'application/vnd.macports.portpkg',
'mgp': 'application/vnd.osgeo.mapguide.package',
'mrc': 'application/marc',
'mrcx': 'application/marcxml+xml',
'mxf': 'application/mxf',
'nbp': 'application/vnd.wolfram.player',
'ma': 'application/mathematica',
'mathml': 'application/mathml+xml',
'mbox': 'application/mbox',
'mc1': 'application/vnd.medcalcdata',
'mscml': 'application/mediaservercontrol+xml',
'cdkey': 'application/vnd.mediastation.cdkey',
'mwf': 'application/vnd.mfer',
'mfm': 'application/vnd.mfmp',
'msh': 'model/mesh',
'mads': 'application/mads+xml',
'mets': 'application/mets+xml',
'mods': 'application/mods+xml',
'meta4': 'application/metalink4+xml',
'mcd': 'application/vnd.mcd',
'flo': 'application/vnd.micrografx.flo',
'igx': 'application/vnd.micrografx.igx',
'es3': 'application/vnd.eszigno3+xml',
'mdb': 'application/x-msaccess',
'asf': 'video/x-ms-asf',
'exe': 'application/x-msdownload',
'cil': 'application/vnd.ms-artgalry',
'cab': 'application/vnd.ms-cab-compressed',
'ims': 'application/vnd.ms-ims',
'application': 'application/x-ms-application',
'clp': 'application/x-msclip',
'mdi': 'image/vnd.ms-modi',
'eot': 'application/vnd.ms-fontobject',
'xls': 'application/vnd.ms-excel',
'xlam': 'application/vnd.ms-excel.addin.macroenabled.12',
'xlsb': 'application/vnd.ms-excel.sheet.binary.macroenabled.12',
'xltm': 'application/vnd.ms-excel.template.macroenabled.12',
'xlsm': 'application/vnd.ms-excel.sheet.macroenabled.12',
'chm': 'application/vnd.ms-htmlhelp',
'crd': 'application/x-mscardfile',
'lrm': 'application/vnd.ms-lrm',
'mvb': 'application/x-msmediaview',
'mny': 'application/x-msmoney',
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'sldx': 'application/vnd.openxmlformats-officedocument.presentationml.slide',
'ppsx': 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'potx': 'application/vnd.openxmlformats-officedocument.presentationml.template',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xltx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'dotx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'obd': 'application/x-msbinder',
'thmx': 'application/vnd.ms-officetheme',
'onetoc': 'application/onenote',
'pya': 'audio/vnd.ms-playready.media.pya',
'pyv': 'video/vnd.ms-playready.media.pyv',
'ppt': 'application/vnd.ms-powerpoint',
'ppam': 'application/vnd.ms-powerpoint.addin.macroenabled.12',
'sldm': 'application/vnd.ms-powerpoint.slide.macroenabled.12',
'pptm': 'application/vnd.ms-powerpoint.presentation.macroenabled.12',
'ppsm': 'application/vnd.ms-powerpoint.slideshow.macroenabled.12',
'potm': 'application/vnd.ms-powerpoint.template.macroenabled.12',
'mpp': 'application/vnd.ms-project',
'pub': 'application/x-mspublisher',
'scd': 'application/x-msschedule',
'xap': 'application/x-silverlight-app',
'stl': 'application/vnd.ms-pki.stl',
'cat': 'application/vnd.ms-pki.seccat',
'vsd': 'application/vnd.visio',
'vsdx': 'application/vnd.visio2013',
'wm': 'video/x-ms-wm',
'wma': 'audio/x-ms-wma',
'wax': 'audio/x-ms-wax',
'wmx': 'video/x-ms-wmx',
'wmd': 'application/x-ms-wmd',
'wpl': 'application/vnd.ms-wpl',
'wmz': 'application/x-ms-wmz',
'wmv': 'video/x-ms-wmv',
'wvx': 'video/x-ms-wvx',
'wmf': 'application/x-msmetafile',
'trm': 'application/x-msterminal',
'doc': 'application/msword',
'docm': 'application/vnd.ms-word.document.macroenabled.12',
'dotm': 'application/vnd.ms-word.template.macroenabled.12',
'wri': 'application/x-mswrite',
'wps': 'application/vnd.ms-works',
'xbap': 'application/x-ms-xbap',
'xps': 'application/vnd.ms-xpsdocument',
'mid': 'audio/midi',
'mpy': 'application/vnd.ibm.minipay',
'afp': 'application/vnd.ibm.modcap',
'rms': 'application/vnd.jcp.javame.midlet-rms',
'tmo': 'application/vnd.tmobile-livetv',
'prc': 'application/x-mobipocket-ebook',
'mbk': 'application/vnd.mobius.mbk',
'dis': 'application/vnd.mobius.dis',
'plc': 'application/vnd.mobius.plc',
'mqy': 'application/vnd.mobius.mqy',
'msl': 'application/vnd.mobius.msl',
'txf': 'application/vnd.mobius.txf',
'daf': 'application/vnd.mobius.daf',
'fly': 'text/vnd.fly',
'mpc': 'application/vnd.mophun.certificate',
'mpn': 'application/vnd.mophun.application',
'mj2': 'video/mj2',
'mpga': 'audio/mpeg',
'mxu': 'video/vnd.mpegurl',
'mpeg': 'video/mpeg',
'm21': 'application/mp21',
'mp4a': 'audio/mp4',
'mp4': 'application/mp4',
'm3u8': 'application/vnd.apple.mpegurl',
'mus': 'application/vnd.musician',
'msty': 'application/vnd.muvee.style',
'mxml': 'application/xv+xml',
'ngdat': 'application/vnd.nokia.n-gage.data',
'n-gage': 'application/vnd.nokia.n-gage.symbian.install',
'ncx': 'application/x-dtbncx+xml',
'nc': 'application/x-netcdf',
'nlu': 'application/vnd.neurolanguage.nlu',
'dna': 'application/vnd.dna',
'nnd': 'application/vnd.noblenet-directory',
'nns': 'application/vnd.noblenet-sealer',
'nnw': 'application/vnd.noblenet-web',
'rpst': 'application/vnd.nokia.radio-preset',
'rpss': 'application/vnd.nokia.radio-presets',
'n3': 'text/n3',
'edm': 'application/vnd.novadigm.edm',
'edx': 'application/vnd.novadigm.edx',
'ext': 'application/vnd.novadigm.ext',
'gph': 'application/vnd.flographit',
'ecelp4800': 'audio/vnd.nuera.ecelp4800',
'ecelp7470': 'audio/vnd.nuera.ecelp7470',
'ecelp9600': 'audio/vnd.nuera.ecelp9600',
'oda': 'application/oda',
'ogx': 'application/ogg',
'oga': 'audio/ogg',
'ogv': 'video/ogg',
'dd2': 'application/vnd.oma.dd2+xml',
'oth': 'application/vnd.oasis.opendocument.text-web',
'opf': 'application/oebps-package+xml',
'qbo': 'application/vnd.intu.qbo',
'oxt': 'application/vnd.openofficeorg.extension',
'osf': 'application/vnd.yamaha.openscoreformat',
'weba': 'audio/webm',
'webm': 'video/webm',
'odc': 'application/vnd.oasis.opendocument.chart',
'otc': 'application/vnd.oasis.opendocument.chart-template',
'odb': 'application/vnd.oasis.opendocument.database',
'odf': 'application/vnd.oasis.opendocument.formula',
'odft': 'application/vnd.oasis.opendocument.formula-template',
'odg': 'application/vnd.oasis.opendocument.graphics',
'otg': 'application/vnd.oasis.opendocument.graphics-template',
'odi': 'application/vnd.oasis.opendocument.image',
'oti': 'application/vnd.oasis.opendocument.image-template',
'odp': 'application/vnd.oasis.opendocument.presentation',
'otp': 'application/vnd.oasis.opendocument.presentation-template',
'ods': 'application/vnd.oasis.opendocument.spreadsheet',
'ots': 'application/vnd.oasis.opendocument.spreadsheet-template',
'odt': 'application/vnd.oasis.opendocument.text',
'odm': 'application/vnd.oasis.opendocument.text-master',
'ott': 'application/vnd.oasis.opendocument.text-template',
'ktx': 'image/ktx',
'sxc': 'application/vnd.sun.xml.calc',
'stc': 'application/vnd.sun.xml.calc.template',
'sxd': 'application/vnd.sun.xml.draw',
'std': 'application/vnd.sun.xml.draw.template',
'sxi': 'application/vnd.sun.xml.impress',
'sti': 'application/vnd.sun.xml.impress.template',
'sxm': 'application/vnd.sun.xml.math',
'sxw': 'application/vnd.sun.xml.writer',
'sxg': 'application/vnd.sun.xml.writer.global',
'stw': 'application/vnd.sun.xml.writer.template',
'otf': 'application/x-font-otf',
'osfpvg': 'application/vnd.yamaha.openscoreformat.osfpvg+xml',
'dp': 'application/vnd.osgi.dp',
'pdb': 'application/vnd.palm',
'p': 'text/x-pascal',
'paw': 'application/vnd.pawaafile',
'pclxl': 'application/vnd.hp-pclxl',
'efif': 'application/vnd.picsel',
'pcx': 'image/x-pcx',
'psd': 'image/vnd.adobe.photoshop',
'prf': 'application/pics-rules',
'pic': 'image/x-pict',
'chat': 'application/x-chat',
'p10': 'application/pkcs10',
'p12': 'application/x-pkcs12',
'p7m': 'application/pkcs7-mime',
'p7s': 'application/pkcs7-signature',
'p7r': 'application/x-pkcs7-certreqresp',
'p7b': 'application/x-pkcs7-certificates',
'p8': 'application/pkcs8',
'plf': 'application/vnd.pocketlearn',
'pnm': 'image/x-portable-anymap',
'pbm': 'image/x-portable-bitmap',
'pcf': 'application/x-font-pcf',
'pfr': 'application/font-tdpfr',
'pgn': 'application/x-chess-pgn',
'pgm': 'image/x-portable-graymap',
'png': 'image/x-png',
'ppm': 'image/x-portable-pixmap',
'pskcxml': 'application/pskc+xml',
'pml': 'application/vnd.ctc-posml',
'ai': 'application/postscript',
'pfa': 'application/x-font-type1',
'pbd': 'application/vnd.powerbuilder6',
'pgp': 'application/pgp-signature',
'box': 'application/vnd.previewsystems.box',
'ptid': 'application/vnd.pvi.ptid1',
'pls': 'application/pls+xml',
'str': 'application/vnd.pg.format',
'ei6': 'application/vnd.pg.osasli',
'dsc': 'text/prs.lines.tag',
'psf': 'application/x-font-linux-psf',
'qps': 'application/vnd.publishare-delta-tree',
'wg': 'application/vnd.pmi.widget',
'qxd': 'application/vnd.quark.quarkxpress',
'esf': 'application/vnd.epson.esf',
'msf': 'application/vnd.epson.msf',
'ssf': 'application/vnd.epson.ssf',
'qam': 'application/vnd.epson.quickanime',
'qfx': 'application/vnd.intu.qfx',
'qt': 'video/quicktime',
'rar': 'application/x-rar-compressed',
'ram': 'audio/x-pn-realaudio',
'rmp': 'audio/x-pn-realaudio-plugin',
'rsd': 'application/rsd+xml',
'rm': 'application/vnd.rn-realmedia',
'bed': 'application/vnd.realvnc.bed',
'mxl': 'application/vnd.recordare.musicxml',
'musicxml': 'application/vnd.recordare.musicxml+xml',
'rnc': 'application/relax-ng-compact-syntax',
'rdz': 'application/vnd.data-vision.rdz',
'rdf': 'application/rdf+xml',
'rp9': 'application/vnd.cloanto.rp9',
'jisp': 'application/vnd.jisp',
'rtf': 'application/rtf',
'rtx': 'text/richtext',
'link66': 'application/vnd.route66.link66+xml',
'rss, .xml': 'application/rss+xml',
'shf': 'application/shf+xml',
'st': 'application/vnd.sailingtracker.track',
'svg': 'image/svg+xml',
'sus': 'application/vnd.sus-calendar',
'sru': 'application/sru+xml',
'setpay': 'application/set-payment-initiation',
'setreg': 'application/set-registration-initiation',
'sema': 'application/vnd.sema',
'semd': 'application/vnd.semd',
'semf': 'application/vnd.semf',
'see': 'application/vnd.seemail',
'snf': 'application/x-font-snf',
'spq': 'application/scvp-vp-request',
'spp': 'application/scvp-vp-response',
'scq': 'application/scvp-cv-request',
'scs': 'application/scvp-cv-response',
'sdp': 'application/sdp',
'etx': 'text/x-setext',
'movie': 'video/x-sgi-movie',
'ifm': 'application/vnd.shana.informed.formdata',
'itp': 'application/vnd.shana.informed.formtemplate',
'iif': 'application/vnd.shana.informed.interchange',
'ipk': 'application/vnd.shana.informed.package',
'tfi': 'application/thraud+xml',
'shar': 'application/x-shar',
'rgb': 'image/x-rgb',
'slt': 'application/vnd.epson.salt',
'aso': 'application/vnd.accpac.simply.aso',
'imp': 'application/vnd.accpac.simply.imp',
'twd': 'application/vnd.simtech-mindmapper',
'csp': 'application/vnd.commonspace',
'saf': 'application/vnd.yamaha.smaf-audio',
'mmf': 'application/vnd.smaf',
'spf': 'application/vnd.yamaha.smaf-phrase',
'teacher': 'application/vnd.smart.teacher',
'svd': 'application/vnd.svd',
'rq': 'application/sparql-query',
'srx': 'application/sparql-results+xml',
'gram': 'application/srgs',
'grxml': 'application/srgs+xml',
'ssml': 'application/ssml+xml',
'skp': 'application/vnd.koan',
'sgml': 'text/sgml',
'sdc': 'application/vnd.stardivision.calc',
'sda': 'application/vnd.stardivision.draw',
'sdd': 'application/vnd.stardivision.impress',
'smf': 'application/vnd.stardivision.math',
'sdw': 'application/vnd.stardivision.writer',
'sgl': 'application/vnd.stardivision.writer-global',
'sm': 'application/vnd.stepmania.stepchart',
'sit': 'application/x-stuffit',
'sitx': 'application/x-stuffitx',
'sdkm': 'application/vnd.solent.sdkm+xml',
'xo': 'application/vnd.olpc-sugar',
'au': 'audio/basic',
'wqd': 'application/vnd.wqd',
'sis': 'application/vnd.symbian.install',
'smi': 'application/smil+xml',
'xsm': 'application/vnd.syncml+xml',
'bdm': 'application/vnd.syncml.dm+wbxml',
'xdm': 'application/vnd.syncml.dm+xml',
'sv4cpio': 'application/x-sv4cpio',
'sv4crc': 'application/x-sv4crc',
'sbml': 'application/sbml+xml',
'tsv': 'text/tab-separated-values',
'tiff': 'image/tiff',
'tao': 'application/vnd.tao.intent-module-archive',
'tar': 'application/x-tar',
'tcl': 'application/x-tcl',
'tex': 'application/x-tex',
'tfm': 'application/x-tex-tfm',
'tei': 'application/tei+xml',
'txt': 'text/plain',
'dxp': 'application/vnd.spotfire.dxp',
'sfs': 'application/vnd.spotfire.sfs',
'tsd': 'application/timestamped-data',
'tpt': 'application/vnd.trid.tpt',
'mxs': 'application/vnd.triscape.mxs',
't': 'text/troff',
'tra': 'application/vnd.trueapp',
'ttf': 'application/x-font-ttf',
'ttl': 'text/turtle',
'umj': 'application/vnd.umajin',
'uoml': 'application/vnd.uoml+xml',
'unityweb': 'application/vnd.unity',
'ufd': 'application/vnd.ufdl',
'uri': 'text/uri-list',
'utz': 'application/vnd.uiq.theme',
'ustar': 'application/x-ustar',
'uu': 'text/x-uuencode',
'vcs': 'text/x-vcalendar',
'vcf': 'text/x-vcard',
'vcd': 'application/x-cdlink',
'vsf': 'application/vnd.vsf',
'wrl': 'model/vrml',
'vcx': 'application/vnd.vcx',
'mts': 'model/vnd.mts',
'vtu': 'model/vnd.vtu',
'vis': 'application/vnd.visionary',
'viv': 'video/vnd.vivo',
'ccxml': 'application/ccxml+xml,',
'vxml': 'application/voicexml+xml',
'src': 'application/x-wais-source',
'wbxml': 'application/vnd.wap.wbxml',
'wbmp': 'image/vnd.wap.wbmp',
'wav': 'audio/x-wav',
'davmount': 'application/davmount+xml',
'woff': 'application/x-font-woff',
'wspolicy': 'application/wspolicy+xml',
'webp': 'image/webp',
'wtb': 'application/vnd.webturbo',
'wgt': 'application/widget',
'hlp': 'application/winhlp',
'wml': 'text/vnd.wap.wml',
'wmls': 'text/vnd.wap.wmlscript',
'wmlsc': 'application/vnd.wap.wmlscriptc',
'wpd': 'application/vnd.wordperfect',
'stf': 'application/vnd.wt.stf',
'wsdl': 'application/wsdl+xml',
'xbm': 'image/x-xbitmap',
'xpm': 'image/x-xpixmap',
'xwd': 'image/x-xwindowdump',
'der': 'application/x-x509-ca-cert',
'fig': 'application/x-xfig',
'xhtml': 'application/xhtml+xml',
'xml': 'application/xml',
'xdf': 'application/xcap-diff+xml',
'xenc': 'application/xenc+xml',
'xer': 'application/patch-ops-error+xml',
'rl': 'application/resource-lists+xml',
'rs': 'application/rls-services+xml',
'rld': 'application/resource-lists-diff+xml',
'xslt': 'application/xslt+xml',
'xop': 'application/xop+xml',
'xpi': 'application/x-xpinstall',
'xspf': 'application/xspf+xml',
'xul': 'application/vnd.mozilla.xul+xml',
'xyz': 'chemical/x-xyz',
'yaml': 'text/yaml',
'yang': 'application/yang',
'yin': 'application/yin+xml',
'zir': 'application/vnd.zul',
'zip': 'application/zip',
'zmm': 'application/vnd.handheld-entertainment+xml',
'zaz': 'application/vnd.zzazz.deck+xml',
}
const mimeIcons = {
pdf: 'mdi-pdf-box',
@ -711,4 +20,4 @@ const mimeIcons = {
xlsm: 'mdi-file-excel-outline',
}
export { mimeTypes, mimeIcons }
export { mimeIcons }

3
packages/nc-gui/utils/viewUtils.ts

@ -6,6 +6,7 @@ import MdiFormIcon from '~icons/mdi/form-select'
import MdiCalendarIcon from '~icons/mdi/calendar'
import MdiGalleryIcon from '~icons/mdi/camera-image'
import MdiKanbanIcon from '~icons/mdi/tablet-dashboard'
import MdiMapIcon from '~icons/mdi/map-outline'
import MdiEyeIcon from '~icons/mdi/eye-circle-outline'
import type { Language } from '~/lib'
@ -14,6 +15,7 @@ export const viewIcons: Record<number | string, { icon: any; color: string }> =
[ViewTypes.FORM]: { icon: MdiFormIcon, color: themeV2Colors.pink['500'] },
calendar: { icon: MdiCalendarIcon, color: 'purple' },
[ViewTypes.GALLERY]: { icon: MdiGalleryIcon, color: 'orange' },
[ViewTypes.MAP]: { icon: MdiMapIcon, color: 'blue' },
[ViewTypes.KANBAN]: { icon: MdiKanbanIcon, color: 'green' },
view: { icon: MdiEyeIcon, color: 'blue' },
}
@ -23,6 +25,7 @@ export const viewTypeAlias: Record<number, string> = {
[ViewTypes.FORM]: 'form',
[ViewTypes.GALLERY]: 'gallery',
[ViewTypes.KANBAN]: 'kanban',
[ViewTypes.MAP]: 'map',
}
export const isRtlLang = (lang: keyof typeof Language) => ['fa', 'ar'].includes(lang)

55
packages/noco-docs/content/en/setup-and-usages/column-types.md

@ -10,9 +10,9 @@ menuTitle: 'Column Types'
| Type | Description |
|---|---|
| [ID](#id) | Primary column of the table |
<!-- | [ID](#id) | Primary column of the table | -->
| [LinkToAnotherRecord](#linktoanotherrecord) | Has Many or Many To Many columns |
| [ForeignKey](#foreignkey)| Belongs To relation |
<!-- | [ForeignKey](#foreignkey)| Belongs To relation | -->
| [SingleLineText](#singlelinetext) | For short text |
| [LongText](#longtext) | For lengthy string content |
| [Attachment](#attachment) | File attachment column |
@ -32,22 +32,19 @@ menuTitle: 'Column Types'
| [Duration](#duration)| Duration |
| [Rating](#rating)| Rating |
| [Formula](#formula)| Formula based generated column |
| [Rollup](#rollup)| Performs calculations and aggregations |
| [DateTime](#datetime)| Date & Time selector |
| [QR Code](#qr-code)| QR Code visualization of another referenced column |
| [Barcode](#barcode)| Barcode visualization of another referenced column |
| [Count](#count) | |
|[DateTime](#datetime)| Date & Time selector |
|[CreateTime](#createtime)| |
|[AutoNumber](#autonumber)| |
<!-- | [CreateTime](#createtime)| | -->
| [Geometry](#geometry)| Geometry column |
| [GeoData](#geodata)| GeoData column |
| [Json](#json)| Json column |
| [SpecificDBType](#specificdbtype)| Custom DB type option |
## Database Types
### ID
<!-- ### ID
#### Available Database Types
@ -56,20 +53,20 @@ menuTitle: 'Column Types'
|**MySQL**|_All types are available_|int|
|**PostgreSQL**|_All types are available_|int4|
|**SQL Server**|_All types are available_|int|
|**SQLite**|_All types are available_|integer|
|**SQLite**|_All types are available_|integer| -->
### LinkToAnotherRecord
N/A
For more about Link To Another Record, please visit [here](./link-to-another-record).
### ForeignKey
<!-- ### ForeignKey
#### Available Database Types
|Database| Types|
|-----|----------|
|**MySQL**|_All types are available_|
|**PostgreSQL**|_All types are available_|
|**SQL Server**|_All types are available_|
|**SQLite**|_All types are available_|
|**SQLite**|_All types are available_| -->
### SingleLineText
@ -292,16 +289,10 @@ Encodes the value of a reference column as Barcode. Supported barcode formats: C
* Email
Since it's a virtual column, the cell content (Barcode) cannot be changed directly.
### Count
#### Available Database Types
### Rollup
|Database| Types| Default Type|
|-----|----------|----------|
|**MySQL**|int, smallint, mediumint, bigint, serial|int|
|**PostgreSQL**|int, integer, bigint, bigserial, int2, int4, int8, serial, serial2, serial8, smallint, smallserial|int8|
|**SQL Server**|int, bigint, smallint, tinyint|int|
|**SQLite**|int, integer, tinyint, smallint, mediumint, bigint, int2, int8|integer|
For more about rollup, please visit [here](./rollup).
### DateTime
@ -314,7 +305,7 @@ Since it's a virtual column, the cell content (Barcode) cannot be changed direct
|**SQL Server**|datetime, datetime2, datetimeoffset|datetime|
|**SQLite**|datetime, timestamp|datetime|
### CreateTime
<!-- ### CreateTime
#### Available Database Types
@ -323,7 +314,7 @@ Since it's a virtual column, the cell content (Barcode) cannot be changed direct
|**MySQL**|datetime, timestamp, varchar|datetime|
|**PostgreSQL**|timestamp, timestamp without time zone, timestamptz, timestamp with time zone|datetime|
|**SQL Server**|datetime, datetime2, datetimeoffset|datetime|
|**SQLite**|datetime, timestamp|datetime|
|**SQLite**|datetime, timestamp|datetime| -->
### Geometry
@ -337,6 +328,20 @@ Since it's a virtual column, the cell content (Barcode) cannot be changed direct
|**SQL Server**|geometry|
|**SQLite**|text|
<!-- TODO: add GeoData -->
### JSON
#### Available Database Types
|Database| Types|
|-----|----------|
|**MySQL**|json|
|**PostgreSQL**|json|
|**SQL Server**|text, ntext|
|**SQLite**|text|
### SpecificDBType
#### Available Database Types

81
packages/nocodb-sdk/src/lib/Api.ts

@ -139,7 +139,7 @@ export interface ViewType {
show_system_fields?: boolean;
lock_type?: 'collaborative' | 'locked' | 'personal';
type?: number;
view?: FormType | GridType | GalleryType | KanbanType;
view?: FormType | GridType | GalleryType | KanbanType | MapType;
}
export interface TableInfoType {
@ -398,6 +398,33 @@ export interface KanbanType {
meta?: string | object;
}
export interface GeoLocationType {
/** @format double */
latitude?: number;
/** @format double */
longitude?: number;
}
export interface MapType {
id?: string;
title?: string;
alias?: string;
initial_geo_position?: GeoLocationType;
fk_model_id?: string;
fk_view_id?: string;
fk_geo_data_col_id?: string | null;
columns?: MapColumnType[];
meta?: string | object;
}
export interface MapColumnType {
id?: string;
label?: string;
help?: string;
fk_col_id?: string;
fk_gallery_id?: string;
}
export interface FormType {
id?: string;
title?: string;
@ -590,6 +617,7 @@ export interface NormalColumnRequestType {
| 'Collaborator'
| 'Date'
| 'Year'
| 'GeoData'
| 'Time'
| 'PhoneNumber'
| 'Email'
@ -2691,6 +2719,57 @@ export class Api<
format: 'json',
...params,
}),
/**
* No description
*
* @tags DB view
* @name MapCreate
* @request POST:/api/v1/db/meta/tables/{tableId}/maps
* @response `200` `object` OK
*/
mapCreate: (tableId: string, data: MapType, params: RequestParams = {}) =>
this.request<object, any>({
path: `/api/v1/db/meta/tables/${tableId}/maps`,
method: 'POST',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),
/**
* No description
*
* @tags DB view
* @name MapUpdate
* @request PATCH:/api/v1/db/meta/maps/{mapId}
* @response `200` `void` OK
*/
mapUpdate: (mapId: string, data: MapType, params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/v1/db/meta/maps/${mapId}`,
method: 'PATCH',
body: data,
type: ContentType.Json,
...params,
}),
/**
* No description
*
* @tags DB view
* @name MapRead
* @request GET:/api/v1/db/meta/maps/{mapId}
* @response `200` `MapType` OK
*/
mapRead: (mapId: string, params: RequestParams = {}) =>
this.request<MapType, any>({
path: `/api/v1/db/meta/maps/${mapId}`,
method: 'GET',
format: 'json',
...params,
}),
};
dbViewShare = {
/**

1
packages/nocodb-sdk/src/lib/UITypes.ts

@ -16,6 +16,7 @@ enum UITypes {
Year = 'Year',
Time = 'Time',
PhoneNumber = 'PhoneNumber',
GeoData = 'GeoData',
Email = 'Email',
URL = 'URL',
Number = 'Number',

1
packages/nocodb-sdk/src/lib/globals.ts

@ -3,6 +3,7 @@ export enum ViewTypes {
GALLERY = 2,
GRID = 3,
KANBAN = 4,
MAP = 5,
}
export enum RelationTypes {

6
packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts

@ -978,6 +978,9 @@ export class MssqlUi {
case 'Collaborator':
colProp.dt = 'varchar';
break;
case 'GeoData':
colProp.dt = 'varchar';
break;
case 'Date':
colProp.dt = 'date';
@ -1133,6 +1136,9 @@ export class MssqlUi {
case 'Decimal':
return ['decimal', 'float'];
case 'GeoData':
return ['decimal', 'float'];
case 'Currency':
return [
'int',

8
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/helpers/getAst.ts

@ -81,13 +81,13 @@ const getAst = async ({
...(await obj),
[col.title]:
allowedCols && (!includePkByDefault || !col.pk)
? allowedCols[col.id] &&
? (allowedCols[col.id] &&
(!isSystemColumn(col) || view.show_system_fields) &&
(!fields?.length || fields.includes(col.title)) &&
value
: fields?.length
value)
: (fields?.length
? fields.includes(col.title) && value
: value,
: value),
};
}, Promise.resolve({}));
};

2
packages/nocodb/src/lib/meta/api/index.ts

@ -54,6 +54,7 @@ import crypto from 'crypto';
import swaggerApis from './swagger/swaggerApis';
import importApis from './sync/importApis';
import syncSourceApis from './sync/syncSourceApis';
import mapViewApis from './mapViewApis';
const clients: { [id: string]: Socket } = {};
const jobs: { [id: string]: { last_message: any } } = {};
@ -105,6 +106,7 @@ export default function (router: Router, server) {
router.use(swaggerApis);
router.use(syncSourceApis);
router.use(kanbanViewApis);
router.use(mapViewApis);
userApis(router);

46
packages/nocodb/src/lib/meta/api/mapViewApis.ts

@ -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;

3
packages/nocodb/src/lib/meta/api/publicApis/publicDataApis.ts

@ -28,7 +28,8 @@ export async function dataList(req: Request, res: Response) {
if (
view.type !== ViewTypes.GRID &&
view.type !== ViewTypes.KANBAN &&
view.type !== ViewTypes.GALLERY
view.type !== ViewTypes.GALLERY &&
view.type !== ViewTypes.MAP
) {
NcError.notFound('Not found');
}

6
packages/nocodb/src/lib/meta/api/publicApis/publicDataExportApis.ts

@ -19,7 +19,8 @@ async function exportExcel(req: Request, res: Response) {
if (
view.type !== ViewTypes.GRID &&
view.type !== ViewTypes.KANBAN &&
view.type !== ViewTypes.GALLERY
view.type !== ViewTypes.GALLERY &&
view.type !== ViewTypes.MAP
)
NcError.notFound('Not found');
@ -67,7 +68,8 @@ async function exportCsv(req: Request, res: Response) {
if (
view.type !== ViewTypes.GRID &&
view.type !== ViewTypes.KANBAN &&
view.type !== ViewTypes.GALLERY
view.type !== ViewTypes.GALLERY &&
view.type !== ViewTypes.MAP
)
NcError.notFound('Not found');

1
packages/nocodb/src/lib/meta/api/viewApis.ts

@ -89,6 +89,7 @@ async function showAllColumns(req: Request<any, any>, res) {
}
async function hideAllColumns(req: Request<any, any>, res) {
res.json(
await View.hideAllColumns(
req.params.viewId,

4
packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts

@ -13,6 +13,7 @@ import * as nc_022_qr_code_column_type from './v2/nc_022_qr_code_column_type';
import * as nc_023_multiple_source from './v2/nc_023_multiple_source';
import * as nc_024_barcode_column_type from './v2/nc_024_barcode_column_type';
import * as nc_025_add_row_height from './v2/nc_025_add_row_height';
import * as nc_026_map_view from './v2/nc_026_map_view';
// Create a custom migration source class
export default class XcMigrationSourcev2 {
@ -37,6 +38,7 @@ export default class XcMigrationSourcev2 {
'nc_023_multiple_source',
'nc_024_barcode_column_type',
'nc_025_add_row_height',
'nc_026_map_view',
]);
}
@ -76,6 +78,8 @@ export default class XcMigrationSourcev2 {
return nc_024_barcode_column_type;
case 'nc_025_add_row_height':
return nc_025_add_row_height;
case 'nc_026_map_view':
return nc_026_map_view;
}
}
}

55
packages/nocodb/src/lib/migrations/v2/nc_026_map_view.ts

@ -0,0 +1,55 @@
import { Knex } from 'knex';
import { MetaTable } from '../../utils/globals';
const up = async (knex: 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: Knex) => {
await knex.schema.dropTable(MetaTable.MAP_VIEW);
await knex.schema.dropTable(MetaTable.MAP_VIEW_COLUMNS);
};
export { up, down };

104
packages/nocodb/src/lib/models/MapView.ts

@ -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,
});
}
}

106
packages/nocodb/src/lib/models/MapViewColumn.ts

@ -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));
}
}

108
packages/nocodb/src/lib/models/View.ts

@ -27,6 +27,8 @@ import KanbanViewColumn from './KanbanViewColumn';
import Column from './Column';
import NocoCache from '../cache/NocoCache';
import { extractProps } from '../meta/helpers/extractProps';
import MapView from './MapView';
import MapViewColumn from './MapViewColumn';
const { v4: uuidv4 } = require('uuid');
export default class View implements ViewType {
@ -42,9 +44,13 @@ export default class View implements ViewType {
fk_model_id: string;
model?: Model;
view?: FormView | GridView | KanbanView | GalleryView;
view?: FormView | GridView | KanbanView | GalleryView | MapView;
columns?: Array<
FormViewColumn | GridViewColumn | GalleryViewColumn | KanbanViewColumn
| FormViewColumn
| GridViewColumn
| GalleryViewColumn
| KanbanViewColumn
| MapViewColumn
>;
sorts: Sort[];
@ -83,6 +89,9 @@ export default class View implements ViewType {
case ViewTypes.GALLERY:
this.view = await GalleryView.get(this.id);
break;
case ViewTypes.MAP:
this.view = await MapView.get(this.id);
break;
case ViewTypes.FORM:
this.view = await FormView.get(this.id);
break;
@ -103,6 +112,9 @@ export default class View implements ViewType {
case ViewTypes.GALLERY:
this.view = await GalleryView.get(this.id, ncMeta);
break;
case ViewTypes.MAP:
this.view = await MapView.get(this.id, ncMeta);
break;
case ViewTypes.FORM:
this.view = await FormView.get(this.id, ncMeta);
break;
@ -236,7 +248,7 @@ export default class View implements ViewType {
static async insert(
view: Partial<View> &
Partial<FormView | GridView | GalleryView | KanbanView> & {
Partial<FormView | GridView | GalleryView | KanbanView | MapView> & {
copy_from_id?: string;
fk_grp_col_id?: string;
created_at?;
@ -310,6 +322,15 @@ export default class View implements ViewType {
ncMeta
);
break;
case ViewTypes.MAP:
await MapView.insert(
{
...(view as MapView),
fk_view_id: view_id,
},
ncMeta
);
break;
case ViewTypes.GALLERY:
await GalleryView.insert(
{
@ -433,6 +454,11 @@ export default class View implements ViewType {
// other columns will be hidden
show = false;
}
} else if (view.type === ViewTypes.MAP && !copyFromView) {
const mapView = await MapView.get(view_id, ncMeta);
if (vCol.id === mapView?.fk_geo_data_col_id) {
show = true;
}
}
// if columns is list of virtual columns then get the parent column
@ -490,6 +516,16 @@ export default class View implements ViewType {
case ViewTypes.GALLERY:
await GalleryViewColumn.insert(modifiedInsertObj, ncMeta);
break;
case ViewTypes.MAP:
await MapViewColumn.insert(
{
...insertObj,
fk_view_id: view.id,
},
ncMeta
);
break;
case ViewTypes.KANBAN:
await KanbanViewColumn.insert(modifiedInsertObj, ncMeta);
break;
@ -533,6 +569,17 @@ export default class View implements ViewType {
);
}
break;
case ViewTypes.MAP:
{
col = await MapViewColumn.insert(
{
...param,
fk_view_id: view.id,
},
ncMeta
);
}
break;
case ViewTypes.FORM:
{
col = await FormViewColumn.insert(
@ -573,7 +620,11 @@ export default class View implements ViewType {
ncMeta = Noco.ncMeta
): Promise<
Array<
GridViewColumn | FormViewColumn | GalleryViewColumn | KanbanViewColumn
| GridViewColumn
| FormViewColumn
| GalleryViewColumn
| KanbanViewColumn
| MapViewColumn
>
> {
let columns: Array<GridViewColumn | any> = [];
@ -587,6 +638,9 @@ export default class View implements ViewType {
case ViewTypes.GALLERY:
columns = await GalleryViewColumn.list(viewId, ncMeta);
break;
case ViewTypes.MAP:
columns = await MapViewColumn.list(viewId, ncMeta);
break;
case ViewTypes.FORM:
columns = await FormViewColumn.list(viewId, ncMeta);
break;
@ -620,6 +674,10 @@ export default class View implements ViewType {
table = MetaTable.GRID_VIEW_COLUMNS;
cacheScope = CacheScope.GRID_VIEW_COLUMN;
break;
case ViewTypes.MAP:
table = MetaTable.MAP_VIEW_COLUMNS;
cacheScope = CacheScope.MAP_VIEW_COLUMN;
break;
case ViewTypes.GALLERY:
table = MetaTable.GALLERY_VIEW_COLUMNS;
cacheScope = CacheScope.GALLERY_VIEW_COLUMN;
@ -687,7 +745,12 @@ export default class View implements ViewType {
},
ncMeta = Noco.ncMeta
): Promise<
GridViewColumn | FormViewColumn | GalleryViewColumn | KanbanViewColumn | any
| GridViewColumn
| FormViewColumn
| GalleryViewColumn
| KanbanViewColumn
| MapViewColumn
| any
> {
const view = await this.get(viewId);
const table = this.extractViewColumnsTableName(view);
@ -734,6 +797,14 @@ export default class View implements ViewType {
show: colData.show,
});
break;
case ViewTypes.MAP:
return await MapViewColumn.insert({
fk_view_id: viewId,
fk_column_id: fkColId,
order: colData.order,
show: colData.show,
});
break;
case ViewTypes.FORM:
return await FormViewColumn.insert({
fk_view_id: viewId,
@ -974,6 +1045,9 @@ export default class View implements ViewType {
case ViewTypes.FORM:
table = MetaTable.FORM_VIEW_COLUMNS;
break;
case ViewTypes.MAP:
table = MetaTable.MAP_VIEW_COLUMNS;
break;
}
return table;
}
@ -993,6 +1067,9 @@ export default class View implements ViewType {
case ViewTypes.FORM:
table = MetaTable.FORM_VIEW;
break;
case ViewTypes.MAP:
table = MetaTable.MAP_VIEW;
break;
}
return table;
}
@ -1006,6 +1083,9 @@ export default class View implements ViewType {
case ViewTypes.GALLERY:
scope = CacheScope.GALLERY_VIEW_COLUMN;
break;
case ViewTypes.MAP:
scope = CacheScope.MAP_VIEW_COLUMN;
break;
case ViewTypes.KANBAN:
scope = CacheScope.KANBAN_VIEW_COLUMN;
break;
@ -1025,6 +1105,9 @@ export default class View implements ViewType {
case ViewTypes.GALLERY:
scope = CacheScope.GALLERY_VIEW;
break;
case ViewTypes.MAP:
scope = CacheScope.MAP_VIEW;
break;
case ViewTypes.KANBAN:
scope = CacheScope.KANBAN_VIEW;
break;
@ -1140,9 +1223,20 @@ export default class View implements ViewType {
// get existing cache
const dataList = await NocoCache.getList(scope, [viewId]);
const colsEssentialForView =
view.type === ViewTypes.MAP
? [(await MapView.get(viewId)).fk_geo_data_col_id]
: [];
const mergedIgnoreColdIds = [...ignoreColdIds, ...colsEssentialForView];
if (dataList?.length) {
for (const o of dataList) {
if (!ignoreColdIds?.length || !ignoreColdIds.includes(o.fk_column_id)) {
if (
!mergedIgnoreColdIds?.length ||
!mergedIgnoreColdIds.includes(o.fk_column_id)
) {
// set data
o.show = false;
// set cache
@ -1159,7 +1253,7 @@ export default class View implements ViewType {
{
fk_view_id: viewId,
},
ignoreColdIds?.length
mergedIgnoreColdIds?.length
? {
_not: {
fk_column_id: {

7
packages/nocodb/src/lib/utils/globals.ts

@ -39,7 +39,10 @@ export enum MetaTable {
API_TOKENS = 'nc_api_tokens',
SYNC_SOURCE = 'nc_sync_source_v2',
SYNC_LOGS = 'nc_sync_logs_v2',
MAP_VIEW = 'nc_map_view_v2',
MAP_VIEW_COLUMNS = 'nc_map_view_columns_v2',
STORE = 'nc_store',
}
export const orderedMetaTables = [
@ -51,6 +54,8 @@ export const orderedMetaTables = [
MetaTable.ORGS,
MetaTable.PROJECT_USERS,
MetaTable.USERS,
MetaTable.MAP_VIEW,
MetaTable.MAP_VIEW_COLUMNS,
MetaTable.KANBAN_VIEW_COLUMNS,
MetaTable.KANBAN_VIEW,
MetaTable.GRID_VIEW_COLUMNS,
@ -127,6 +132,8 @@ export enum CacheScope {
GRID_VIEW = 'gridView',
GRID_VIEW_COLUMN = 'gridViewColumn',
KANBAN_VIEW = 'kanbanView',
MAP_VIEW = 'mapView',
MAP_VIEW_COLUMN = 'mapViewColumn',
KANBAN_VIEW_COLUMN = 'kanbanViewColumn',
USER = 'user',
ORGS = 'orgs',

178
scripts/sdk/swagger.json

@ -3320,6 +3320,99 @@
]
}
},
"/api/v1/db/meta/tables/{tableId}/maps": {
"parameters": [
{
"schema": {
"type": "string"
},
"name": "tableId",
"in": "path",
"required": true
}
],
"post": {
"summary": "",
"operationId": "db-view-map-create",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
}
}
},
"tags": [
"DB view"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Map"
}
}
}
}
}
},
"/api/v1/db/meta/maps/{mapId}": {
"parameters": [
{
"schema": {
"type": "string"
},
"name": "mapId",
"in": "path",
"required": true
}
],
"patch": {
"summary": "",
"operationId": "db-view-map-update",
"responses": {
"200": {
"description": "OK"
}
},
"tags": [
"DB view"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Map"
}
}
}
}
},
"get": {
"summary": "",
"operationId": "db-view-map-read",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Map"
}
}
}
}
},
"tags": [
"DB view"
]
}
},
"/api/v1/db/meta/projects/{projectId}/meta-diff": {
"parameters": [
{
@ -7703,6 +7796,9 @@
},
{
"$ref": "#/components/schemas/Kanban"
},
{
"$ref": "#/components/schemas/Map"
}
]
}
@ -8755,6 +8851,87 @@
}
}
},
"GeoLocation": {
"title": "GeoLocation",
"type": "object",
"description": "",
"properties": {
"latitude": {
"type": "number",
"format": "double"
},
"longitude": {
"type": "number",
"format": "double"
}
}
},
"Map": {
"title": "Map",
"type": "object",
"description": "",
"properties": {
"id": {
"type": "string"
},
"title": {
"type": "string"
},
"alias": {
"type": "string"
},
"initial_geo_position": {
"$ref": "#/components/schemas/GeoLocation"
},
"fk_model_id": {
"type": "string"
},
"fk_view_id": {
"type": "string",
"minLength": 1
},
"fk_geo_data_col_id": {
"type": [
"string",
"null"
]
},
"columns": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MapColumn"
}
},
"meta": {
"type": [
"string",
"object"
]
}
}
},
"MapColumn": {
"title": "MapColumn",
"type": "object",
"description": "",
"properties": {
"id": {
"type": "string"
},
"label": {
"type": "string"
},
"help": {
"type": "string"
},
"fk_col_id": {
"type": "string"
},
"fk_gallery_id": {
"type": "string"
}
}
},
"Form": {
"title": "Form",
"type": "object",
@ -9407,6 +9584,7 @@
"Collaborator",
"Date",
"Year",
"GeoData",
"Time",
"PhoneNumber",
"Email",

8
tests/playwright/pages/Dashboard/ExpandedForm/index.ts

@ -67,6 +67,14 @@ export class ExpandedFormPage extends BasePage {
case 'text':
await field.locator('input').fill(value);
break;
case 'geodata': {
const [lat, long] = value.split(',');
await this.rootPage.locator(`[data-testid="nc-geo-data-set-location-button"]`).click();
await this.rootPage.locator(`[data-testid="nc-geo-data-latitude"]`).fill(lat);
await this.rootPage.locator(`[data-testid="nc-geo-data-longitude"]`).fill(long);
await this.rootPage.locator(`[data-testid="nc-geo-data-save"]`).click();
break;
}
case 'belongsTo':
await field.locator('.nc-action-icon').click();
await this.dashboard.linkRecord.select(value);

51
tests/playwright/pages/Dashboard/Map/index.ts

@ -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);
}
}

13
tests/playwright/pages/Dashboard/ViewSidebar/index.ts

@ -9,6 +9,7 @@ export class ViewSidebarPage extends BasePage {
readonly createGridButton: Locator;
readonly createFormButton: Locator;
readonly createKanbanButton: Locator;
readonly createMapButton: Locator;
constructor(dashboard: DashboardPage) {
super(dashboard.rootPage);
@ -17,6 +18,7 @@ export class ViewSidebarPage extends BasePage {
this.createGridButton = this.get().locator('.nc-create-grid-view:visible');
this.createFormButton = this.get().locator('.nc-create-form-view:visible');
this.createKanbanButton = this.get().locator('.nc-create-kanban-view:visible');
this.createMapButton = this.get().locator('.nc-create-map-view:visible');
}
get() {
@ -35,6 +37,13 @@ export class ViewSidebarPage extends BasePage {
}
}
async activateGeoDataEasterEgg() {
await this.dashboard.rootPage.evaluate(_ => {
window.localStorage.setItem('geodataToggleState', 'true');
});
await this.rootPage.goto(this.rootPage.url());
}
private async createView({ title, locator }: { title: string; locator: Locator }) {
await locator.click();
await this.rootPage.locator('input[id="form_item_title"]:visible').fill(title);
@ -71,6 +80,10 @@ export class ViewSidebarPage extends BasePage {
await this.createView({ title, locator: this.createKanbanButton });
}
async createMapView({ title }: { title: string }) {
await this.createView({ title, locator: this.createMapButton });
}
// Todo: Make selection better
async verifyView({ title, index }: { title: string; index: number }) {
await expect(

36
tests/playwright/pages/Dashboard/common/Cell/GeoDataCell.ts

@ -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');
}
}

30
tests/playwright/pages/Dashboard/common/Cell/index.ts

@ -8,6 +8,7 @@ import { CheckboxCellPageObject } from './CheckboxCell';
import { RatingCellPageObject } from './RatingCell';
import { DateCellPageObject } from './DateCell';
import { DateTimeCellPageObject } from './DateTimeCell';
import { GeoDataCellPageObject } from './GeoDataCell';
export interface CellProps {
index?: number;
@ -20,6 +21,7 @@ export class CellPageObject extends BasePage {
readonly attachment: AttachmentCellPageObject;
readonly checkbox: CheckboxCellPageObject;
readonly rating: RatingCellPageObject;
readonly geoData: GeoDataCellPageObject;
readonly date: DateCellPageObject;
readonly dateTime: DateTimeCellPageObject;
@ -30,6 +32,7 @@ export class CellPageObject extends BasePage {
this.attachment = new AttachmentCellPageObject(this);
this.checkbox = new CheckboxCellPageObject(this);
this.rating = new RatingCellPageObject(this);
this.geoData = new GeoDataCellPageObject(this);
this.date = new DateCellPageObject(this);
this.dateTime = new DateTimeCellPageObject(this);
}
@ -138,6 +141,33 @@ export class CellPageObject extends BasePage {
}
}
async verifyGeoDataCell({
index,
columnHeader,
lat,
long,
}: {
index: number;
columnHeader: string;
lat: string;
long: string;
}) {
const _verify = async expectedValue => {
await expect
.poll(async () => {
const cell = await this.get({
index,
columnHeader,
}).locator(`[data-testid="nc-geo-data-lat-long-set"]`);
return await cell.textContent(); //.getAttribute('title');
})
.toEqual(expectedValue);
};
const value = `${lat}; ${long}`;
await _verify(value);
}
async verifyDateCell({ index, columnHeader, value }: { index: number; columnHeader: string; value: string }) {
const _verify = async expectedValue => {
await expect

5
tests/playwright/pages/Dashboard/common/Toolbar/index.ts

@ -15,9 +15,10 @@ import { ToolbarStackbyPage } from './StackBy';
import { ToolbarAddEditStackPage } from './AddEditKanbanStack';
import { ToolbarSearchDataPage } from './SearchData';
import { RowHeight } from './RowHeight';
import { MapPage } from '../../Map';
export class ToolbarPage extends BasePage {
readonly parent: GridPage | GalleryPage | FormPage | KanbanPage;
readonly parent: GridPage | GalleryPage | FormPage | KanbanPage | MapPage;
readonly fields: ToolbarFieldsPage;
readonly sort: ToolbarSortPage;
readonly filter: ToolbarFilterPage;
@ -29,7 +30,7 @@ export class ToolbarPage extends BasePage {
readonly searchData: ToolbarSearchDataPage;
readonly rowHeight: RowHeight;
constructor(parent: GridPage | GalleryPage | FormPage | KanbanPage) {
constructor(parent: GridPage | GalleryPage | FormPage | KanbanPage | MapPage) {
super(parent.rootPage);
this.parent = parent;
this.fields = new ToolbarFieldsPage(this);

3
tests/playwright/pages/Dashboard/index.ts

@ -10,6 +10,7 @@ import { SettingsPage } from './Settings';
import { ViewSidebarPage } from './ViewSidebar';
import { GalleryPage } from './Gallery';
import { KanbanPage } from './Kanban';
import { MapPage } from './Map';
import { ImportAirtablePage } from './Import/Airtable';
import { ImportTemplatePage } from './Import/ImportTemplate';
import { WebhookFormPage } from './WebhookForm';
@ -24,6 +25,7 @@ export class DashboardPage extends BasePage {
readonly gallery: GalleryPage;
readonly form: FormPage;
readonly kanban: KanbanPage;
readonly map: MapPage;
readonly expandedForm: ExpandedFormPage;
readonly webhookForm: WebhookFormPage;
readonly childList: ChildList;
@ -43,6 +45,7 @@ export class DashboardPage extends BasePage {
this.gallery = new GalleryPage(this);
this.form = new FormPage(this);
this.kanban = new KanbanPage(this);
this.map = new MapPage(this);
this.expandedForm = new ExpandedFormPage(this);
this.webhookForm = new WebhookFormPage(this);
this.childList = new ChildList(this);

73
tests/playwright/tests/columnGeoData.spec.ts

@ -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' });
});
});

345
tests/playwright/tests/keyboardShortcuts.spec.ts

@ -2,6 +2,9 @@ import { expect, test } from '@playwright/test';
import { DashboardPage } from '../pages/Dashboard';
import { GridPage } from '../pages/Dashboard/Grid';
import setup from '../setup';
import { Api, UITypes } from 'nocodb-sdk';
let api: Api<any>;
test.describe('Verify shortcuts', () => {
let dashboard: DashboardPage, grid: GridPage;
@ -103,220 +106,147 @@ test.describe('Verify shortcuts', () => {
});
test('Clipboard support for cells', async () => {
// close 'Team & Auth' tab
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.createTable({ title: 'Sheet1' });
api = new Api({
baseURL: `http://localhost:8080/`,
headers: {
'xc-auth': context.token,
},
});
await dashboard.grid.column.create({
const columns = [
{
column_name: 'Id',
title: 'Id',
uidt: UITypes.ID,
},
{
column_name: 'SingleLineText',
title: 'SingleLineText',
type: 'SingleLineText',
});
await dashboard.grid.column.create({
uidt: UITypes.SingleLineText,
},
{
column_name: 'LongText',
title: 'LongText',
type: 'LongText',
});
await dashboard.grid.column.create({
uidt: UITypes.LongText,
},
{
column_name: 'Number',
title: 'Number',
type: 'Number',
});
await dashboard.grid.column.create({
uidt: UITypes.Number,
},
{
column_name: 'PhoneNumber',
title: 'PhoneNumber',
type: 'PhoneNumber',
});
await dashboard.grid.column.create({
uidt: UITypes.PhoneNumber,
},
{
column_name: 'Email',
title: 'Email',
type: 'Email',
});
await dashboard.grid.column.create({
uidt: UITypes.Email,
},
{
column_name: 'URL',
title: 'URL',
type: 'URL',
});
await dashboard.grid.column.create({
uidt: UITypes.URL,
},
{
column_name: 'Decimal',
title: 'Decimal',
type: 'Decimal',
});
await dashboard.grid.column.create({
uidt: UITypes.Decimal,
},
{
column_name: 'Percent',
title: 'Percent',
type: 'Percent',
});
await dashboard.grid.column.create({
uidt: UITypes.Percent,
},
{
column_name: 'Currency',
title: 'Currency',
type: 'Currency',
});
await dashboard.grid.column.create({
uidt: UITypes.Currency,
},
{
column_name: 'Duration',
title: 'Duration',
type: 'Duration',
});
await dashboard.grid.column.create({
title: 'Rating',
type: 'Rating',
});
await dashboard.grid.column.create({
uidt: UITypes.Duration,
},
{
column_name: 'SingleSelect',
title: 'SingleSelect',
type: 'SingleSelect',
});
await dashboard.grid.column.selectOption.addOptions({
columnTitle: 'SingleSelect',
options: ['Option 1', 'Option 2'],
});
await dashboard.grid.column.create({
uidt: UITypes.SingleSelect,
dtxp: "'Option1','Option2'",
},
{
column_name: 'MultiSelect',
title: 'MultiSelect',
type: 'MultiSelect',
});
await dashboard.grid.column.selectOption.addOptions({
columnTitle: 'MultiSelect',
options: ['Option 1', 'Option 2'],
});
await dashboard.grid.column.create({
uidt: UITypes.MultiSelect,
dtxp: "'Option1','Option2'",
},
{
column_name: 'Rating',
title: 'Rating',
uidt: UITypes.Rating,
},
{
column_name: 'Checkbox',
title: 'Checkbox',
type: 'Checkbox',
});
await dashboard.grid.column.create({
uidt: UITypes.Checkbox,
},
{
column_name: 'Date',
title: 'Date',
type: 'Date',
});
await dashboard.grid.column.create({
uidt: UITypes.Date,
},
{
column_name: 'Attachment',
title: 'Attachment',
type: 'Attachment',
});
// ########################################
await dashboard.grid.addNewRow({
index: 0,
});
await dashboard.grid.cell.click({
index: 0,
columnHeader: 'SingleLineText',
});
await dashboard.grid.cell.fillText({
index: 0,
columnHeader: 'SingleLineText',
text: 'SingleLineText',
});
await dashboard.grid.cell.click({
index: 0,
columnHeader: 'LongText',
});
await dashboard.grid.cell.fillText({
index: 0,
columnHeader: 'LongText',
text: 'LongText',
});
await grid.cell.selectOption.select({ index: 0, columnHeader: 'SingleSelect', option: 'Option 1' });
await grid.cell.selectOption.select({
index: 0,
columnHeader: 'MultiSelect',
option: 'Option 2',
multiSelect: true,
});
await grid.cell.selectOption.select({
index: 0,
columnHeader: 'MultiSelect',
option: 'Option 1',
multiSelect: true,
});
await dashboard.grid.cell.click({
index: 0,
columnHeader: 'Number',
});
await dashboard.grid.cell.fillText({
index: 0,
columnHeader: 'Number',
text: '123',
});
await dashboard.grid.cell.click({
index: 0,
columnHeader: 'PhoneNumber',
});
await dashboard.grid.cell.fillText({
index: 0,
columnHeader: 'PhoneNumber',
text: '987654321',
});
await dashboard.grid.cell.click({
index: 0,
columnHeader: 'Email',
});
await dashboard.grid.cell.fillText({
index: 0,
columnHeader: 'Email',
text: 'test@example.com',
});
await dashboard.grid.cell.click({
index: 0,
columnHeader: 'URL',
});
await dashboard.grid.cell.fillText({
index: 0,
columnHeader: 'URL',
text: 'nocodb.com',
});
await dashboard.grid.cell.click({
index: 0,
columnHeader: 'Decimal',
});
await dashboard.grid.cell.fillText({
index: 0,
columnHeader: 'Decimal',
text: '1.1',
});
await dashboard.grid.cell.click({
index: 0,
columnHeader: 'Percent',
});
await dashboard.grid.cell.fillText({
index: 0,
columnHeader: 'Percent',
text: '80',
});
uidt: UITypes.Attachment,
},
];
await dashboard.grid.cell.click({
index: 0,
columnHeader: 'Currency',
});
await dashboard.grid.cell.fillText({
index: 0,
columnHeader: 'Currency',
text: '20',
});
const today = new Date().toISOString().slice(0, 10);
const record = {
Id: 1,
SingleLineText: 'SingleLineText',
LongText: 'LongText',
SingleSelect: 'Option1',
MultiSelect: 'Option1,Option2',
Number: 123,
PhoneNumber: '987654321',
Email: 'test@example.com',
URL: 'nocodb.com',
Rating: 4,
Decimal: 1.12,
Percent: 80,
Currency: 20,
Duration: 480,
Checkbox: 1,
Date: today,
};
try {
const project = await api.project.read(context.project.id);
const table = await api.base.tableCreate(context.project.id, project.bases?.[0].id, {
table_name: 'Sheet1',
title: 'Sheet1',
columns: columns,
});
await api.dbTableRow.bulkCreate('noco', context.project.id, table.id, [record]);
const records = await api.dbTableRow.list('noco', context.project.id, table.id, { limit: 1 });
console.log(records);
} catch (e) {
console.error(e);
}
// reload page
await dashboard.rootPage.reload();
await dashboard.grid.cell.click({
index: 0,
columnHeader: 'Duration',
});
await dashboard.grid.cell.fillText({
index: 0,
columnHeader: 'Duration',
text: '0008',
});
// close 'Team & Auth' tab
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.grid.cell.rating.select({
index: 0,
columnHeader: 'Rating',
rating: 3,
});
await dashboard.grid.cell.click({
index: 0,
columnHeader: 'Checkbox',
});
await dashboard.treeView.openTable({ title: 'Sheet1' });
const today = new Date().toISOString().slice(0, 10);
await dashboard.grid.cell.date.open({
index: 0,
columnHeader: 'Date',
});
await dashboard.grid.cell.date.selectDate({
date: today,
});
await dashboard.grid.cell.date.close();
// ########################################
await dashboard.grid.cell.attachment.addFile({
index: 0,
@ -359,9 +289,9 @@ test.describe('Verify shortcuts', () => {
await dashboard.grid.cell.copyToClipboard({
index: 0,
columnHeader: 'Title',
columnHeader: 'SingleLineText',
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('Row 0');
expect(await dashboard.grid.cell.getClipboardText()).toBe('SingleLineText');
await dashboard.grid.cell.copyToClipboard({
index: 0,
@ -375,23 +305,29 @@ test.describe('Verify shortcuts', () => {
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('987654321');
await dashboard.grid.cell.copyToClipboard({
await dashboard.grid.cell.copyToClipboard(
{
index: 0,
columnHeader: 'Email',
});
},
{ position: { x: 1, y: 1 } }
);
expect(await dashboard.grid.cell.getClipboardText()).toBe('test@example.com');
await dashboard.grid.cell.copyToClipboard({
await dashboard.grid.cell.copyToClipboard(
{
index: 0,
columnHeader: 'URL',
});
},
{ position: { x: 1, y: 1 } }
);
expect(await dashboard.grid.cell.getClipboardText()).toBe('nocodb.com');
await dashboard.grid.cell.copyToClipboard({
index: 0,
columnHeader: 'Decimal',
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('1.1');
expect(await dashboard.grid.cell.getClipboardText()).toBe('1.12');
await dashboard.grid.cell.copyToClipboard({
index: 0,
@ -403,13 +339,14 @@ test.describe('Verify shortcuts', () => {
index: 0,
columnHeader: 'Currency',
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('20');
// convert from string to integer
expect(parseInt(await dashboard.grid.cell.getClipboardText())).toBe(20);
await dashboard.grid.cell.copyToClipboard({
index: 0,
columnHeader: 'Duration',
});
expect(await dashboard.grid.cell.getClipboardText()).toBe('480');
expect(parseInt(await dashboard.grid.cell.getClipboardText())).toBe(480);
await dashboard.grid.cell.copyToClipboard(
{
@ -427,7 +364,7 @@ test.describe('Verify shortcuts', () => {
},
{ position: { x: 1, y: 1 } }
);
await new Promise(resolve => setTimeout(resolve, 5000));
// await new Promise(resolve => setTimeout(resolve, 5000));
expect(await dashboard.grid.cell.getClipboardText()).toBe('true');
await dashboard.grid.cell.click({

92
tests/playwright/tests/viewMap.spec.ts

@ -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…
Cancel
Save