Browse Source

resolve merging conflicts

pull/4749/head
flisowna 2 years ago
parent
commit
1ef7b3719c
  1. 34
      .github/workflows/cleanup-caches-by-branch.yml
  2. 2
      package.json
  3. 2
      packages/nc-gui/assets/style.scss
  4. 2
      packages/nc-gui/components.d.ts
  5. 28
      packages/nc-gui/components/cell/attachment/Carousel.vue
  6. 25
      packages/nc-gui/components/cell/attachment/Image.vue
  7. 16
      packages/nc-gui/components/cell/attachment/Modal.vue
  8. 33
      packages/nc-gui/components/cell/attachment/index.vue
  9. 33
      packages/nc-gui/components/cell/attachment/utils.ts
  10. 8
      packages/nc-gui/components/dashboard/TreeView.vue
  11. 20
      packages/nc-gui/components/smartsheet/Gallery.vue
  12. 51
      packages/nc-gui/components/smartsheet/Grid.vue
  13. 20
      packages/nc-gui/components/smartsheet/Kanban.vue
  14. 11
      packages/nc-gui/components/smartsheet/header/Menu.vue
  15. 33
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  16. 5
      packages/nc-gui/composables/useApi/interceptors.ts
  17. 40
      packages/nc-gui/composables/useAttachment.ts
  18. 6
      packages/nc-gui/composables/useCellUrlConfig.ts
  19. 6
      packages/nc-gui/composables/useDashboard.ts
  20. 29
      packages/nc-gui/composables/useGlobal/actions.ts
  21. 2
      packages/nc-gui/composables/useGlobal/index.ts
  22. 62
      packages/nc-gui/composables/useKanbanViewStore.ts
  23. 6
      packages/nc-gui/composables/useLTARStore.ts
  24. 33
      packages/nc-gui/composables/useProject.ts
  25. 18
      packages/nc-gui/composables/useTabs.ts
  26. 10
      packages/nc-gui/composables/useViewColumns.ts
  27. 63
      packages/nc-gui/composables/useViewData.ts
  28. 6
      packages/nc-gui/middleware/auth.global.ts
  29. 4
      packages/nc-gui/plugins/tele.ts
  30. 1
      packages/nc-gui/utils/index.ts
  31. 714
      packages/nc-gui/utils/mimeTypeUtils.ts
  32. 4
      packages/noco-docs/content/en/engineering/unit-testing.md
  33. 2
      packages/nocodb/src/lib/Noco.ts
  34. 49
      packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts
  35. 9
      packages/nocodb/src/lib/meta/api/attachmentApis.ts
  36. 263
      packages/nocodb/src/lib/meta/api/baseApis.ts
  37. 57
      packages/nocodb/src/lib/meta/api/columnApis.ts
  38. 3
      packages/nocodb/src/lib/meta/api/helpers/index.ts
  39. 278
      packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts
  40. 5
      packages/nocodb/src/lib/meta/api/metaDiffApis.ts
  41. 263
      packages/nocodb/src/lib/meta/api/projectApis.ts
  42. 12
      packages/nocodb/src/lib/meta/api/userApi/userApis.ts
  43. 2
      packages/nocodb/src/lib/models/GridViewColumn.ts
  44. 17
      packages/nocodb/src/lib/models/Model.ts
  45. 159
      packages/nocodb/src/lib/models/View.ts
  46. 2
      packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts
  47. 90
      packages/nocodb/src/lib/version-upgrader/ncStickyColumnUpgrader.ts
  48. 18
      tests/playwright/pages/Dashboard/Grid/Column/index.ts
  49. 22
      tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts
  50. 4
      tests/playwright/pages/Dashboard/common/Toolbar/Fields.ts
  51. 2
      tests/playwright/quickTests/commonTest.ts
  52. 2
      tests/playwright/tests/cellSelection.spec.ts
  53. 1
      tests/playwright/tests/columnAttachments.spec.ts
  54. 2
      tests/playwright/tests/columnMenuOperations.spec.ts
  55. 6
      tests/playwright/tests/metaSync.spec.ts

34
.github/workflows/cleanup-caches-by-branch.yml

@ -0,0 +1,34 @@
name: cleanup caches by branch
on:
pull_request:
types:
- closed
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Cleanup
run: |
gh extension install actions/gh-actions-cache
REPO=${{ github.repository }}
# get the branch
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
# fetch list of cache key
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
# set this to not fail the workflow while deleting cache keys
set +e
# delete cache key
for cacheKey in $cacheKeysForPR
do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
done
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
package.json

@ -43,6 +43,8 @@
"install:local:dep": "cd packages/nc-lib-gui;npm uninstall -S xc-lib;rm package-lock.json; npm i ../../../xc-lib-private; cd ../xc-instant;npm uninstall -S xc-lib xc-lib-gui;npm i ../../../xc-lib-private;npm i ../xc-lib-gui",
"install:npm:dep": "cd packages/nc-lib-gui;npm uninstall -S xc-lib; npm i -S xc-lib@latest; cd ../xc-instant;npm uninstall -S xc-lib xc-lib-gui;npm i -S xc-lib@latest xc-lib-gui@latest;npm i ../xc-lib-gui",
"prepare": "husky install",
"start:mysql": "docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml up -d",
"stop:mysql": "docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml down",
"start:pg": "docker-compose -f ./tests/playwright/scripts/docker-compose-pg.yml up -d",
"stop:pg": "docker-compose -f ./tests/playwright/scripts/docker-compose-pg.yml down"
},

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

@ -312,7 +312,7 @@ a {
.ant-btn-loading-icon{
& > span {
@apply block bg-red-500
@apply block;
}
}

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

@ -81,7 +81,6 @@ declare module '@vue/runtime-core' {
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
EvaEmailOutline: typeof import('~icons/eva/email-outline')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
Icon: typeof import('~icons/ic/on')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
@ -235,6 +234,7 @@ declare module '@vue/runtime-core' {
MdiTable: typeof import('~icons/mdi/table')['default']
MdiTableColumnPlusAfter: typeof import('~icons/mdi/table-column-plus-after')['default']
MdiTableColumnPlusBefore: typeof import('~icons/mdi/table-column-plus-before')['default']
MdiTableKey: typeof import('~icons/mdi/table-key')['default']
MdiTableLarge: typeof import('~icons/mdi/table-large')['default']
MdiText: typeof import('~icons/mdi/text')['default']
MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default']

28
packages/nc-gui/components/cell/attachment/Carousel.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { onKeyDown } from '@vueuse/core'
import { useAttachmentCell } from './utils'
import { computed, isImage, onClickOutside, ref } from '#imports'
import { computed, isImage, onClickOutside, ref, useAttachment } from '#imports'
const { selectedImage, visibleItems, downloadFile } = useAttachmentCell()!
@ -9,6 +9,8 @@ const carouselRef = ref()
const imageItems = computed(() => visibleItems.value.filter((item) => isImage(item.title, item.mimetype)))
const { getPossibleAttachmentSrc } = useAttachment()
/** navigate to previous image on button click */
onKeyDown(
(e) => ['Left', 'ArrowLeft', 'A'].includes(e.key),
@ -81,22 +83,16 @@ onClickOutside(carouselRef, () => {
</template>
<template #customPaging="props">
<a>
<LazyNuxtImg
quality="90"
placeholder
class="!block"
<div class="cursor-pointer h-full nc-attachment-img-wrapper">
<LazyCellAttachmentImage
class="!block m-auto h-full w-full"
:alt="imageItems[props.i].title || `#${props.i}`"
:src="imageItems[props.i].url || imageItems[props.i].data"
:srcs="getPossibleAttachmentSrc(imageItems[props.i])"
/>
</a>
</div>
</template>
<div v-for="item of imageItems" :key="item.url">
<div
:style="{ backgroundImage: `url('${item.url || item.data}')` }"
class="min-w-70vw min-h-70vh w-full h-full bg-contain bg-center bg-no-repeat"
/>
<div v-for="(item, idx) of imageItems" :key="idx">
<LazyCellAttachmentImage :srcs="getPossibleAttachmentSrc(item)" class="max-w-70vw max-h-70vh" />
</div>
</a-carousel>
</div>
@ -146,4 +142,8 @@ onClickOutside(carouselRef, () => {
.ant-carousel :deep(.custom-slick-arrow:hover) {
opacity: 0.5;
}
.nc-attachment-img-wrapper {
width: fit-content !important;
}
</style>

25
packages/nc-gui/components/cell/attachment/Image.vue

@ -0,0 +1,25 @@
<script setup lang="ts">
interface Props {
srcs: string[]
alt?: string
}
const props = defineProps<Props>()
const index = ref(0)
const onError = () => index.value++
</script>
<template>
<LazyNuxtImg
v-if="index < props.srcs.length"
class="m-auto"
:src="props.srcs[index]"
:alt="props?.alt || ''"
placeholder
quality="75"
@error="onError"
/>
<MdiFileImageBox v-else />
</template>

16
packages/nc-gui/components/cell/attachment/Modal.vue

@ -2,7 +2,7 @@
import { onKeyDown } from '@vueuse/core'
import { useAttachmentCell } from './utils'
import { useSortable } from './sort'
import { isImage, openLink, ref, useDropZone, useUIPermission, watch } from '#imports'
import { isImage, ref, useAttachment, useDropZone, useUIPermission, watch } from '#imports'
const { isUIAllowed } = useUIPermission()
@ -38,6 +38,8 @@ const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
const { isSharedForm } = useSmartsheetStoreOrThrow()
const { getPossibleAttachmentSrc, openAttachment } = useAttachment()
onKeyDown('Escape', () => {
modalVisible.value = false
isOverDropZone.value = false
@ -159,12 +161,12 @@ function onRemoveFileClick(title: any, i: number) {
<div
:class="[dragging ? 'cursor-move' : 'cursor-pointer']"
class="nc-attachment h-full w-full flex items-center justify-center"
class="nc-attachment h-full w-full flex items-center justify-center overflow-hidden"
>
<div
<LazyCellAttachmentImage
v-if="isImage(item.title, item.mimetype)"
:style="{ backgroundImage: `url('${item.url || item.data}')` }"
class="w-full h-full bg-contain bg-center bg-no-repeat"
:srcs="getPossibleAttachmentSrc(item)"
class="max-w-full max-h-full m-auto justify-center"
@click.stop="onClick(item)"
/>
@ -173,10 +175,10 @@ function onRemoveFileClick(title: any, i: number) {
v-else-if="item.icon"
height="150"
width="150"
@click.stop="openLink(item.url || item.data)"
@click.stop="openAttachment(item)"
/>
<IcOutlineInsertDriveFile v-else height="150" width="150" @click.stop="openLink(item.url || item.data)" />
<IcOutlineInsertDriveFile v-else height="150" width="150" @click.stop="openAttachment(item)" />
</div>
</a-card>

33
packages/nc-gui/components/cell/attachment/index.vue

@ -10,8 +10,8 @@ import {
inject,
isImage,
nextTick,
openLink,
ref,
useAttachment,
useDropZone,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
@ -46,6 +46,8 @@ const currentCellRef = ref<Element | undefined>(dropZoneInjection.value)
const { cellRefs, isSharedForm } = useSmartsheetStoreOrThrow()!
const { getPossibleAttachmentSrc, openAttachment } = useAttachment()
const {
isPublic,
isForm,
@ -60,7 +62,6 @@ const {
selectedImage,
isReadonly,
storedFiles,
getAttachmentUrl,
} = useProvideAttachmentCell(updateModelValue)
watch(
@ -101,16 +102,7 @@ watch(
async (nextModel) => {
if (nextModel) {
try {
let nextAttachments = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean)
// reconstruct the url
// See /packages/nocodb/src/lib/version-upgrader/ncAttachmentUpgrader.ts for the details
nextAttachments = await Promise.all(
nextAttachments.map(async (attachment: any) => ({
...attachment,
url: await getAttachmentUrl(attachment),
})),
)
const nextAttachments = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean)
if (isPublic.value && isForm.value) {
storedFiles.value = nextAttachments
@ -229,21 +221,12 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
<template #title>
<div class="text-center w-full">{{ item.title }}</div>
</template>
<template v-if="isImage(item.title, item.mimetype ?? item.type) && (item.url || item.data)">
<div v-if="isImage(item.title, item.mimetype ?? item.type)">
<div class="nc-attachment flex items-center justify-center" @click.stop="selectedImage = item">
<LazyNuxtImg
quality="75"
placeholder
fit="cover"
:alt="item.title || `#${i}`"
:src="item.url || item.data"
class="max-w-full max-h-full"
/>
<LazyCellAttachmentImage :alt="item.title || `#${i}`" :srcs="getPossibleAttachmentSrc(item)" />
</div>
</template>
<div v-else class="nc-attachment flex items-center justify-center" @click="openLink(item.url || item.data)">
</div>
<div v-else class="nc-attachment flex items-center justify-center" @click="openAttachment(item)">
<component :is="FileIcon(item.icon)" v-if="item.icon" />
<IcOutlineInsertDriveFile v-else />

33
packages/nc-gui/components/cell/attachment/utils.ts

@ -14,6 +14,7 @@ import {
message,
ref,
useApi,
useAttachment,
useFileDialog,
useI18n,
useInjectionState,
@ -60,6 +61,8 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
const { t } = useI18n()
const { getAttachmentSrc } = useAttachment()
const defaultAttachmentMeta = {
...(appInfo.value.ee && {
// Maximum Number of Attachments per cell
@ -226,31 +229,12 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
/** download a file */
async function downloadFile(item: AttachmentType) {
;(await import('file-saver')).saveAs(item.url || item.data, item.title)
}
/** construct the attachment url
* See /packages/nocodb/src/lib/version-upgrader/ncAttachmentUpgrader.ts for the details
* */
async function getAttachmentUrl(item: AttachmentType) {
const path = item?.path
// if path doesn't exist, use `item.url`
if (path) {
// try ${appInfo.value.ncSiteUrl}/${item.path} first
const url = `${appInfo.value.ncSiteUrl}/${item.path}`
try {
const res = await fetch(url)
if (res.ok) {
// use `url` if it is accessible
return Promise.resolve(url)
}
} catch {
// for some cases, `url` is not accessible as expected
// do nothing here
}
const src = await getAttachmentSrc(item)
if (src) {
;(await import('file-saver')).saveAs(src, item.title)
} else {
message.error('Failed to download file')
}
// if it fails, use the original url
return Promise.resolve(item.url)
}
const FileIcon = (icon: string) => {
@ -294,7 +278,6 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
storedFiles,
bulkDownloadFiles,
defaultAttachmentMeta,
getAttachmentUrl,
}
},
'useAttachmentCell',

8
packages/nc-gui/components/dashboard/TreeView.vue

@ -35,7 +35,7 @@ const { addTab, updateTab } = useTabs()
const { $api, $e } = useNuxtApp()
const { project, loadProject, bases, tables, loadTables, isSharedBase } = useProject()
const { bases, tables, loadTables, isSharedBase } = useProject()
const { activeTab } = useTabs()
@ -324,12 +324,6 @@ const setIcon = async (icon: string, table: TableType) => {
message.error(await extractSdkResponseErrorMsg(e))
}
}
onMounted(async () => {
if (!project.value?.id) {
await loadProject()
}
})
</script>
<template>

20
packages/nc-gui/components/smartsheet/Gallery.vue

@ -18,11 +18,13 @@ import {
createEventHook,
extractPkFromRow,
inject,
isImage,
isLTAR,
nextTick,
onMounted,
provide,
ref,
useAttachment,
useViewData,
} from '#imports'
import type { Row as RowType } from '~/lib'
@ -64,6 +66,8 @@ const route = useRoute()
const router = useRouter()
const { getPossibleAttachmentSrc } = useAttachment()
const fieldsWithoutCover = computed(() => fields.value.filter((f) => f.id !== galleryData.value?.fk_cover_image_col_id))
const coverImageColumn: any = $(
@ -199,14 +203,14 @@ watch(view, async (nextView) => {
<div style="z-index: 1"></div>
</template>
<LazyNuxtImg
v-for="(attachment, index) in attachments(record)"
:key="`carousel-${record.row.id}-${index}`"
quality="90"
placeholder
class="h-52 object-contain"
:src="attachment.url"
/>
<template v-for="(attachment, index) in attachments(record)">
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-52 object-contain"
:srcs="getPossibleAttachmentSrc(attachment)"
/>
</template>
</a-carousel>
<MdiFileImageBox v-else class="w-full h-48 my-4 text-cool-gray-200" />

51
packages/nc-gui/components/smartsheet/Grid.vue

@ -714,9 +714,9 @@ const closeAddColumnDropdown = () => {
@contextmenu="showContextMenu"
>
<thead ref="tableHead">
<tr class="nc-grid-header border-1 bg-gray-100 sticky top[-1px] !z-4">
<th data-testid="grid-id-column">
<div class="w-full h-full bg-gray-100 flex min-w-[70px] pl-5 pr-1 items-center" data-testid="nc-check-all">
<tr class="nc-grid-header">
<th class="w-[80px] min-w-[80px]" data-testid="grid-id-column">
<div class="w-full h-full bg-gray-100 flex pl-5 pr-1 items-center" data-testid="nc-check-all">
<template v-if="!readOnly">
<div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div>
<div
@ -993,21 +993,24 @@ const closeAddColumnDropdown = () => {
td,
th {
@apply border-gray-200 border-solid border-b border-r;
min-height: 41px !important;
height: 41px !important;
position: relative;
}
th {
@apply bg-gray-100;
}
td:not(:first-child) > div {
overflow: hidden;
@apply flex px-1 h-auto;
}
table,
td,
th {
@apply !border-1;
border-collapse: collapse;
table {
border-collapse: separate;
border-spacing: 0;
}
td {
@ -1027,7 +1030,7 @@ const closeAddColumnDropdown = () => {
// todo: replace with css variable
td.active::after {
@apply border-2 border-solid text-primary border-current bg-primary bg-opacity-5;
@apply border-1 border-solid text-primary border-current bg-primary bg-opacity-5;
}
//td.active::before {
@ -1035,6 +1038,34 @@ const closeAddColumnDropdown = () => {
// z-index:4;
// @apply absolute !w-[10px] !h-[10px] !right-[-5px] !bottom-[-5px] bg-primary;
//}
thead th:nth-child(1) {
position: sticky !important;
left: 0;
z-index: 5;
}
tbody td:nth-child(1) {
position: sticky !important;
left: 0;
z-index: 4;
background: white;
}
thead th:nth-child(2) {
position: sticky !important;
left: 80px;
z-index: 5;
@apply border-r-2 border-r-gray-300;
}
tbody td:nth-child(2) {
position: sticky !important;
left: 80px;
z-index: 4;
background: white;
@apply shadow-lg border-r-2 border-r-gray-300;
}
}
:deep {
@ -1081,7 +1112,7 @@ const closeAddColumnDropdown = () => {
position: sticky;
top: -1px;
@apply z-1;
@apply z-10 bg-gray-100;
&:hover {
.nc-no-label {

20
packages/nc-gui/components/smartsheet/Kanban.vue

@ -13,10 +13,12 @@ import {
MetaInj,
OpenNewRecordFormHookInj,
inject,
isImage,
isLTAR,
onBeforeMount,
onBeforeUnmount,
provide,
useAttachment,
useKanbanViewStoreOrThrow,
} from '#imports'
import type { Row as RowType } from '~/lib'
@ -55,6 +57,8 @@ const route = useRoute()
const router = useRouter()
const { getPossibleAttachmentSrc } = useAttachment()
const {
loadKanbanData,
loadMoreKanbanData,
@ -456,14 +460,14 @@ watch(view, async (nextView) => {
<div style="z-index: 1"></div>
</template>
<LazyNuxtImg
v-for="(attachment, index) in attachments(record)"
:key="`carousel-${record.row.id}-${index}`"
quality="90"
placeholder
class="h-52 object-cover"
:src="attachment.url"
/>
<template v-for="(attachment, index) in attachments(record)">
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-52 object-cover"
:srcs="getPossibleAttachmentSrc(attachment)"
/>
</template>
</a-carousel>
<MdiFileImageBox v-else class="w-full h-48 my-4 text-cool-gray-200" />

11
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -74,6 +74,8 @@ const setAsPrimaryValue = async () => {
await getMeta(meta?.value?.id as string, true)
eventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD)
// Successfully updated as primary column
message.success(t('msg.success.primaryColumnUpdated'))
@ -154,6 +156,7 @@ const duplicateColumn = async () => {
await $api.dbTableColumn.create(meta!.value!.id!, {
...columnCreatePayload,
pv: false,
column_order: {
order: newColumnOrder,
view_id: view.value?.id as string,
@ -241,7 +244,7 @@ const hideField = async () => {
</a-menu-item>
</template>
<a-divider class="!my-0" />
<a-menu-item @click="hideField">
<a-menu-item v-if="!column?.pv" @click="hideField">
<div v-e="['a:field:hide']" class="nc-column-insert-before nc-header-menu-item">
<MdiEyeOffOutline class="text-primary" />
<!-- Hide Field -->
@ -268,7 +271,7 @@ const hideField = async () => {
{{ t('general.insertAfter') }}
</div>
</a-menu-item>
<a-menu-item @click="addColumn(true)">
<a-menu-item v-if="!column?.pv" @click="addColumn(true)">
<div v-e="['a:field:insert:before']" class="nc-column-insert-before nc-header-menu-item">
<MdiTableColumnPlusBefore class="text-primary" />
<!-- Insert Before -->
@ -277,7 +280,7 @@ const hideField = async () => {
</a-menu-item>
<a-divider class="!my-0" />
<a-menu-item v-if="!virtual" @click="setAsPrimaryValue">
<a-menu-item v-if="!virtual && !column?.pv" @click="setAsPrimaryValue">
<div class="nc-column-set-primary nc-header-menu-item">
<MdiStar class="text-primary" />
@ -287,7 +290,7 @@ const hideField = async () => {
</div>
</a-menu-item>
<a-menu-item @click="deleteColumn">
<a-menu-item v-if="!column?.pv" @click="deleteColumn">
<div class="nc-column-delete nc-header-menu-item">
<MdiDeleteOutline class="text-error" />
<!-- Delete -->

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

@ -68,6 +68,12 @@ watch(
const numberOfHiddenFields = computed(() => filteredFieldList.value?.filter((field) => !field.show)?.length)
const gridPrimaryValueField = computed(() => {
if (activeView.value?.type !== ViewTypes.GRID) return null
const pvCol = Object.values(metaColumnById.value)?.find((col) => col?.pv)
return filteredFieldList.value?.find((field) => field.fk_column_id === pvCol?.id)
})
const onMove = (_event: { moved: { newIndex: number } }) => {
// todo : sync with server
if (!fields.value) return
@ -188,7 +194,7 @@ useMenuCloseOnEsc(open)
<Draggable v-model="fields" item-key="id" @change="onMove($event)">
<template #item="{ element: field, index: index }">
<div
v-show="filteredFieldList.includes(field)"
v-if="filteredFieldList.filter((el) => el !== gridPrimaryValueField).includes(field)"
:key="field.id"
class="px-2 py-1 flex items-center"
:data-testid="`nc-fields-menu-${field.title}`"
@ -212,6 +218,31 @@ useMenuCloseOnEsc(open)
<MdiDrag class="cursor-move" />
</div>
</template>
<template v-if="activeView?.type === ViewTypes.GRID" #header>
<div
v-if="gridPrimaryValueField"
:key="`pv-${gridPrimaryValueField.id}`"
class="px-2 py-1 flex items-center"
:data-testid="`nc-fields-menu-${gridPrimaryValueField.title}`"
@click.stop
>
<a-tooltip placement="bottom">
<template #title>
<span class="text-sm">Primary Value</span>
</template>
<MdiTableKey class="text-xs" />
</a-tooltip>
<div class="flex items-center px-[8px]">
<component :is="getIcon(metaColumnById[filteredFieldList[0].fk_column_id as string])" />
<span>{{ filteredFieldList[0].title }}</span>
</div>
<div class="flex-1" />
</div>
</template>
</Draggable>
</div>

5
packages/nc-gui/composables/useApi/interceptors.ts

@ -1,12 +1,12 @@
import type { Api } from 'nocodb-sdk'
import { navigateTo, useGlobal, useRoute, useRouter } from '#imports'
import { navigateTo, useGlobal, useRouter } from '#imports'
const DbNotFoundMsg = 'Database config not found'
export function addAxiosInterceptors(api: Api<any>) {
const state = useGlobal()
const router = useRouter()
const route = useRoute()
const route = $(router.currentRoute)
api.instance.interceptors.request.use((config) => {
config.headers['xc-gui'] = 'true'
@ -40,7 +40,6 @@ export function addAxiosInterceptors(api: Api<any>) {
// Logout user if token refresh didn't work or user is disabled
if (error.config.url === '/auth/token/refresh') {
state.signOut()
return Promise.reject(error)
}

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

@ -0,0 +1,40 @@
import { mimeTypes, openLink, useGlobal } from '#imports'
const useAttachment = () => {
const { appInfo } = useGlobal()
const getPossibleAttachmentSrc = (item: Record<string, any>) => {
const res: string[] = []
if (item?.path) res.push(`${appInfo.value.ncSiteUrl}/${item.path}`)
if (item?.url) res.push(item.url)
return res
}
const getAttachmentSrc = async (item: Record<string, any>) => {
if (item?.data) {
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) {
return source
}
}
return null
}
const openAttachment = async (item: Record<string, any>) => {
openLink(await getAttachmentSrc(item))
}
return {
getAttachmentSrc,
getPossibleAttachmentSrc,
openAttachment,
}
}
export default useAttachment

6
packages/nc-gui/composables/useCellUrlConfig.ts

@ -1,5 +1,5 @@
import type { MaybeRef } from '@vueuse/core'
import { computed, unref, useRoute } from '#imports'
import { computed, unref, useRouter } from '#imports'
export interface CellUrlOptions {
behavior?: string
@ -21,7 +21,9 @@ const parseUrlRules = (serialized?: string): ParsedRules[] | undefined => {
}
export function useCellUrlConfig(url?: MaybeRef<string>) {
const route = useRoute()
const router = useRouter()
const route = $(router.currentRoute)
const config = $computed(() => ({
behavior: route.query.url_behavior as string | undefined,

6
packages/nc-gui/composables/useDashboard.ts

@ -1,7 +1,9 @@
import { computed, useRoute } from '#imports'
import { computed, useRouter } from '#imports'
export function useDashboard() {
const route = useRoute()
const router = useRouter()
const route = $(router.currentRoute)
const dashboardUrl = computed(() => {
// todo: test in different scenarios

29
packages/nc-gui/composables/useGlobal/actions.ts

@ -28,19 +28,22 @@ export function useGlobalActions(state: State): Actions {
const nuxtApp = useNuxtApp()
const t = nuxtApp.vueApp.i18n.global.t
nuxtApp.$api.instance
.post('/auth/token/refresh', null, {
withCredentials: true,
})
.then((response) => {
if (response.data?.token) {
signIn(response.data.token)
}
})
.catch((err) => {
message.error(err.message || t('msg.error.youHaveBeenSignedOut'))
signOut()
})
return new Promise((resolve) => {
nuxtApp.$api.instance
.post('/auth/token/refresh', null, {
withCredentials: true,
})
.then((response) => {
if (response.data?.token) {
signIn(response.data.token)
}
})
.catch((err) => {
message.error(err.message || t('msg.error.youHaveBeenSignedOut'))
signOut()
})
.finally(resolve)
})
}
const loadAppInfo = async () => {

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

@ -53,7 +53,7 @@ export const useGlobal = createGlobalState((): UseGlobalReturn => {
state.jwtPayload.value.exp &&
state.jwtPayload.value.exp - 5 * 60 < state.timestamp.value / 1000
),
async (expiring) => {
async (expiring: boolean) => {
if (getters.signedIn.value && state.jwtPayload.value && expiring) {
await actions.refreshToken()
}

62
packages/nc-gui/composables/useKanbanViewStore.ts

@ -1,15 +1,5 @@
import type { ComputedRef, Ref } from 'vue'
import type {
Api,
AttachmentType,
ColumnType,
KanbanType,
SelectOptionType,
SelectOptionsType,
TableType,
ViewType,
} from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import type { Api, ColumnType, KanbanType, SelectOptionType, SelectOptionsType, TableType, ViewType } from 'nocodb-sdk'
import type { Row } from '~/lib'
import {
IsPublicInj,
@ -25,7 +15,6 @@ import {
ref,
useApi,
useFieldQuery,
useGlobal,
useI18n,
useInjectionState,
useNuxtApp,
@ -55,8 +44,6 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
const { $e, $api } = useNuxtApp()
const { appInfo } = useGlobal()
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()
const { sharedView, fetchSharedViewData, fetchSharedViewGroupedData } = useSharedView()
@ -91,10 +78,6 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
return where
})
const attachmentColumns = computed(() =>
(meta.value?.columns as ColumnType[])?.filter((c) => c.uidt === UITypes.Attachment).map((c) => c.title),
)
provide(SharedViewPasswordInj, password)
// kanban view meta data
@ -144,27 +127,6 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
rowMeta: {},
}))
async function getAttachmentUrl(item: AttachmentType) {
const path = item?.path
// if path doesn't exist, use `item.url`
if (path) {
// try ${appInfo.value.ncSiteUrl}/${item.path} first
const url = `${appInfo.value.ncSiteUrl}/${item.path}`
try {
const res = await fetch(url)
if (res.ok) {
// use `url` if it is accessible
return Promise.resolve(url)
}
} catch {
// for some cases, `url` is not accessible as expected
// do nothing here
}
}
// if it fails, use the original url
return Promise.resolve(item.url)
}
async function loadKanbanData() {
if ((!project?.value?.id || !meta.value?.id || !viewMeta?.value?.id || !groupingFieldColumn?.value?.id) && !isPublic.value)
return
@ -193,28 +155,8 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
}
for (const data of groupData) {
const records = []
const key = data.key
// TODO: optimize
// reconstruct the url
// See /packages/nocodb/src/lib/version-upgrader/ncAttachmentUpgrader.ts for the details
for (const record of data.value.list) {
for (const attachmentColumn of attachmentColumns.value) {
// attachment column can be hidden
if (!record[attachmentColumn!]) continue
const oldAttachment = JSON.parse(record[attachmentColumn!])
const newAttachment = []
for (const attachmentObj of oldAttachment) {
newAttachment.push({
...attachmentObj,
url: await getAttachmentUrl(attachmentObj),
})
}
record[attachmentColumn!] = newAttachment
}
records.push(record)
}
formattedData.value.set(key, formatData(records))
formattedData.value.set(key, formatData(data.value.list))
countByStack.value.set(key, data.value.pageInfo.totalRows || 0)
}
}

6
packages/nc-gui/composables/useLTARStore.ts

@ -16,6 +16,7 @@ import {
useMetas,
useNuxtApp,
useProject,
useRouter,
useSharedView,
watch,
} from '#imports'
@ -107,7 +108,10 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const loadChildrenExcludedList = async () => {
try {
if (isPublic) {
const route = useRoute()
const router = useRouter()
const route = $(router.currentRoute)
childrenExcludedList.value = await $api.public.dataRelationList(
route.params.viewId as string,
column.value.id,

33
packages/nc-gui/composables/useProject.ts

@ -5,31 +5,30 @@ import {
ClientType,
computed,
createEventHook,
createSharedComposable,
ref,
useApi,
useGlobal,
useInjectionState,
useNuxtApp,
useRoles,
useRoute,
useRouter,
useTheme,
} from '#imports'
import type { ProjectMetaInfo, ThemeConfig } from '~/lib'
const [setup, use] = useInjectionState(() => {
export const useProject = createSharedComposable(() => {
const { $e } = useNuxtApp()
const { api, isLoading } = useApi()
const route = useRoute()
const router = useRouter()
const route = $(router.currentRoute)
const { includeM2M } = useGlobal()
const { setTheme, theme } = useTheme()
const router = useRouter()
const { projectRoles, loadProjectRoles } = useRoles()
const projectLoadedHook = createEventHook<ProjectType>()
@ -178,6 +177,14 @@ const [setup, use] = useInjectionState(() => {
setTheme()
}
watch(
() => route.params.projectType,
(n) => {
if (!n) reset()
},
{ immediate: true },
)
return {
project,
bases,
@ -201,16 +208,4 @@ const [setup, use] = useInjectionState(() => {
lastOpenedViewMap,
isXcdbBase,
}
}, 'useProject')
export const provideProject = setup
export function useProject() {
const state = use()
if (!state) {
return setup()
}
return state
}
})

18
packages/nc-gui/composables/useTabs.ts

@ -1,5 +1,5 @@
import type { WritableComputedRef } from '@vue/reactivity'
import { computed, navigateTo, ref, useInjectionState, useProject, useRoute, useRouter, watch } from '#imports'
import { computed, createSharedComposable, navigateTo, ref, useProject, useRouter, watch } from '#imports'
import type { TabItem } from '~/lib'
import { TabType } from '~/lib'
@ -10,13 +10,13 @@ function getPredicate(key: Partial<TabItem>) {
(!('type' in key) || tab.type === key.type)
}
const [setup, use] = useInjectionState(() => {
export const useTabs = createSharedComposable(() => {
const tabs = ref<TabItem[]>([])
const route = useRoute()
const router = useRouter()
const route = $(router.currentRoute)
const { bases, tables } = useProject()
const projectType = $computed(() => route.params.projectType as string)
@ -157,13 +157,3 @@ const [setup, use] = useInjectionState(() => {
return { tabs, addTab, activeTabIndex, activeTab, clearTabs, closeTab, updateTab }
})
export function useTabs() {
const state = use()
if (!state) {
return setup()
}
return state
}

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

@ -162,7 +162,10 @@ export function useViewColumns(
.update(view.value.id, {
show_system_fields: v,
})
.finally(() => reloadData?.())
.finally(() => {
loadViewColumns()
reloadData?.()
})
}
view.value.show_system_fields = v
}
@ -173,6 +176,8 @@ export function useViewColumns(
const filteredFieldList = computed(() => {
return (
fields.value?.filter((field: Field) => {
if (metaColumnById?.value?.[field.fk_column_id!]?.pv) return true
// hide system columns if not enabled
if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) {
return false
@ -195,7 +200,8 @@ export function useViewColumns(
!showSystemFields.value &&
metaColumnById.value &&
metaColumnById?.value?.[field.fk_column_id!] &&
isSystemColumn(metaColumnById.value?.[field.fk_column_id!])
isSystemColumn(metaColumnById.value?.[field.fk_column_id!]) &&
!metaColumnById.value?.[field.fk_column_id!]?.pv
) {
return false
}

63
packages/nc-gui/composables/useViewData.ts

@ -1,15 +1,5 @@
import { UITypes, ViewTypes } from 'nocodb-sdk'
import type {
Api,
AttachmentType,
ColumnType,
FormColumnType,
FormType,
GalleryType,
PaginatedType,
TableType,
ViewType,
} from 'nocodb-sdk'
import type { Api, ColumnType, FormColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import {
IsPublicInj,
@ -29,7 +19,6 @@ import {
useMetas,
useNuxtApp,
useProject,
useRoute,
useRouter,
useSharedView,
useSmartsheetStoreOrThrow,
@ -59,7 +48,7 @@ export function useViewData(
const router = useRouter()
const route = useRoute()
const route = $(router.currentRoute)
const { appInfo } = $(useGlobal())
@ -91,10 +80,6 @@ export function useViewData(
const { isUIAllowed } = useUIPermission()
const attachmentColumns = computed(() =>
(meta.value?.columns as ColumnType[])?.filter((c) => c.uidt === UITypes.Attachment).map((c) => c.title),
)
const routeQuery = $computed(() => route.query as Record<string, string>)
const paginationData = computed({
@ -201,28 +186,6 @@ export function useViewData(
}
}
// TODO: refactor
async function getAttachmentUrl(item: AttachmentType) {
const path = item?.path
// if path doesn't exist, use `item.url`
if (path) {
// try ${appInfo.value.ncSiteUrl}/${item.path} first
const url = `${appInfo.ncSiteUrl}/${item.path}`
try {
const res = await fetch(url)
if (res.ok) {
// use `url` if it is accessible
return Promise.resolve(url)
}
} catch {
// for some cases, `url` is not accessible as expected
// do nothing here
}
}
// if it fails, use the original url
return Promise.resolve(item.url)
}
async function loadData(params: Parameters<Api<any>['dbViewRow']['list']>[4] = {}) {
if ((!project?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic.value) return
const response = !isPublic.value
@ -234,27 +197,7 @@ export function useViewData(
where: where?.value,
})
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value })
// reconstruct the url
// See /packages/nocodb/src/lib/version-upgrader/ncAttachmentUpgrader.ts for the details
const records = []
for (const record of response.list) {
for (const attachmentColumn of attachmentColumns.value) {
// attachment column can be hidden
if (!record[attachmentColumn!]) continue
const oldAttachment =
typeof record[attachmentColumn!] === 'string' ? JSON.parse(record[attachmentColumn!]) : record[attachmentColumn!]
const newAttachment = []
for (const attachmentObj of oldAttachment) {
newAttachment.push({
...attachmentObj,
url: await getAttachmentUrl(attachmentObj),
})
}
record[attachmentColumn!] = newAttachment
}
records.push(record)
}
formattedData.value = formatData(records)
formattedData.value = formatData(response.list)
paginationData.value = response.pageInfo
// to cater the case like when querying with a non-zero offset

6
packages/nc-gui/middleware/auth.global.ts

@ -52,7 +52,11 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
return navigateTo('/signup')
}
return navigateTo('/signin')
/** try generating access token using refresh token */
await state.refreshToken()
/** if user is still not signed in, redirect to signin page */
if (!state.signedIn.value) return navigateTo('/signin')
} else if (to.meta.requiresAuth === false && state.signedIn.value) {
/**
* if user was turned away from non-auth page but also came from a non-auth page (e.g. user went to /signin and reloaded the page)

4
packages/nc-gui/plugins/tele.ts

@ -1,12 +1,12 @@
import type { Socket } from 'socket.io-client'
import io from 'socket.io-client'
import { defineNuxtPlugin, useGlobal, useRoute, useRouter, watch } from '#imports'
import { defineNuxtPlugin, useGlobal, useRouter, watch } from '#imports'
// todo: ignore init if tele disabled
export default defineNuxtPlugin(async (nuxtApp) => {
const router = useRouter()
const route = useRoute()
const route = $(router.currentRoute)
const { appInfo } = $(useGlobal())

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

@ -21,3 +21,4 @@ export * from './stringUtils'
export * from './memStorage'
export * from './browserUtils'
export * from './geoDataUtils'
export * from './mimeTypeUtils'

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

@ -0,0 +1,714 @@
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',
docx: 'mdi-file-word-outline',
dotx: 'mdi-file-word-outline',
odt: 'mdi-file-word-outline',
odm: 'mdi-file-word-outline',
ott: 'mdi-file-word-outline',
ppt: 'mdi-file-powerpoint-box',
pptx: 'mdi-file-powerpoint-box',
sldx: 'mdi-file-powerpoint-box',
ppsx: 'mdi-file-powerpoint-box',
potx: 'mdi-file-powerpoint-box',
xls: 'mdi-file-excel-outline',
xlam: 'mdi-file-excel-outline',
xlsb: 'mdi-file-excel-outline',
xltm: 'mdi-file-excel-outline',
xlsm: 'mdi-file-excel-outline',
}
export { mimeTypes, mimeIcons }

4
packages/noco-docs/content/en/engineering/unit-testing.md

@ -43,7 +43,7 @@ npm run test:unit
### Folder Structure
The root folder for unit tests is `packages/tests/unit`
The root folder for unit tests is `packages/nocodb/tests/unit`
- `rest` folder contains all the test suites for rest apis.
- `model` folder contains all the test suites for models.
@ -69,7 +69,7 @@ We will create an `Table` test suite as an example.
#### Configure test
We will configure `beforeEach` which is called before each test is executed. We will use `init` function from `nocodb/packages/tests/unit/init/index.ts`, which is a helper function which configures the test environment(i.e resetting state, etc.).
We will configure `beforeEach` which is called before each test is executed. We will use `init` function from `nocodb/packages/nocodb/tests/unit/init/index.ts`, which is a helper function which configures the test environment(i.e resetting state, etc.).
`init` does the following things -

2
packages/nocodb/src/lib/Noco.ts

@ -105,7 +105,7 @@ export default class Noco {
constructor() {
process.env.PORT = process.env.PORT || '8080';
// todo: move
process.env.NC_VERSION = '0104002';
process.env.NC_VERSION = '0104003';
// if env variable NC_MINIMAL_DBS is set, then disable project creation with external sources
if (process.env.NC_MINIMAL_DBS) {

49
packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts

@ -2367,39 +2367,26 @@ class KnexClient extends SqlClient {
const foreignKeyName = args.foreignKeyName || null;
try {
// s = await this.sqlClient.schema.index(Object.keys(args.columns));
await this.sqlClient.schema.table(args.childTable, function (table) {
table = table
.foreign(args.childColumn, foreignKeyName)
.references(args.parentColumn)
.on(args.parentTable);
if (args.onUpdate) {
table = table.onUpdate(args.onUpdate);
}
if (args.onDelete) {
table = table.onDelete(args.onDelete);
const upQb = this.sqlClient.schema.table(
args.childTable,
function (table) {
table = table
.foreign(args.childColumn, foreignKeyName)
.references(args.parentColumn)
.on(args.parentTable);
if (args.onUpdate) {
table = table.onUpdate(args.onUpdate);
}
if (args.onDelete) {
table.onDelete(args.onDelete);
}
}
});
);
const upStatement =
this.querySeparator() +
(await this.sqlClient.schema
.table(args.childTable, function (table) {
table = table
.foreign(args.childColumn, foreignKeyName)
.references(args.parentColumn)
.on(args.parentTable);
await upQb;
if (args.onUpdate) {
table = table.onUpdate(args.onUpdate);
}
if (args.onDelete) {
table = table.onDelete(args.onDelete);
}
})
.toQuery());
const upStatement = this.querySeparator() + upQb.toQuery();
this.emit(`Success : ${upStatement}`);
@ -2407,7 +2394,7 @@ class KnexClient extends SqlClient {
this.querySeparator() +
this.sqlClient.schema
.table(args.childTable, function (table) {
table = table.dropForeign(args.childColumn, foreignKeyName);
table.dropForeign(args.childColumn, foreignKeyName);
})
.toQuery();

9
packages/nocodb/src/lib/meta/api/attachmentApis.ts

@ -17,7 +17,9 @@ import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants';
const isUploadAllowed = async (req: Request, _res: Response, next: any) => {
if (!req['user']?.id) {
NcError.unauthorized('Unauthorized');
if (!req['user']?.isPublicBase) {
NcError.unauthorized('Unauthorized');
}
}
try {
@ -25,6 +27,7 @@ const isUploadAllowed = async (req: Request, _res: Response, next: any) => {
if (
req['user'].roles?.includes(OrgUserRoles.SUPER_ADMIN) ||
req['user'].roles?.includes(OrgUserRoles.CREATOR) ||
req['user'].roles?.includes(ProjectRoles.EDITOR) ||
// if viewer then check at-least one project have editor or higher role
// todo: cache
!!(await Noco.ncMeta
@ -54,7 +57,7 @@ export async function upload(req: Request, res: Response) {
(req as any).files?.map(async (file) => {
const fileName = `${nanoid(18)}${path.extname(file.originalname)}`;
let url = await storageAdapter.fileCreate(
const url = await storageAdapter.fileCreate(
slash(path.join(destPath, fileName)),
file
);
@ -98,7 +101,7 @@ export async function uploadViaURL(req: Request, res: Response) {
const fileName = `${nanoid(18)}${_fileName || url.split('/').pop()}`;
let attachmentUrl = await (storageAdapter as any).fileCreateByUrl(
const attachmentUrl = await (storageAdapter as any).fileCreateByUrl(
slash(path.join(destPath, fileName)),
url
);

263
packages/nocodb/src/lib/meta/api/baseApis.ts

@ -1,22 +1,13 @@
import { Request, Response } from 'express';
import Project from '../../models/Project';
import { BaseListType, ModelTypes, UITypes } from 'nocodb-sdk';
import { BaseListType } from 'nocodb-sdk';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import { syncBaseMigration } from '../helpers/syncMigration';
import { IGNORE_TABLES } from '../../utils/common/BaseApiBuilder';
import Column from '../../models/Column';
import Model from '../../models/Model';
import NcHelp from '../../utils/NcHelp';
import Base from '../../models/Base';
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import { Tele } from 'nc-help';
import getColumnUiType from '../helpers/getColumnUiType';
import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue';
import { extractAndGenerateManyToManyRelations } from './metaDiffApis';
import { metaApiMetrics } from '../helpers/apiMetrics';
import { populateMeta } from './helpers';
export async function baseGet(
req: Request<any, any, any>,
@ -107,256 +98,6 @@ async function baseCreate(req: Request<any, any>, res) {
res.json(base);
}
async function populateMeta(base: Base, project: Project): Promise<any> {
const info = {
type: 'rest',
apiCount: 0,
tablesCount: 0,
relationsCount: 0,
viewsCount: 0,
client: base?.getConnectionConfig()?.client,
timeTaken: 0,
};
const t = process.hrtime();
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
let order = 1;
const models2: { [tableName: string]: Model } = {};
const virtualColumnsInsert = [];
/* Get all relations */
const relations = (await sqlClient.relationListAll())?.data?.list;
info.relationsCount = relations.length;
let tables = (await sqlClient.tableList())?.data?.list
?.filter(({ tn }) => !IGNORE_TABLES.includes(tn))
?.map((t) => {
t.order = ++order;
t.title = getTableNameAlias(t.tn, project.prefix, base);
t.table_name = t.tn;
return t;
});
/* filter based on prefix */
if (base.is_meta && project?.prefix) {
tables = tables.filter((t) => {
return t?.tn?.startsWith(project?.prefix);
});
}
info.tablesCount = tables.length;
tables.forEach((t) => {
t.title = getTableNameAlias(t.tn, project.prefix, base);
});
relations.forEach((r) => {
r.title = getTableNameAlias(r.tn, project.prefix, base);
r.rtitle = getTableNameAlias(r.rtn, project.prefix, base);
});
// await this.syncRelations();
const tableMetasInsert = tables.map((table) => {
return async () => {
/* filter relation where this table is present */
const tableRelations = relations.filter(
(r) => r.tn === table.tn || r.rtn === table.tn
);
const columns: Array<
Omit<Column, 'column_name' | 'title'> & {
cn: string;
system?: boolean;
}
> = (await sqlClient.columnList({ tn: table.tn }))?.data?.list;
const hasMany =
table.type === 'view'
? []
: tableRelations.filter((r) => r.rtn === table.tn);
const belongsTo =
table.type === 'view'
? []
: tableRelations.filter((r) => r.tn === table.tn);
mapDefaultPrimaryValue(columns);
// add vitual columns
const virtualColumns = [
...hasMany.map((hm) => {
return {
uidt: UITypes.LinkToAnotherRecord,
type: 'hm',
hm,
title: `${hm.title} List`,
};
}),
...belongsTo.map((bt) => {
// find and mark foreign key column
const fkColumn = columns.find((c) => c.cn === bt.cn);
if (fkColumn) {
fkColumn.uidt = UITypes.ForeignKey;
fkColumn.system = true;
}
return {
uidt: UITypes.LinkToAnotherRecord,
type: 'bt',
bt,
title: `${bt.rtitle}`,
};
}),
];
// await Model.insert(project.id, base.id, meta);
/* create nc_models and its rows if it doesn't exists */
models2[table.table_name] = await Model.insert(project.id, base.id, {
table_name: table.tn || table.table_name,
title: table.title,
type: table.type || 'table',
order: table.order,
});
// table crud apis
info.apiCount += 5;
let colOrder = 1;
for (const column of columns) {
await Column.insert({
uidt: column.uidt || getColumnUiType(base, column),
fk_model_id: models2[table.tn].id,
...column,
title: getColumnNameAlias(column.cn, base),
column_name: column.cn,
order: colOrder++,
});
}
virtualColumnsInsert.push(async () => {
const columnNames = {};
for (const column of virtualColumns) {
// generate unique name if there is any duplicate column name
let c = 0;
while (`${column.title}${c || ''}` in columnNames) {
c++;
}
column.title = `${column.title}${c || ''}`;
columnNames[column.title] = true;
const rel = column.hm || column.bt;
const rel_column_id = (await models2?.[rel.tn]?.getColumns())?.find(
(c) => c.column_name === rel.cn
)?.id;
const tnId = models2?.[rel.tn]?.id;
const ref_rel_column_id = (
await models2?.[rel.rtn]?.getColumns()
)?.find((c) => c.column_name === rel.rcn)?.id;
const rtnId = models2?.[rel.rtn]?.id;
try {
await Column.insert<LinkToAnotherRecordColumn>({
project_id: project.id,
db_alias: base.id,
fk_model_id: models2[table.tn].id,
cn: column.cn,
title: column.title,
uidt: column.uidt,
type: column.hm ? 'hm' : column.mm ? 'mm' : 'bt',
// column_id,
fk_child_column_id: rel_column_id,
fk_parent_column_id: ref_rel_column_id,
fk_index_name: rel.fkn,
ur: rel.ur,
dr: rel.dr,
order: colOrder++,
fk_related_model_id: column.hm ? tnId : rtnId,
system: column.system,
});
// nested relations data apis
info.apiCount += 5;
} catch (e) {
console.log(e);
}
}
});
};
});
/* handle xc_tables update in parallel */
await NcHelp.executeOperations(tableMetasInsert, base.type);
await NcHelp.executeOperations(virtualColumnsInsert, base.type);
await extractAndGenerateManyToManyRelations(Object.values(models2));
let views: Array<{ order: number; table_name: string; title: string }> = (
await sqlClient.viewList()
)?.data?.list
// ?.filter(({ tn }) => !IGNORE_TABLES.includes(tn))
?.map((v) => {
v.order = ++order;
v.table_name = v.view_name;
v.title = getTableNameAlias(v.view_name, project.prefix, base);
return v;
});
/* filter based on prefix */
if (base.is_meta && project?.prefix) {
views = tables.filter((t) => {
return t?.tn?.startsWith(project?.prefix);
});
}
info.viewsCount = views.length;
const viewMetasInsert = views.map((table) => {
return async () => {
const columns = (await sqlClient.columnList({ tn: table.table_name }))
?.data?.list;
/* create nc_models and its rows if it doesn't exists */
models2[table.table_name] = await Model.insert(project.id, base.id, {
table_name: table.table_name,
title: getTableNameAlias(table.table_name, project.prefix, base),
// todo: sanitize
type: ModelTypes.VIEW,
order: table.order,
});
let colOrder = 1;
// view apis
info.apiCount += 2;
for (const column of columns) {
await Column.insert({
fk_model_id: models2[table.table_name].id,
...column,
title: getColumnNameAlias(column.cn, base),
order: colOrder++,
uidt: getColumnUiType(base, column),
});
}
};
});
await NcHelp.executeOperations(viewMetasInsert, base.type);
const t1 = process.hrtime(t);
const t2 = t1[0] + t1[1] / 1000000000;
(info as any).timeTaken = t2.toFixed(1);
return info;
}
export default (router) => {
router.get(
'/api/v1/db/meta/projects/:projectId/bases/:baseId',

57
packages/nocodb/src/lib/meta/api/columnApis.ts

@ -50,12 +50,26 @@ export enum Altered {
UPDATE_COLUMN = 8,
}
// generate unique foreign key constraint name for foreign key
const generateFkName = (parent: TableType, child: TableType) => {
// generate a unique constraint name by taking first 10 chars of parent and child table name (by replacing all non word chars with _)
// and appending a random string of 15 chars maximum length.
// In database constraint name can be upto 64 chars and here we are generating a name of maximum 40 chars
const constraintName = `fk_${parent.table_name
.replace(/\W+/g, '_')
.slice(0, 10)}_${child.table_name
.replace(/\W+/g, '_')
.slice(0, 10)}_${randomID(15)}`;
return constraintName;
};
async function createHmAndBtColumn(
child: Model,
parent: Model,
childColumn: Column,
type?: RelationTypes,
alias?: string,
fkColName?: string,
virtual = false,
isSystemCol = false
) {
@ -79,6 +93,7 @@ async function createHmAndBtColumn(
fk_related_model_id: parent.id,
virtual,
system: isSystemCol,
fk_index_name: fkColName,
});
}
// save hm column
@ -97,6 +112,7 @@ async function createHmAndBtColumn(
fk_related_model_id: child.id,
virtual,
system: isSystemCol,
fk_index_name: fkColName,
});
}
}
@ -262,6 +278,7 @@ export async function columnAdd(
`${parent.table_name}_id`
);
let foreignKeyName;
{
// create foreign key
const newColumn = {
@ -307,6 +324,7 @@ export async function columnAdd(
// ignore relation creation if virtual
if (!(req.body as LinkToAnotherColumnReqType).virtual) {
foreignKeyName = generateFkName(parent, child);
// create relation
await sqlMgr.sqlOpPlus(base, 'relationCreate', {
childColumn: fkColName,
@ -316,6 +334,7 @@ export async function columnAdd(
onUpdate: 'NO ACTION',
type: 'real',
parentColumn: parent.primaryKey.column_name,
foreignKeyName,
});
}
@ -338,6 +357,7 @@ export async function columnAdd(
childColumn,
(req.body as LinkToAnotherColumnReqType).type as RelationTypes,
(req.body as LinkToAnotherColumnReqType).title,
foreignKeyName,
(req.body as LinkToAnotherColumnReqType).virtual
);
} else if ((req.body as LinkToAnotherColumnReqType).type === 'mm') {
@ -399,7 +419,13 @@ export async function columnAdd(
columns: associateTableCols,
});
let foreignKeyName1;
let foreignKeyName2;
if (!(req.body as LinkToAnotherColumnReqType).virtual) {
foreignKeyName1 = generateFkName(parent, child);
foreignKeyName2 = generateFkName(parent, child);
const rel1Args = {
...req.body,
childTable: aTn,
@ -407,6 +433,7 @@ export async function columnAdd(
parentTable: parent.table_name,
parentColumn: parentPK.column_name,
type: 'real',
foreignKeyName: foreignKeyName1,
};
const rel2Args = {
...req.body,
@ -415,6 +442,7 @@ export async function columnAdd(
parentTable: child.table_name,
parentColumn: childPK.column_name,
type: 'real',
foreignKeyName: foreignKeyName2,
};
await sqlMgr.sqlOpPlus(base, 'relationCreate', rel1Args);
@ -433,6 +461,7 @@ export async function columnAdd(
childCol,
null,
null,
foreignKeyName1,
(req.body as LinkToAnotherColumnReqType).virtual,
true
);
@ -442,6 +471,7 @@ export async function columnAdd(
parentCol,
null,
null,
foreignKeyName2,
(req.body as LinkToAnotherColumnReqType).virtual,
true
);
@ -1724,6 +1754,31 @@ const deleteHmOrBtRelation = async (
},
ignoreFkDelete = false
) => {
let foreignKeyName;
// if relationColOpt is not provided, extract it from child table
// and get the foreign key name for dropping the foreign key
if (!relationColOpt) {
foreignKeyName = (
(
await childTable.getColumns().then((cols) => {
return cols?.find((c) => {
return (
c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions.fk_related_model_id === parentTable.id &&
(c.colOptions as LinkToAnotherRecordType).fk_child_column_id ===
childColumn.id &&
(c.colOptions as LinkToAnotherRecordType).fk_parent_column_id ===
parentColumn.id
);
});
})
).colOptions as LinkToAnotherRecordType
).fk_index_name;
} else {
foreignKeyName = relationColOpt.fk_index_name;
}
// todo: handle relation delete exception
try {
await sqlMgr.sqlOpPlus(base, 'relationDelete', {
@ -1731,7 +1786,7 @@ const deleteHmOrBtRelation = async (
childTable: childTable.table_name,
parentTable: parentTable.table_name,
parentColumn: parentColumn.column_name,
// foreignKeyName: relation.fkn
foreignKeyName,
});
} catch (e) {
console.log(e);

3
packages/nocodb/src/lib/meta/api/helpers/index.ts

@ -0,0 +1,3 @@
import { populateMeta } from './populateMeta';
export { populateMeta };

278
packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts

@ -0,0 +1,278 @@
import Project from '../../../models/Project';
import Column from '../../../models/Column';
import Model from '../../../models/Model';
import NcHelp from '../../../utils/NcHelp';
import Base from '../../../models/Base';
import View from '../../../models/View';
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2';
import getTableNameAlias, {
getColumnNameAlias,
} from '../../helpers/getTableName';
import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn';
import getColumnUiType from '../../helpers/getColumnUiType';
import mapDefaultPrimaryValue from '../../helpers/mapDefaultPrimaryValue';
import { extractAndGenerateManyToManyRelations } from '../metaDiffApis';
import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk';
import { IGNORE_TABLES } from '../../../utils/common/BaseApiBuilder';
export async function populateMeta(base: Base, project: Project): Promise<any> {
const info = {
type: 'rest',
apiCount: 0,
tablesCount: 0,
relationsCount: 0,
viewsCount: 0,
client: base?.getConnectionConfig()?.client,
timeTaken: 0,
};
const t = process.hrtime();
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
let order = 1;
const models2: { [tableName: string]: Model } = {};
const virtualColumnsInsert = [];
/* Get all relations */
const relations = (await sqlClient.relationListAll())?.data?.list;
info.relationsCount = relations.length;
let tables = (await sqlClient.tableList())?.data?.list
?.filter(({ tn }) => !IGNORE_TABLES.includes(tn))
?.map((t) => {
t.order = ++order;
t.title = getTableNameAlias(t.tn, project.prefix, base);
t.table_name = t.tn;
return t;
});
/* filter based on prefix */
if (base.is_meta && project?.prefix) {
tables = tables.filter((t) => {
return t?.tn?.startsWith(project?.prefix);
});
}
info.tablesCount = tables.length;
tables.forEach((t) => {
t.title = getTableNameAlias(t.tn, project.prefix, base);
});
relations.forEach((r) => {
r.title = getTableNameAlias(r.tn, project.prefix, base);
r.rtitle = getTableNameAlias(r.rtn, project.prefix, base);
});
// await this.syncRelations();
const tableMetasInsert = tables.map((table) => {
return async () => {
/* filter relation where this table is present */
const tableRelations = relations.filter(
(r) => r.tn === table.tn || r.rtn === table.tn
);
const columns: Array<
Omit<Column, 'column_name' | 'title'> & {
cn: string;
system?: boolean;
}
> = (await sqlClient.columnList({ tn: table.tn }))?.data?.list;
const hasMany =
table.type === 'view'
? []
: tableRelations.filter((r) => r.rtn === table.tn);
const belongsTo =
table.type === 'view'
? []
: tableRelations.filter((r) => r.tn === table.tn);
mapDefaultPrimaryValue(columns);
// add vitual columns
const virtualColumns = [
...hasMany.map((hm) => {
return {
uidt: UITypes.LinkToAnotherRecord,
type: 'hm',
hm,
title: `${hm.title} List`,
};
}),
...belongsTo.map((bt) => {
// find and mark foreign key column
const fkColumn = columns.find((c) => c.cn === bt.cn);
if (fkColumn) {
fkColumn.uidt = UITypes.ForeignKey;
fkColumn.system = true;
}
return {
uidt: UITypes.LinkToAnotherRecord,
type: 'bt',
bt,
title: `${bt.rtitle}`,
};
}),
];
// await Model.insert(project.id, base.id, meta);
/* create nc_models and its rows if it doesn't exists */
models2[table.table_name] = await Model.insert(project.id, base.id, {
table_name: table.tn || table.table_name,
title: table.title,
type: table.type || 'table',
order: table.order,
});
// table crud apis
info.apiCount += 5;
let colOrder = 1;
for (const column of columns) {
await Column.insert({
uidt: column.uidt || getColumnUiType(base, column),
fk_model_id: models2[table.tn].id,
...column,
title: getColumnNameAlias(column.cn, base),
column_name: column.cn,
order: colOrder++,
});
}
virtualColumnsInsert.push(async () => {
const columnNames = {};
for (const column of virtualColumns) {
// generate unique name if there is any duplicate column name
let c = 0;
while (`${column.title}${c || ''}` in columnNames) {
c++;
}
column.title = `${column.title}${c || ''}`;
columnNames[column.title] = true;
const rel = column.hm || column.bt;
const rel_column_id = (await models2?.[rel.tn]?.getColumns())?.find(
(c) => c.column_name === rel.cn
)?.id;
const tnId = models2?.[rel.tn]?.id;
const ref_rel_column_id = (
await models2?.[rel.rtn]?.getColumns()
)?.find((c) => c.column_name === rel.rcn)?.id;
const rtnId = models2?.[rel.rtn]?.id;
try {
await Column.insert<LinkToAnotherRecordColumn>({
project_id: project.id,
db_alias: base.id,
fk_model_id: models2[table.tn].id,
cn: column.cn,
title: column.title,
uidt: column.uidt,
type: column.hm ? 'hm' : column.mm ? 'mm' : 'bt',
// column_id,
fk_child_column_id: rel_column_id,
fk_parent_column_id: ref_rel_column_id,
fk_index_name: rel.cstn,
ur: rel.ur,
dr: rel.dr,
order: colOrder++,
fk_related_model_id: column.hm ? tnId : rtnId,
system: column.system,
});
// nested relations data apis
info.apiCount += 5;
} catch (e) {
console.log(e);
}
}
});
};
});
/* handle xc_tables update in parallel */
await NcHelp.executeOperations(tableMetasInsert, base.type);
await NcHelp.executeOperations(virtualColumnsInsert, base.type);
await extractAndGenerateManyToManyRelations(Object.values(models2));
let views: Array<{ order: number; table_name: string; title: string }> = (
await sqlClient.viewList()
)?.data?.list
// ?.filter(({ tn }) => !IGNORE_TABLES.includes(tn))
?.map((v) => {
v.order = ++order;
v.table_name = v.view_name;
v.title = getTableNameAlias(v.view_name, project.prefix, base);
return v;
});
/* filter based on prefix */
if (base.is_meta && project?.prefix) {
views = tables.filter((t) => {
return t?.tn?.startsWith(project?.prefix);
});
}
info.viewsCount = views.length;
const viewMetasInsert = views.map((table) => {
return async () => {
const columns = (await sqlClient.columnList({ tn: table.table_name }))
?.data?.list;
/* create nc_models and its rows if it doesn't exists */
models2[table.table_name] = await Model.insert(project.id, base.id, {
table_name: table.table_name,
title: getTableNameAlias(table.table_name, project.prefix, base),
// todo: sanitize
type: ModelTypes.VIEW,
order: table.order,
});
let colOrder = 1;
// view apis
info.apiCount += 2;
for (const column of columns) {
await Column.insert({
fk_model_id: models2[table.table_name].id,
...column,
title: getColumnNameAlias(column.cn, base),
order: colOrder++,
uidt: getColumnUiType(base, column),
});
}
};
});
await NcHelp.executeOperations(viewMetasInsert, base.type);
// fix pv column for created grid views
const models = await Model.list({ project_id: project.id, base_id: base.id });
for (const model of models) {
const views = await model.getViews();
for (const view of views) {
if (view.type === ViewTypes.GRID) {
await View.fixPVColumnForView(view.id);
}
}
}
const t1 = process.hrtime(t);
const t2 = t1[0] + t1[1] / 1000000000;
(info as any).timeTaken = t2.toFixed(1);
return info;
}

5
packages/nocodb/src/lib/meta/api/metaDiffApis.ts

@ -107,6 +107,7 @@ type MetaDiffChange = {
cn?: string;
rcn?: string;
relationType: RelationTypes;
cstn?: string;
}
);
@ -146,6 +147,7 @@ async function getMetaDiff(
cn: string;
rcn: string;
found?: any;
cstn?: string;
}> = (await sqlClient.relationListAll())?.data?.list;
for (const table of tableList) {
@ -394,6 +396,7 @@ async function getMetaDiff(
rcn: relation.rcn,
msg: `New relation added`,
relationType: RelationTypes.BELONGS_TO,
cstn: relation.cstn,
});
}
if (!relation?.found?.[RelationTypes.HAS_MANY]) {
@ -736,6 +739,7 @@ export async function metaDiffSync(req, res) {
fk_parent_column_id: parentCol.id,
fk_child_column_id: childCol.id,
virtual: false,
fk_index_name: change.cstn,
});
} else if (change.relationType === RelationTypes.HAS_MANY) {
const title = getUniqueColumnAliasName(
@ -751,6 +755,7 @@ export async function metaDiffSync(req, res) {
fk_parent_column_id: parentCol.id,
fk_child_column_id: childCol.id,
virtual: false,
fk_index_name: change.cstn,
});
}
});

263
packages/nocodb/src/lib/meta/api/projectApis.ts

@ -1,33 +1,24 @@
import { Request, Response } from 'express';
import { OrgUserRoles, ProjectType } from 'nocodb-sdk';
import Project from '../../models/Project';
import { ModelTypes, ProjectListType, UITypes } from 'nocodb-sdk';
import { ProjectListType } from 'nocodb-sdk';
import DOMPurify from 'isomorphic-dompurify';
import { packageVersion } from '../../utils/packageVersion';
import { Tele } from 'nc-help';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import syncMigration from '../helpers/syncMigration';
import { IGNORE_TABLES } from '../../utils/common/BaseApiBuilder';
import Column from '../../models/Column';
import Model from '../../models/Model';
import NcHelp from '../../utils/NcHelp';
import Base from '../../models/Base';
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName';
import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import ncMetaAclMw from '../helpers/ncMetaAclMw';
import ProjectUser from '../../models/ProjectUser';
import { customAlphabet } from 'nanoid';
import Noco from '../../Noco';
import isDocker from 'is-docker';
import { NcError } from '../helpers/catchError';
import getColumnUiType from '../helpers/getColumnUiType';
import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue';
import { extractAndGenerateManyToManyRelations } from './metaDiffApis';
import { metaApiMetrics } from '../helpers/apiMetrics';
import { extractPropsAndSanitize } from '../helpers/extractProps';
import NcConfigFactory from '../../utils/NcConfigFactory';
import { promisify } from 'util';
import { populateMeta } from './helpers';
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 4);
@ -202,256 +193,6 @@ async function projectCreate(req: Request<any, any>, res) {
res.json(project);
}
async function populateMeta(base: Base, project: Project): Promise<any> {
const info = {
type: 'rest',
apiCount: 0,
tablesCount: 0,
relationsCount: 0,
viewsCount: 0,
client: base?.getConnectionConfig()?.client,
timeTaken: 0,
};
const t = process.hrtime();
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
let order = 1;
const models2: { [tableName: string]: Model } = {};
const virtualColumnsInsert = [];
/* Get all relations */
const relations = (await sqlClient.relationListAll())?.data?.list;
info.relationsCount = relations.length;
let tables = (await sqlClient.tableList())?.data?.list
?.filter(({ tn }) => !IGNORE_TABLES.includes(tn))
?.map((t) => {
t.order = ++order;
t.title = getTableNameAlias(t.tn, project.prefix, base);
t.table_name = t.tn;
return t;
});
/* filter based on prefix */
if (project?.prefix) {
tables = tables.filter((t) => {
return t?.tn?.startsWith(project?.prefix);
});
}
info.tablesCount = tables.length;
tables.forEach((t) => {
t.title = getTableNameAlias(t.tn, project.prefix, base);
});
relations.forEach((r) => {
r.title = getTableNameAlias(r.tn, project.prefix, base);
r.rtitle = getTableNameAlias(r.rtn, project.prefix, base);
});
// await this.syncRelations();
const tableMetasInsert = tables.map((table) => {
return async () => {
/* filter relation where this table is present */
const tableRelations = relations.filter(
(r) => r.tn === table.tn || r.rtn === table.tn
);
const columns: Array<
Omit<Column, 'column_name' | 'title'> & {
cn: string;
system?: boolean;
}
> = (await sqlClient.columnList({ tn: table.tn }))?.data?.list;
const hasMany =
table.type === 'view'
? []
: tableRelations.filter((r) => r.rtn === table.tn);
const belongsTo =
table.type === 'view'
? []
: tableRelations.filter((r) => r.tn === table.tn);
mapDefaultPrimaryValue(columns);
// add vitual columns
const virtualColumns = [
...hasMany.map((hm) => {
return {
uidt: UITypes.LinkToAnotherRecord,
type: 'hm',
hm,
title: `${hm.title} List`,
};
}),
...belongsTo.map((bt) => {
// find and mark foreign key column
const fkColumn = columns.find((c) => c.cn === bt.cn);
if (fkColumn) {
fkColumn.uidt = UITypes.ForeignKey;
fkColumn.system = true;
}
return {
uidt: UITypes.LinkToAnotherRecord,
type: 'bt',
bt,
title: `${bt.rtitle}`,
};
}),
];
// await Model.insert(project.id, base.id, meta);
/* create nc_models and its rows if it doesn't exists */
models2[table.table_name] = await Model.insert(project.id, base.id, {
table_name: table.tn || table.table_name,
title: table.title,
type: table.type || 'table',
order: table.order,
});
// table crud apis
info.apiCount += 5;
let colOrder = 1;
for (const column of columns) {
await Column.insert({
uidt: column.uidt || getColumnUiType(base, column),
fk_model_id: models2[table.tn].id,
...column,
title: getColumnNameAlias(column.cn, base),
column_name: column.cn,
order: colOrder++,
});
}
virtualColumnsInsert.push(async () => {
const columnNames = {};
for (const column of virtualColumns) {
// generate unique name if there is any duplicate column name
let c = 0;
while (`${column.title}${c || ''}` in columnNames) {
c++;
}
column.title = `${column.title}${c || ''}`;
columnNames[column.title] = true;
const rel = column.hm || column.bt;
const rel_column_id = (await models2?.[rel.tn]?.getColumns())?.find(
(c) => c.column_name === rel.cn
)?.id;
const tnId = models2?.[rel.tn]?.id;
const ref_rel_column_id = (
await models2?.[rel.rtn]?.getColumns()
)?.find((c) => c.column_name === rel.rcn)?.id;
const rtnId = models2?.[rel.rtn]?.id;
try {
await Column.insert<LinkToAnotherRecordColumn>({
project_id: project.id,
db_alias: base.id,
fk_model_id: models2[table.tn].id,
cn: column.cn,
title: column.title,
uidt: column.uidt,
type: column.hm ? 'hm' : column.mm ? 'mm' : 'bt',
// column_id,
fk_child_column_id: rel_column_id,
fk_parent_column_id: ref_rel_column_id,
fk_index_name: rel.fkn,
ur: rel.ur,
dr: rel.dr,
order: colOrder++,
fk_related_model_id: column.hm ? tnId : rtnId,
system: column.system,
});
// nested relations data apis
info.apiCount += 5;
} catch (e) {
console.log(e);
}
}
});
};
});
/* handle xc_tables update in parallel */
await NcHelp.executeOperations(tableMetasInsert, base.type);
await NcHelp.executeOperations(virtualColumnsInsert, base.type);
await extractAndGenerateManyToManyRelations(Object.values(models2));
let views: Array<{ order: number; table_name: string; title: string }> = (
await sqlClient.viewList()
)?.data?.list
// ?.filter(({ tn }) => !IGNORE_TABLES.includes(tn))
?.map((v) => {
v.order = ++order;
v.table_name = v.view_name;
v.title = getTableNameAlias(v.view_name, project.prefix, base);
return v;
});
/* filter based on prefix */
if (project?.prefix) {
views = tables.filter((t) => {
return t?.tn?.startsWith(project?.prefix);
});
}
info.viewsCount = views.length;
const viewMetasInsert = views.map((table) => {
return async () => {
const columns = (await sqlClient.columnList({ tn: table.table_name }))
?.data?.list;
/* create nc_models and its rows if it doesn't exists */
models2[table.table_name] = await Model.insert(project.id, base.id, {
table_name: table.table_name,
title: getTableNameAlias(table.table_name, project.prefix, base),
// todo: sanitize
type: ModelTypes.VIEW,
order: table.order,
});
let colOrder = 1;
// view apis
info.apiCount += 2;
for (const column of columns) {
await Column.insert({
fk_model_id: models2[table.table_name].id,
...column,
title: getColumnNameAlias(column.cn, base),
order: colOrder++,
uidt: getColumnUiType(base, column),
});
}
};
});
await NcHelp.executeOperations(viewMetasInsert, base.type);
const t1 = process.hrtime(t);
const t2 = t1[0] + t1[1] / 1000000000;
(info as any).timeTaken = t2.toFixed(1);
return info;
}
export async function projectInfoGet(_req, res) {
res.json({
Node: process.version,

12
packages/nocodb/src/lib/meta/api/userApi/userApis.ts

@ -534,7 +534,7 @@ const mapRoutes = (router) => {
'/user/password/change',
ncMetaAclMw(passwordChange, 'passwordChange')
);
router.post('/auth/token/refresh', ncMetaAclMw(refreshToken, 'refreshToken'));
router.post('/auth/token/refresh', catchError(refreshToken));
/* Google auth apis */
@ -573,10 +573,7 @@ const mapRoutes = (router) => {
'/api/v1/db/auth/password/change',
ncMetaAclMw(passwordChange, 'passwordChange')
);
router.post(
'/api/v1/db/auth/token/refresh',
ncMetaAclMw(refreshToken, 'refreshToken')
);
router.post('/api/v1/db/auth/token/refresh', catchError(refreshToken));
router.get(
'/api/v1/db/auth/password/reset/:tokenId',
catchError(renderPasswordReset)
@ -607,10 +604,7 @@ const mapRoutes = (router) => {
'/api/v1/auth/password/change',
ncMetaAclMw(passwordChange, 'passwordChange')
);
router.post(
'/api/v1/auth/token/refresh',
ncMetaAclMw(refreshToken, 'refreshToken')
);
router.post('/api/v1/auth/token/refresh', catchError(refreshToken));
// respond with password reset page
router.get('/auth/password/reset/:tokenId', catchError(renderPasswordReset));
};

2
packages/nocodb/src/lib/models/GridViewColumn.ts

@ -109,6 +109,8 @@ export default class GridViewColumn implements GridColumnType {
`${CacheScope.GRID_VIEW_COLUMN}:${id}`
);
await View.fixPVColumnForView(column.fk_view_id, ncMeta);
return this.get(id, ncMeta);
}

17
packages/nocodb/src/lib/models/Model.ts

@ -606,6 +606,23 @@ export default class Model implements TableType {
newPvCol.id
);
const grid_views_with_column = await ncMeta.metaList2(
null,
null,
MetaTable.GRID_VIEW_COLUMNS,
{
condition: {
fk_column_id: newPvCol.id,
},
}
);
if (grid_views_with_column.length) {
for (const gv of grid_views_with_column) {
await View.fixPVColumnForView(gv.fk_view_id, ncMeta);
}
}
return true;
}

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

@ -692,6 +692,35 @@ export default class View implements ViewType {
break;
}
const updateObj = extractProps(colData, ['order', 'show']);
// keep primary_value_column always visible and first in grid view
if (view.type === ViewTypes.GRID) {
const primary_value_column_meta = await ncMeta.metaGet2(
null,
null,
MetaTable.COLUMNS,
{
fk_model_id: view.fk_model_id,
pv: true,
}
);
const primary_value_column = await ncMeta.metaGet2(
null,
null,
MetaTable.GRID_VIEW_COLUMNS,
{
fk_view_id: view.id,
fk_column_id: primary_value_column_meta.id,
}
);
if (primary_value_column && primary_value_column.id === colId) {
updateObj.order = 1;
updateObj.show = true;
}
}
// get existing cache
const key = `${cacheScope}:${colId}`;
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
@ -954,7 +983,16 @@ export default class View implements ViewType {
// set meta
await ncMeta.metaUpdate(null, null, MetaTable.VIEWS, updateObj, viewId);
return this.get(viewId);
const view = await this.get(viewId);
if (view.type === ViewTypes.GRID) {
if ('show_system_fields' in updateObj) {
await View.fixPVColumnForView(viewId, ncMeta);
}
}
return view;
}
// @ts-ignore
@ -1165,6 +1203,24 @@ export default class View implements ViewType {
const view = await this.get(viewId);
const table = this.extractViewColumnsTableName(view);
const scope = this.extractViewColumnsTableNameScope(view);
if (view.type === ViewTypes.GRID) {
const primary_value_column = await ncMeta.metaGet2(
null,
null,
MetaTable.COLUMNS,
{
fk_model_id: view.fk_model_id,
pv: true,
}
);
// keep primary_value_column always visible
if (primary_value_column) {
ignoreColdIds.push(primary_value_column.id);
}
}
// get existing cache
const dataList = await NocoCache.getList(scope, [viewId]);
if (dataList?.length) {
@ -1222,4 +1278,105 @@ export default class View implements ViewType {
sharedViews = sharedViews.filter((v) => v.uuid !== null);
return sharedViews?.map((v) => new View(v));
}
static async fixPVColumnForView(viewId, ncMeta = Noco.ncMeta) {
// get a list of view columns sorted by order
const view_columns = await ncMeta.metaList2(
null,
null,
MetaTable.GRID_VIEW_COLUMNS,
{
condition: {
fk_view_id: viewId,
},
orderBy: {
order: 'asc',
},
}
);
const view_columns_meta = [];
// get column meta for each view column
for (const col of view_columns) {
const col_meta = await ncMeta.metaGet2(
null,
null,
MetaTable.COLUMNS,
col.fk_column_id
);
view_columns_meta.push(col_meta);
}
const primary_value_column_meta = view_columns_meta.find((col) => col.pv);
if (primary_value_column_meta) {
const primary_value_column = view_columns.find(
(col) => col.fk_column_id === primary_value_column_meta.id
);
const primary_value_column_index = view_columns.findIndex(
(col) => col.fk_column_id === primary_value_column_meta.id
);
const view_orders = view_columns.map((col) => col.order);
const view_min_order = Math.min(...view_orders);
// if primary_value_column is not visible, make it visible
if (!primary_value_column.show) {
await ncMeta.metaUpdate(
null,
null,
MetaTable.GRID_VIEW_COLUMNS,
{ show: true },
primary_value_column.id
);
await NocoCache.set(
`${CacheScope.GRID_VIEW_COLUMN}:${primary_value_column.id}`,
primary_value_column
);
}
if (
primary_value_column.order === view_min_order &&
view_orders.filter((o) => o === view_min_order).length === 1
) {
// if primary_value_column is in first order do nothing
return;
} else {
// if primary_value_column not in first order, move it to the start of array
if (primary_value_column_index !== 0) {
const temp_pv = view_columns.splice(primary_value_column_index, 1);
view_columns.unshift(...temp_pv);
}
// update order of all columns in view to match the order in array
for (let i = 0; i < view_columns.length; i++) {
await ncMeta.metaUpdate(
null,
null,
MetaTable.GRID_VIEW_COLUMNS,
{ order: i + 1 },
view_columns[i].id
);
await NocoCache.set(
`${CacheScope.GRID_VIEW_COLUMN}:${view_columns[i].id}`,
view_columns[i]
);
}
}
}
const views = await ncMeta.metaList2(
null,
null,
MetaTable.GRID_VIEW_COLUMNS,
{
condition: {
fk_view_id: viewId,
},
orderBy: {
order: 'asc',
},
}
);
await NocoCache.setList(CacheScope.GRID_VIEW_COLUMN, [viewId], views);
}
}

2
packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts

@ -11,6 +11,7 @@ import ncProjectRolesUpgrader from './ncProjectRolesUpgrader';
import ncFilterUpgrader from './ncFilterUpgrader';
import ncAttachmentUpgrader from './ncAttachmentUpgrader';
import ncAttachmentUpgrader_0104002 from './ncAttachmentUpgrader_0104002';
import ncStickyColumnUpgrader from './ncStickyColumnUpgrader';
const log = debug('nc:version-upgrader');
import boxen from 'boxen';
@ -41,6 +42,7 @@ export default class NcUpgrader {
{ name: '0100002', handler: ncFilterUpgrader },
{ name: '0101002', handler: ncAttachmentUpgrader },
{ name: '0104002', handler: ncAttachmentUpgrader_0104002 },
{ name: '0104003', handler: ncStickyColumnUpgrader },
];
if (!(await ctx.ncMeta.knexConnection?.schema?.hasTable?.('nc_store'))) {
return;

90
packages/nocodb/src/lib/version-upgrader/ncStickyColumnUpgrader.ts

@ -0,0 +1,90 @@
import { NcUpgraderCtx } from './NcUpgrader';
import { MetaTable } from '../utils/globals';
// before 0.104.3, primary value column can be in any position in table
// with this upgrade we introduced sticky primary column feature
// this upgrader will make primary value column first column in grid views
export default async function ({ ncMeta }: NcUpgraderCtx) {
const grid_columns = await ncMeta.metaList2(
null,
null,
MetaTable.GRID_VIEW_COLUMNS
);
const grid_views = [...new Set(grid_columns.map((col) => col.fk_view_id))];
for (const view_id of grid_views) {
// get a list of view columns sorted by order
const view_columns = await ncMeta.metaList2(
null,
null,
MetaTable.GRID_VIEW_COLUMNS,
{
condition: {
fk_view_id: view_id,
},
orderBy: {
order: 'asc',
},
}
);
const view_columns_meta = [];
// get column meta for each view column
for (const col of view_columns) {
const col_meta = await ncMeta.metaGet(null, null, MetaTable.COLUMNS, {
id: col.fk_column_id,
});
view_columns_meta.push(col_meta);
}
const primary_value_column_meta = view_columns_meta.find((col) => col.pv);
if (primary_value_column_meta) {
const primary_value_column = view_columns.find(
(col) => col.fk_column_id === primary_value_column_meta.id
);
const primary_value_column_index = view_columns.findIndex(
(col) => col.fk_column_id === primary_value_column_meta.id
);
const view_orders = view_columns.map((col) => col.order);
const view_min_order = Math.min(...view_orders);
// if primary_value_column is not visible, make it visible
if (!primary_value_column.show) {
await ncMeta.metaUpdate(
null,
null,
MetaTable.GRID_VIEW_COLUMNS,
{ show: true },
primary_value_column.id
);
}
if (
primary_value_column.order === view_min_order &&
view_orders.filter((o) => o === view_min_order).length === 1
) {
// if primary_value_column is in first order do nothing
continue;
} else {
// if primary_value_column not in first order, move it to the start of array
if (primary_value_column_index !== 0) {
const temp_pv = view_columns.splice(primary_value_column_index, 1);
view_columns.unshift(...temp_pv);
}
// update order of all columns in view to match the order in array
for (let i = 0; i < view_columns.length; i++) {
await ncMeta.metaUpdate(
null,
null,
MetaTable.GRID_VIEW_COLUMNS,
{ order: i + 1 },
view_columns[i].id
);
}
}
}
}
}

18
tests/playwright/pages/Dashboard/Grid/Column/index.ts

@ -44,6 +44,7 @@ export class ColumnPageObject extends BasePage {
timeFormat = '',
insertAfterColumnTitle,
insertBeforeColumnTitle,
isPrimaryValue = false,
}: {
title: string;
type?: string;
@ -60,9 +61,16 @@ export class ColumnPageObject extends BasePage {
timeFormat?: string;
insertBeforeColumnTitle?: string;
insertAfterColumnTitle?: string;
isPrimaryValue?: boolean;
}) {
if (insertBeforeColumnTitle) {
await this.grid.get().locator(`th[data-title="${insertBeforeColumnTitle}"] .nc-ui-dt-dropdown`).click();
if (isPrimaryValue) {
await expect(this.rootPage.locator('li[role="menuitem"]:has-text("Insert Before")')).toHaveCount(0);
return;
}
await this.rootPage.locator('li[role="menuitem"]:has-text("Insert Before"):visible').click();
} else if (insertAfterColumnTitle) {
await this.grid.get().locator(`th[data-title="${insertAfterColumnTitle}"] .nc-ui-dt-dropdown`).click();
@ -313,9 +321,14 @@ export class ColumnPageObject extends BasePage {
await this.grid.get().locator(`th[data-title="${expectedTitle}"]`).isVisible();
}
async hideColumn({ title }: { title: string }) {
async hideColumn({ title, isPrimaryValue = false }: { title: string; isPrimaryValue?: boolean }) {
await this.grid.get().locator(`th[data-title="${title}"] .nc-ui-dt-dropdown`).click();
if (isPrimaryValue) {
await expect(this.rootPage.locator('li[role="menuitem"]:has-text("Hide Field")')).toHaveCount(0);
return;
}
await this.waitForResponse({
uiAction: this.rootPage.locator('li[role="menuitem"]:has-text("Hide Field"):visible').click(),
requestUrlPathToMatch: 'api/v1/db/meta/views',
@ -386,5 +399,8 @@ export class ColumnPageObject extends BasePage {
)
.first()
.isVisible();
// close sort menu
await this.grid.toolbar.clickSort();
}
}

22
tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts

@ -48,15 +48,21 @@ export class AttachmentCellPageObject extends BasePage {
}
async verifyFileCount({ index, columnHeader, count }: { index: number; columnHeader: string; count: number }) {
const attachments = await this.get({ index, columnHeader }).locator(
'.nc-cell > .nc-attachment-cell > .flex > .nc-attachment'
);
// retry below logic for 5 times, with 1 second delay
let retryCount = 0;
while (retryCount < 5) {
const attachments = await this.get({ index, columnHeader }).locator('.nc-attachment');
console.log(await attachments.count());
if ((await attachments.count()) === count) {
break;
}
retryCount++;
await this.rootPage.waitForTimeout(1000);
console.log(await attachments.count());
expect(await attachments.count()).toBe(count);
// attachments should be of count 'count'
// await expect(await attachments.count()).toBe(count);
if (retryCount === 5) {
expect(await attachments.count()).toBe(count);
}
}
}
async expandModalClose() {

4
tests/playwright/pages/Dashboard/common/Toolbar/Fields.ts

@ -31,12 +31,12 @@ export class ToolbarFieldsPage extends BasePage {
await this.toolbar.clickFields();
}
async verify({ title, checked }: { title: string; checked: boolean }) {
async verify({ title, checked }: { title: string; checked?: boolean }) {
const checkbox = this.get().locator(`[data-testid="nc-fields-menu-${title}"]`).locator('input[type="checkbox"]');
if (checked) {
await expect(checkbox).toBeChecked();
} else {
} else if (checked === false) {
await expect(checkbox).not.toBeChecked();
}
}

2
tests/playwright/quickTests/commonTest.ts

@ -208,7 +208,7 @@ const quickVerify = async ({
// Verify Fields
await dashboard.grid.toolbar.clickFields();
await dashboard.grid.toolbar.fields.verify({ title: 'Name', checked: true });
await dashboard.grid.toolbar.fields.verify({ title: 'Name' });
await dashboard.grid.toolbar.fields.verify({ title: 'Notes', checked: true });
await dashboard.grid.toolbar.fields.verify({ title: 'Attachments', checked: false });
await dashboard.grid.toolbar.fields.verify({ title: 'Status', checked: true });

2
tests/playwright/tests/cellSelection.spec.ts

@ -83,7 +83,7 @@ test.describe('Verify cell selection', () => {
await dashboard.grid.toolbar.fields.toggleShowSystemFields();
await grid.selectRange({
start: { index: 2, columnHeader: 'City List' },
end: { index: 0, columnHeader: 'CountryId' },
end: { index: 0, columnHeader: 'Country' },
});
expect(await grid.selectedCount()).toBe(12);

1
tests/playwright/tests/columnAttachments.spec.ts

@ -138,7 +138,6 @@ test.describe('Attachment column', () => {
columnHeader: 'testAttach',
filePath: twoFileArray,
});
await dashboard.rootPage.waitForTimeout(2000);
await dashboard.grid.cell.attachment.verifyFileCount({
index: 1,
columnHeader: 'testAttach',

2
tests/playwright/tests/columnMenuOperations.spec.ts

@ -91,6 +91,7 @@ test.describe('Column menu operations', () => {
title: 'InsertBeforeColumn',
type: 'SingleLineText',
insertBeforeColumnTitle: 'Title',
isPrimaryValue: true,
});
await dashboard.grid.column.create({
@ -107,6 +108,7 @@ test.describe('Column menu operations', () => {
await dashboard.grid.column.hideColumn({
title: 'Title',
isPrimaryValue: true,
});
await dashboard.grid.column.hideColumn({

6
tests/playwright/tests/metaSync.spec.ts

@ -252,18 +252,18 @@ test.describe('Meta sync', () => {
await dashboard.treeView.openTable({ title: 'Table1' });
await dashboard.grid.toolbar.clickFields();
await dashboard.grid.toolbar.fields.click({ title: 'Col1' });
await dashboard.grid.toolbar.fields.click({ title: 'Col2' });
await dashboard.grid.toolbar.clickFields();
await dashboard.grid.toolbar.sort.add({
columnTitle: 'Col1',
columnTitle: 'Col2',
isAscending: false,
isLocallySaved: false,
});
await dashboard.grid.toolbar.clickFilter();
await dashboard.grid.toolbar.filter.add({
columnTitle: 'Col1',
columnTitle: 'Col2',
opType: '>=',
value: '5',
isLocallySaved: false,

Loading…
Cancel
Save