Browse Source

Merge branch 'develop' of https://github.com/DIWAKARKASHYAP/nocodb into develop

pull/5799/head
DIWAKAR 2 years ago
parent
commit
3048b92ab9
  1. 8
      packages/nc-gui/components/smartsheet/Gallery.vue
  2. 29
      packages/nc-gui/components/smartsheet/Grid.vue
  3. 2
      packages/nc-gui/components/smartsheet/header/Cell.vue
  4. 3
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  5. 5
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  6. 5
      packages/nc-gui/components/virtual-cell/HasMany.vue
  7. 3
      packages/nc-gui/components/virtual-cell/Lookup.vue
  8. 5
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  9. 3
      packages/nc-gui/composables/useApi/interceptors.ts
  10. 6
      packages/nc-gui/composables/useGridViewColumnWidth.ts
  11. 1
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  12. 5
      packages/nc-gui/composables/useMultiSelect/index.ts
  13. 1
      packages/nc-gui/context/index.ts
  14. 80
      packages/nc-gui/lang/tr.json
  15. 2
      packages/nc-gui/package-lock.json
  16. 9
      packages/nc-gui/pages/index/index/index.vue
  17. 2
      packages/nc-lib-gui/package.json
  18. 4
      packages/nocodb-sdk/package-lock.json
  19. 2
      packages/nocodb-sdk/package.json
  20. 74
      packages/nocodb-sdk/src/lib/Api.ts
  21. 10
      packages/nocodb/.eslintrc.js
  22. 20
      packages/nocodb/package-lock.json
  23. 4
      packages/nocodb/package.json
  24. 8
      packages/nocodb/src/Noco.ts
  25. 6
      packages/nocodb/src/cache/RedisCacheMgr.ts
  26. 2
      packages/nocodb/src/cache/RedisMockCacheMgr.ts
  27. 1
      packages/nocodb/src/controllers/api-tokens.controller.ts
  28. 1
      packages/nocodb/src/controllers/attachments.controller.ts
  29. 1
      packages/nocodb/src/controllers/audits.controller.ts
  30. 1
      packages/nocodb/src/controllers/bases.controller.ts
  31. 1
      packages/nocodb/src/controllers/bulk-data-alias.controller.ts
  32. 1
      packages/nocodb/src/controllers/columns.controller.ts
  33. 1
      packages/nocodb/src/controllers/data-alias-export.controller.ts
  34. 3
      packages/nocodb/src/controllers/data-alias-nested.controller.ts
  35. 1
      packages/nocodb/src/controllers/data-alias.controller.ts
  36. 2
      packages/nocodb/src/controllers/filters.controller.ts
  37. 1
      packages/nocodb/src/controllers/form-columns.controller.ts
  38. 1
      packages/nocodb/src/controllers/galleries.controller.ts
  39. 1
      packages/nocodb/src/controllers/grid-columns.controller.ts
  40. 1
      packages/nocodb/src/controllers/grids.controller.ts
  41. 1
      packages/nocodb/src/controllers/maps.controller.ts
  42. 1
      packages/nocodb/src/controllers/meta-diffs.controller.ts
  43. 1
      packages/nocodb/src/controllers/model-visibilities.controller.ts
  44. 1
      packages/nocodb/src/controllers/old-datas/old-datas.controller.ts
  45. 1
      packages/nocodb/src/controllers/org-users.controller.ts
  46. 10
      packages/nocodb/src/controllers/plugins.controller.ts
  47. 1
      packages/nocodb/src/controllers/project-users.controller.ts
  48. 77
      packages/nocodb/src/controllers/projects.controller.ts
  49. 1
      packages/nocodb/src/controllers/shared-bases.controller.ts
  50. 1
      packages/nocodb/src/controllers/sorts.controller.ts
  51. 1
      packages/nocodb/src/controllers/tables.controller.ts
  52. 117
      packages/nocodb/src/controllers/users/users.controller.ts
  53. 3
      packages/nocodb/src/controllers/view-columns.controller.ts
  54. 6
      packages/nocodb/src/db/BaseModelSql.ts
  55. 19
      packages/nocodb/src/db/BaseModelSqlv2.ts
  56. 2
      packages/nocodb/src/db/CustomKnex.ts
  57. 2
      packages/nocodb/src/db/conditionV2.ts
  58. 2
      packages/nocodb/src/db/sql-client/lib/SqlClientFactory.ts
  59. 1
      packages/nocodb/src/interceptors/is-upload-allowed/is-upload-allowed.interceptor.ts
  60. 9
      packages/nocodb/src/middlewares/extract-project-id/extract-project-id.middleware.ts
  61. 2
      packages/nocodb/src/models/Model.ts
  62. 20
      packages/nocodb/src/models/ProjectUser.ts
  63. 1
      packages/nocodb/src/modules/datas/helpers.ts
  64. 3
      packages/nocodb/src/modules/jobs/jobs/at-import/helpers/readAndProcessData.ts
  65. 5
      packages/nocodb/src/modules/users/users.module.ts
  66. 1
      packages/nocodb/src/run/docker.ts
  67. 1
      packages/nocodb/src/run/dockerRunPG_CyQuick.ts
  68. 150
      packages/nocodb/src/schema/swagger.json
  69. 5
      packages/nocodb/src/services/bulk-data-alias.service.ts
  70. 90
      packages/nocodb/src/services/public-metas.service.ts
  71. 4
      packages/nocodb/src/services/users/users.service.ts
  72. 9
      packages/nocodb/src/utils/nc-config/NcConfig.ts
  73. 1
      packages/nocodb/src/version-upgrader/v1-legacy/gql/GqlApiBuilder.ts
  74. 5
      packages/nocodb/src/version-upgrader/v1-legacy/rest/RestApiBuilder.ts
  75. 79
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
  76. 49
      tests/playwright/tests/db/filters.spec.ts
  77. 34
      tests/playwright/tests/db/timezone.spec.ts

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

@ -7,6 +7,7 @@ import {
IsFormInj,
IsGalleryInj,
IsGridInj,
IsPublicInj,
MetaInj,
NavigateDir,
OpenNewRecordFormHookInj,
@ -62,6 +63,8 @@ provide(IsGridInj, ref(false))
provide(PaginationDataInj, paginationData)
provide(ChangePageInj, changePage)
const isPublic = inject(IsPublicInj, ref(false))
const fields = inject(FieldsInj, ref([]))
const route = useRoute()
@ -125,6 +128,10 @@ const attachments = (record: any): Attachment[] => {
}
const expandForm = (row: RowType, state?: Record<string, any>) => {
if (isPublic.value) {
return
}
const rowId = extractPkFromRow(row.row, meta.value!.columns!)
if (rowId) {
@ -234,6 +241,7 @@ watch(view, async (nextView) => {
:data-testid="`nc-gallery-card-${record.row.id}`"
@click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, { row: rowIndex })"
:style="isPublic ? { cursor: 'default' } : { cursor: 'pointer' }"
>
<template v-if="galleryData?.fk_cover_image_col_id" #cover>
<a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows>

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

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, GridType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import {
@ -141,11 +142,20 @@ const getContainerScrollForElement = (
) => {
const childPos = el.getBoundingClientRect()
const parentPos = container.getBoundingClientRect()
// provide an extra offset to show the prev/next/up/bottom cell
const extraOffset = 15
const numColWidth = container.querySelector('thead th:nth-child(1)')?.getBoundingClientRect().width ?? 0
const primaryColWidth = container.querySelector('thead th:nth-child(2)')?.getBoundingClientRect().width ?? 0
const stickyColsWidth = numColWidth + primaryColWidth
const relativePos = {
top: childPos.top - parentPos.top,
right: childPos.right - parentPos.right,
bottom: childPos.bottom - parentPos.bottom,
left: childPos.left - parentPos.left,
left: childPos.left - parentPos.left - stickyColsWidth,
}
const scroll = {
@ -159,9 +169,9 @@ const getContainerScrollForElement = (
*/
scroll.left =
relativePos.right + (offset?.right || 0) > 0
? container.scrollLeft + relativePos.right + (offset?.right || 0)
? container.scrollLeft + relativePos.right + (offset?.right || 0) + extraOffset
: relativePos.left - (offset?.left || 0) < 0
? container.scrollLeft + relativePos.left - (offset?.left || 0)
? container.scrollLeft + relativePos.left - (offset?.left || 0) - extraOffset
: container.scrollLeft
/*
@ -170,9 +180,9 @@ const getContainerScrollForElement = (
*/
scroll.top =
relativePos.bottom + (offset?.bottom || 0) > 0
? container.scrollTop + relativePos.bottom + (offset?.bottom || 0)
? container.scrollTop + relativePos.bottom + (offset?.bottom || 0) + extraOffset
: relativePos.top - (offset?.top || 0) < 0
? container.scrollTop + relativePos.top - (offset?.top || 0)
? container.scrollTop + relativePos.top - (offset?.top || 0) - extraOffset
: container.scrollTop
return scroll
@ -342,6 +352,12 @@ function scrollToCell(row?: number | null, col?: number | null) {
const { height: headerHeight } = tableHead.value!.getBoundingClientRect()
const tdScroll = getContainerScrollForElement(td, gridWrapper.value, { top: headerHeight, bottom: 9, right: 9 })
// if first column set left to 0 since it's sticky it will be visible and calculated value will be wrong
// setting left to 0 will make it scroll to the left
if (col === 0) {
tdScroll.left = 0
}
if (rows && row === rows.length - 2) {
// if last row make 'Add New Row' visible
gridWrapper.value.scrollTo({
@ -631,6 +647,9 @@ const onNavigate = (dir: NavigateDir) => {
}
break
}
nextTick(() => {
scrollToCell()
})
}
const showContextMenu = (e: MouseEvent, target?: { row: number; col: number }) => {

2
packages/nc-gui/components/smartsheet/header/Cell.vue

@ -52,7 +52,7 @@ const openHeaderMenu = () => {
<span
v-if="column"
class="name"
:class="{ 'cursor-pointer': !isForm && isUIAllowed('edit-column') }"
:class="{ 'cursor-pointer': !isForm && isUIAllowed('edit-column') && !hideMenu }"
style="white-space: nowrap"
:title="column.title"
@dblclick="openHeaderMenu"

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

@ -225,7 +225,8 @@ defineExpose({
/>
<span v-else :key="`${i}dummy`" />
<div :key="`${i}nested`" class="flex">
<span v-if="!i" class="flex items-center">{{ $t('labels.where') }}</span>
<div v-else :key="`${i}nested`" class="flex bob">
<a-select
v-model:value="filter.logical_op"
:dropdown-match-select-width="false"

5
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -7,6 +7,7 @@ import {
ColumnInj,
IsFormInj,
IsLockedInj,
IsUnderLookupInj,
ReadonlyInj,
ReloadRowDataHookInj,
RowInj,
@ -36,6 +37,8 @@ const isForm = inject(IsFormInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false))
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const { isUIAllowed } = useUIPermission()
const listItemsDlg = ref(false)
@ -89,7 +92,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
</div>
<div
v-if="!readOnly && !isLocked && (isUIAllowed('xcDatatableEditable') || isForm)"
v-if="!readOnly && !isLocked && (isUIAllowed('xcDatatableEditable') || isForm) && !isUnderLookup"
class="flex justify-end gap-1 min-h-[30px] items-center"
>
<GeneralIcon

5
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -16,6 +16,7 @@ import {
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
useUIPermission,
IsUnderLookupInj
} from '#imports'
const column = inject(ColumnInj)!
@ -32,6 +33,8 @@ const readOnly = inject(ReadonlyInj, ref(false))
const isLocked = inject(IsLockedInj)
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const listItemsDlg = ref(false)
const childListDlg = ref(false)
@ -112,7 +115,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</template>
</div>
<div v-if="!isLocked" class="flex justify-end gap-1 min-h-[30px] items-center">
<div v-if="!isLocked && !isUnderLookup" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon
icon="expand"
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"

3
packages/nc-gui/components/virtual-cell/Lookup.vue

@ -5,6 +5,7 @@ import {
CellUrlDisableOverlayInj,
CellValueInj,
ColumnInj,
IsUnderLookupInj,
MetaInj,
computed,
inject,
@ -70,6 +71,8 @@ const arrValue = computed(() => {
provide(MetaInj, lookupTableMeta)
provide(IsUnderLookupInj, ref(true))
provide(CellUrlDisableOverlayInj, ref(true))
const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activateShowEditNonEditableFieldWarning } =

5
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -7,6 +7,7 @@ import {
ColumnInj,
IsFormInj,
IsLockedInj,
IsUnderLookupInj,
ReadonlyInj,
ReloadRowDataHookInj,
RowInj,
@ -34,6 +35,8 @@ const readOnly = inject(ReadonlyInj, ref(false))
const isLocked = inject(IsLockedInj)
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const listItemsDlg = ref(false)
const childListDlg = ref(false)
@ -114,7 +117,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</template>
</div>
<div v-if="!isLocked" class="flex justify-end gap-1 min-h-[30px] items-center">
<div v-if="!isLocked && !isUnderLookup" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon
icon="expand"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"

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

@ -67,9 +67,8 @@ export function addAxiosInterceptors(api: Api<any>) {
})
.catch(async (error) => {
await state.signOut()
// todo: handle new user
navigateTo('/signIn')
if (!route.meta.public) navigateTo('/signIn')
return Promise.reject(error)
})

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

@ -13,7 +13,7 @@ import {
watch,
} from '#imports'
export function useGridViewColumnWidth(view: Ref<ViewType | undefined>) {
export function useGridViewColumnWidth(view: Ref<(ViewType & { columns?: GridColumnType[] }) | undefined>) {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
const { isUIAllowed } = useUIPermission()
@ -52,7 +52,9 @@ export function useGridViewColumnWidth(view: Ref<ViewType | undefined>) {
const loadGridViewColumns = async () => {
if (!view.value?.id && !isPublic.value) return
const colsData: GridColumnType[] = (isPublic.value ? columns.value : await $api.dbView.gridColumnsList(view.value!.id!)) ?? []
const colsData: GridColumnType[] =
(isPublic.value ? view.value?.columns : await $api.dbView.gridColumnsList(view.value!.id!)) ?? []
gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>(
(o, col) => ({
...o,

1
packages/nc-gui/composables/useMultiSelect/convertCellData.ts

@ -7,7 +7,6 @@ import { parseProp } from '#imports'
export default function convertCellData(
args: { from: UITypes; to: UITypes; value: any; column: ColumnType; appInfo: AppInfo },
isMysql = false,
isXcdbBase = false,
) {
const { from, to, value } = args
if (from === to && ![UITypes.Attachment, UITypes.Date, UITypes.DateTime, UITypes.Time, UITypes.Year].includes(to)) {

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

@ -55,7 +55,7 @@ export function useMultiSelect(
const { appInfo } = useGlobal()
const { isMysql, isXcdbBase } = useProject()
const { isMysql } = useProject()
let clipboardContext = $ref<{ value: any; uidt: UITypes } | null>(null)
@ -208,6 +208,7 @@ export function useMultiSelect(
selectedRange.startRange({ row, col })
selectedRange.endRange({ row, col })
makeActive(row, col)
scrollToActiveCell?.()
isMouseDown = false
}
@ -359,7 +360,6 @@ export function useMultiSelect(
appInfo: unref(appInfo),
},
isMysql(meta.value?.base_id),
isXcdbBase(meta.value?.base_id),
)
e.preventDefault()
@ -394,7 +394,6 @@ export function useMultiSelect(
appInfo: unref(appInfo),
},
isMysql(meta.value?.base_id),
isXcdbBase(meta.value?.base_id),
)
e.preventDefault()
syncCellData?.(activeCell)

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

@ -38,3 +38,4 @@ export const ToggleDialogInj: InjectionKey<Function> = Symbol('toggle-dialog-inj
export const CellClickHookInj: InjectionKey<EventHook<MouseEvent> | undefined> = Symbol('cell-click-injection')
export const SaveRowInj: InjectionKey<(() => void) | undefined> = Symbol('save-row-injection')
export const CurrentCellInj: InjectionKey<Ref<Element | undefined>> = Symbol('current-cell-injection')
export const IsUnderLookupInj: InjectionKey<Ref<boolean>> = Symbol('is-under-lookup-injection')

80
packages/nc-gui/lang/tr.json

@ -39,7 +39,7 @@
"signIn": "KAYIT OL",
"signOut": "Oturumu Kapat",
"required": "Gerekli",
"enableScanner": "Enable Scanner for filling",
"enableScanner": "Doldurmak için tarayıcıyı etkinleştir",
"preferred": "Tercihen",
"mandatory": "Zorunlu",
"loading": "Yükleniyor ...",
@ -76,7 +76,7 @@
"hideField": "Alanı Gizle",
"sortAsc": "Artan Sırala",
"sortDesc": "Azalan Sıralama",
"geoDataField": "GeoData Field"
"geoDataField": "GeoData Alanı"
},
"objects": {
"project": "Proje",
@ -101,7 +101,7 @@
"form": "Form",
"kanban": "Kanban",
"calendar": "Takvim",
"map": "Map"
"map": "Harita"
},
"user": "Kullanıcı",
"users": "Kullanıcılar",
@ -210,8 +210,8 @@
"advancedSettings": "Gelişmiş Ayarlar",
"codeSnippet": "Kod Parçacığı",
"keyboardShortcut": "Klavye Kısayolları",
"generateRandomName": "Generate Random Name",
"findRowByScanningCode": "Find row by scanning a QR or Barcode"
"generateRandomName": "Rastgele Ad Oluştur",
"findRowByScanningCode": "QR veya Barkod okutarak satır bulun"
},
"labels": {
"createdBy": "Tarafından Oluşturuldu",
@ -221,7 +221,7 @@
"viewName": "Görünüm adı",
"viewLink": "Görünüm Linki",
"columnName": "Sütun Adı",
"columnToScanFor": "Column to scan",
"columnToScanFor": "Taranacak sütun",
"columnType": "Sütun Tipi",
"roleName": "Rol Adı",
"roleDescription": "Rol Tanımı",
@ -238,7 +238,7 @@
"action": "Aksiyon",
"actions": "Aksiyonlar",
"operation": "İşlem",
"operationSub": "Sub Operation",
"operationSub": "Alt İşlem",
"operationType": "İşlem türü",
"operationSubType": "İşlem alt-türü",
"description": "Tanım",
@ -260,9 +260,9 @@
"barcodeFormat": "Barkod formatı",
"qrCodeValueTooLong": "QR kodu için çok fazla karakter",
"barcodeValueTooLong": "Barkod için çok fazla karakter",
"currentLocation": "Current Location",
"lng": "Lng",
"lat": "Lat",
"currentLocation": "Geçerli Konum",
"lng": "Boylam",
"lat": "Enlem",
"aggregateFunction": "Birleştirme fonksiyonu",
"dbCreateIfNotExists": "Veritabanı : yoksa oluştur",
"clientKey": "İstemci Anahtarı",
@ -385,18 +385,18 @@
"nextRecord": "Sonraki kayıt",
"previousRecord": "Önceki kayıt",
"copyApiURL": "API linkini kopyala",
"createTable": "Create New Table",
"createTable": "Yeni tablo oluştur",
"refreshTable": "Tabloları Yenile",
"renameTable": "Rename Table",
"deleteTable": "Delete Table",
"renameTable": "Tabloyu Yeniden Adlandır",
"deleteTable": "Tabloyu Sil",
"addField": "Tabloya yeni alan ekle",
"setDisplay": "Set as Display value",
"setDisplay": "Görünüm değeri olarak ayarla",
"addRow": "Yeni satır ekle",
"saveRow": "Satırı kaydet",
"saveAndExit": "Kaydet ve Çık",
"saveAndStay": "Kaydet ve Kal",
"insertRow": "Yeni Satır Ekle",
"duplicateRow": "Duplicate Row",
"duplicateRow": "Satırı Çoğalt",
"deleteRow": "Satırı Sil",
"deleteSelectedRow": "Seçilen Satırları Sil",
"importExcel": "Excel içe aktar",
@ -412,8 +412,8 @@
"changePwd": "Şifre değiştir",
"createView": "Bir görünüm oluştur",
"shareView": "Görünümü paylaş",
"findRowByCodeScan": "Find row by scan",
"fillByCodeScan": "Fill by scan",
"findRowByCodeScan": "Tarayarak satır bul",
"fillByCodeScan": "Tarayarak doldur",
"listSharedView": "Paylaşılan Görünümler",
"ListView": "Görünüm Listesi",
"copyView": "Görünümü kopyala",
@ -429,10 +429,10 @@
"openTab": "Yeni sekme aç",
"iFrame": "Gömülü HTML kodunu kopyalayın",
"addWebhook": "Yeni Webhook ekle",
"enableWebhook": "Enable Webhook",
"testWebhook": "Test Webhook",
"copyWebhook": "Copy Webhook",
"deleteWebhook": "Delete Webhook",
"enableWebhook": "Webhook'u Etkinleştir",
"testWebhook": "Webhook'u Test Et",
"copyWebhook": "Webhook'u Kopyala",
"deleteWebhook": "Webhook'u Sil",
"newToken": "Yeni Token ekle",
"exportZip": "Zip olarak dışa aktar",
"importZip": "Zip olarak içe aktar",
@ -472,10 +472,10 @@
"map": {
"mappedBy": "Mapped By",
"chooseMappingField": "Choose a Mapping Field",
"openInGoogleMaps": "Google Maps",
"openInGoogleMaps": "Google Haritalar",
"openInOpenStreetMap": "OSM"
},
"toggleMobileMode": "Toggle Mobile Mode"
"toggleMobileMode": "Mobil Modu Aç / Kapat"
},
"tooltip": {
"saveChanges": "Değişiklikleri Kaydet",
@ -542,15 +542,15 @@
"orgViewer": "İzleyicinin yeni proje oluşturmasına izin verilmez ancak davet edilen herhangi bir projeye erişebilir."
},
"codeScanner": {
"loadingScanner": "Loading the scanner...",
"selectColumn": "Select a column (QR code or Barcode) that you want to use for finding a row by scanning.",
"moreThanOneRowFoundForCode": "More than one row found for this code. Currently only unique codes are supported.",
"noRowFoundForCode": "No row found for this code for the selected column"
"loadingScanner": "Tarayıcı yükleniyor...",
"selectColumn": "Tarayarak satır bulmak için kullanmak istediğiniz sütunu (QR kodu veya Barkod) seçin.",
"moreThanOneRowFoundForCode": "Bu kod için birden fazla satır bulundu. Şu anda yalnızca benzersiz kodlar desteklenmektedir.",
"noRowFoundForCode": "Seçilen sütunda bu kod için satır bulunamadı"
},
"map": {
"overLimit": "You're over the limit.",
"closeLimit": "You're getting close to the limit.",
"limitNumber": "The limit of markers shown in a Map View is 1000 records."
"overLimit": "Sınırı aştınız.",
"closeLimit": "Sınıra yaklaşıyorsunuz.",
"limitNumber": "Bir Harita Görünümünde en fazla 1000 kayıt gösterilebilir."
},
"footerInfo": "Sayfa başına satır",
"upload": "Yüklenecek Dosyayı Seçin",
@ -634,7 +634,7 @@
"gallery": "Galeri görünümü ekle",
"form": "Form görünümü ekle",
"kanban": "Kanban görünümü ekle",
"map": "Add Map View",
"map": "Harita Görünümü Ekle",
"calendar": "Takvim görünümü ekle"
},
"tablesMetadataInSync": "Tablonun meta verileri senkronize",
@ -666,11 +666,11 @@
"deleteViewConfirmation": "Bu görünümü silmek istediğinizden emin misiniz?",
"deleteTableConfirmation": "Tabloyu silmek istiyor musunuz",
"showM2mTables": "M2M Tablolarını Göster",
"showM2mTablesDesc": "Many-to-many relation is supported via a junction table & is hidden by default. Enable this option to list all such tables along with existing tables.",
"showNullInCells": "Show NULL in Cells",
"showNullInCellsDesc": "Display 'NULL' tag in cells holding NULL value. This helps differentiate against cells holding EMPTY string.",
"showNullAndEmptyInFilter": "Show NULL and EMPTY in Filter",
"showNullAndEmptyInFilterDesc": "Enable 'additional' filters to differentiate fields containing NULL & Empty Strings. Default support for Blank treats both NULL & Empty strings alike.",
"showM2mTablesDesc": "Çoktan çoğa ilişki bir köprü tablosu aracılığıyla oluşturulur ve bu tablo varsayılan olarak gizlenir. Mevcut tablolarla birlikte bu köprü tablolarını da listelemek için bu seçeneği etkinleştirin.",
"showNullInCells": "Hücrelerde NULL Göster",
"showNullInCellsDesc": "NULL değerini taşıyan hücreler için 'NULL' etiketini göster. Bu BOŞ dizi değerleri ile ayırt etmenize yardımcı olur.",
"showNullAndEmptyInFilter": "Filtrelerde NULL ve EMPTY Göster",
"showNullAndEmptyInFilterDesc": "NULL ve Boş dizileri ayırt etmek için 'Ekstra' filtreleri etkinleştir. Varsayılan 'Blank' değeri NULL ve boş dizileri aynı şekilde ele alır.",
"deleteKanbanStackConfirmation": "Bu yığının silinmesi `{stackToBeDeleted}` seçim seçeneğini `{groupingField}` adresinden de kaldıracaktır. Kayıtlar kategorize edilmemiş yığına taşınacaktır.",
"computedFieldEditWarning": "Hesaplanan alan: içerik salt okunurdur. Yeniden yapılandırmak için sütun düzenleme menüsünü kullanın",
"computedFieldDeleteWarning": "Hesaplanan alan: içerik salt okunurdur. İçerik temizlenemiyor.",
@ -698,7 +698,7 @@
"allowedSpecialCharList": "İzin verilen özel karakter listesi"
},
"invalidURL": "Geçersiz URL",
"invalidEmail": "Invalid Email",
"invalidEmail": "Geçersiz E-posta",
"internalError": "Bazı dahili hatalar oluştu",
"templateGeneratorNotFound": "Şablon Oluşturucu bulunamıyor!",
"fileUploadFailed": "Dosya yüklenemedi",
@ -726,7 +726,7 @@
"nameShouldStartWithAnAlphabetOr_": "İsim bir alfabe veya _ ile başlamalıdır",
"followingCharactersAreNotAllowed": "Aşağıdaki karakterlere izin verilmez",
"columnNameRequired": "Sütun adı gereklidir",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"columnNameExceedsCharacters": "Sütun adının uzunluğu maksimum {value} karakter sınırını aşıyor",
"projectNameExceeds50Characters": "Proje adı 50 karakteri aşıyor",
"projectNameCannotStartWithSpace": "Proje adı boşlukla başlayamaz",
"requiredField": "Zorunlu alan",
@ -759,7 +759,7 @@
},
"success": {
"columnDuplicated": "Sütun başarıyla çoğaltıldı",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"rowDuplicatedWithoutSavedYet": "Satır çoğaltıldı (kaydedilmedi)",
"updatedUIACL": "Tablolar için UI ACL başarıyla güncellendi",
"pluginUninstalled": "Eklenti başarıyla kaldırıldı",
"pluginSettingsSaved": "Eklenti ayarları başarıyla kaydedildi",
@ -779,7 +779,7 @@
"userDeletedFromProject": "Kullanıcı projeden başarıyla silindi",
"inviteEmailSent": "Davet E-postası başarıyla gönderildi",
"inviteURLCopied": "Panoya kopyalanan davet URL'si",
"commentCopied": "Comment copied to clipboard",
"commentCopied": "Yorum panoya kopyalandı",
"passwordResetURLCopied": "Panoya kopyalanan parola sıfırlama URL'si",
"shareableURLCopied": "Paylaşılabilir temel URL panoya kopyalandı!",
"embeddableHTMLCodeCopied": "Yerleştirilebilir HTML kodu kopyalandı!",

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

@ -110,7 +110,7 @@
}
},
"../nocodb-sdk": {
"version": "0.108.0-beta.0",
"version": "0.108.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

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

@ -308,6 +308,7 @@ const copyProjectMeta = async () => {
<div v-if="record.status !== ProjectStatus.JOB" class="flex items-center gap-2">
<component
:is="iconMap.edit"
v-if="isUIAllowed('projectUpdate', true)"
v-e="['c:project:edit:rename']"
class="nc-action-btn"
@click.stop="navigateTo(`/${text}`)"
@ -315,12 +316,18 @@ const copyProjectMeta = async () => {
<component
:is="iconMap.delete"
v-if="isUIAllowed('projectDelete', true)"
class="nc-action-btn"
:data-testid="`delete-project-${record.title}`"
@click.stop="deleteProject(record)"
/>
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-import-menu" @click.stop>
<a-dropdown
v-if="isUIAllowed('duplicateProject', true)"
:trigger="['click']"
overlay-class-name="nc-dropdown-import-menu"
@click.stop
>
<GeneralIcon
icon="threeDotVertical"
class="nc-import-menu outline-0"

2
packages/nc-lib-gui/package.json

@ -1,6 +1,6 @@
{
"name": "nc-lib-gui",
"version": "0.108.0-beta.0",
"version": "0.108.1",
"description": "NocoDB GUI",
"author": {
"name": "NocoDB",

4
packages/nocodb-sdk/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb-sdk",
"version": "0.108.0-beta.0",
"version": "0.108.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb-sdk",
"version": "0.108.0-beta.0",
"version": "0.108.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

2
packages/nocodb-sdk/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb-sdk",
"version": "0.108.0-beta.0",
"version": "0.108.1",
"description": "NocoDB SDK",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",

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

@ -546,7 +546,39 @@ export interface FilterType {
| 'notchecked'
| 'notempty'
| 'notnull'
| 'null';
| 'null'
| null
| (
| 'allof'
| 'anyof'
| 'blank'
| 'btw'
| 'checked'
| 'empty'
| 'eq'
| 'ge'
| 'gt'
| 'gte'
| 'in'
| 'is'
| 'isWithin'
| 'isnot'
| 'le'
| 'like'
| 'lt'
| 'lte'
| 'nallof'
| 'nanyof'
| 'nbtw'
| 'neq'
| 'nlike'
| 'not'
| 'notblank'
| 'notchecked'
| 'notempty'
| 'notnull'
| ('null' & null)
);
/** Comparison Sub-Operator */
comparison_sub_op?:
| 'daysAgo'
@ -589,7 +621,7 @@ export interface FilterType {
| ('yesterday' & null)
);
/** Foreign Key to Column */
fk_column_id?: IdType;
fk_column_id?: StringOrNullType;
/** Foreign Key to Hook */
fk_hook_id?: StringOrNullType;
/** Foreign Key to Model */
@ -664,7 +696,39 @@ export interface FilterReqType {
| 'notchecked'
| 'notempty'
| 'notnull'
| 'null';
| 'null'
| null
| (
| 'allof'
| 'anyof'
| 'blank'
| 'btw'
| 'checked'
| 'empty'
| 'eq'
| 'ge'
| 'gt'
| 'gte'
| 'in'
| 'is'
| 'isWithin'
| 'isnot'
| 'le'
| 'like'
| 'lt'
| 'lte'
| 'nallof'
| 'nanyof'
| 'nbtw'
| 'neq'
| 'nlike'
| 'not'
| 'notblank'
| 'notchecked'
| 'notempty'
| 'notnull'
| ('null' & null)
);
/** Comparison Sub-Operator */
comparison_sub_op?:
| 'daysAgo'
@ -707,7 +771,7 @@ export interface FilterReqType {
| ('yesterday' & null)
);
/** Foreign Key to Column */
fk_column_id?: IdType;
fk_column_id?: StringOrNullType;
/** Belong to which filter ID */
fk_parent_id?: StringOrNullType;
/** Is this filter grouped? */
@ -2255,6 +2319,8 @@ export interface UserType {
* @example org-level-viewer
*/
roles?: string;
/** Access token version */
token_version?: string;
}
/**

10
packages/nocodb/.eslintrc.js

@ -63,12 +63,20 @@ module.exports = {
],
},
],
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/no-this-alias': 'off',
// todo: enable
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-var-requires': 'off',
'no-useless-catch': 'off',
'no-empty': 'off',

20
packages/nocodb/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb",
"version": "0.108.0-beta.0",
"version": "0.108.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb",
"version": "0.108.0-beta.0",
"version": "0.108.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@google-cloud/storage": "^5.7.2",
@ -80,7 +80,7 @@
"mysql2": "^3.2.0",
"nanoid": "^3.1.20",
"nc-help": "^0.2.87",
"nc-lib-gui": "0.108.0-beta.0",
"nc-lib-gui": "0.108.1",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
@ -190,7 +190,7 @@
}
},
"../nocodb-sdk": {
"version": "0.108.0-beta.0",
"version": "0.108.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
@ -13157,9 +13157,9 @@
}
},
"node_modules/nc-lib-gui": {
"version": "0.108.0-beta.0",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.108.0-beta.0.tgz",
"integrity": "sha512-y6WWH11Jj8Di8bK7bupLhPrZAHJ69ut5vV7i26fG0346J7A6SLrCaUcc/SGcJwJoeSwEwER3gzq2RXuWkGt1vA==",
"version": "0.108.1",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.108.1.tgz",
"integrity": "sha512-GYhDzkcqOCQ/pqWpxU8hI/4KcuuQrqkhG9zXmwUx1pQlz9qD70X8bQtjqb5rdvJw6diGdYneMl+wRI48bLiUAg==",
"dependencies": {
"express": "^4.17.1"
}
@ -28442,9 +28442,9 @@
}
},
"nc-lib-gui": {
"version": "0.108.0-beta.0",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.108.0-beta.0.tgz",
"integrity": "sha512-y6WWH11Jj8Di8bK7bupLhPrZAHJ69ut5vV7i26fG0346J7A6SLrCaUcc/SGcJwJoeSwEwER3gzq2RXuWkGt1vA==",
"version": "0.108.1",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.108.1.tgz",
"integrity": "sha512-GYhDzkcqOCQ/pqWpxU8hI/4KcuuQrqkhG9zXmwUx1pQlz9qD70X8bQtjqb5rdvJw6diGdYneMl+wRI48bLiUAg==",
"requires": {
"express": "^4.17.1"
}

4
packages/nocodb/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.108.0-beta.0",
"version": "0.108.1",
"description": "NocoDB Backend",
"main": "dist/bundle.js",
"author": {
@ -113,7 +113,7 @@
"mysql2": "^3.2.0",
"nanoid": "^3.1.20",
"nc-help": "^0.2.87",
"nc-lib-gui": "0.108.0-beta.0",
"nc-lib-gui": "0.108.1",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",

8
packages/nocodb/src/Noco.ts

@ -30,13 +30,7 @@ export default class Noco {
private static logger = new Logger(Noco.name);
public static get dashboardUrl(): string {
let siteUrl = `http://localhost:${process.env.PORT || 8080}`;
// if (Noco._this?.config?.envs?.[Noco._this?.env]?.publicUrl) {
// siteUrl = Noco._this?.config?.envs?.[Noco._this?.env]?.publicUrl;
// }
if (Noco._this?.config?.envs?.['_noco']?.publicUrl) {
siteUrl = Noco._this?.config?.envs?.['_noco']?.publicUrl;
}
const siteUrl = `http://localhost:${process.env.PORT || 8080}`;
return `${siteUrl}${Noco._this?.config?.dashboardPath}`;
}

6
packages/nocodb/src/cache/RedisCacheMgr.ts vendored

@ -45,7 +45,7 @@ export default class RedisCacheMgr extends CacheMgr {
}
// @ts-ignore
async get(key: string, type: string, config?: any): Promise<any> {
async get(key: string, type: string): Promise<any> {
log(`RedisCacheMgr::get: getting key ${key} with type ${type}`);
if (type === CacheGetType.TYPE_ARRAY) {
return this.client.smembers(key);
@ -135,7 +135,7 @@ export default class RedisCacheMgr extends CacheMgr {
// e.g. arr = ["nc:<orgs>:<scope>:<model_id_1>", "nc:<orgs>:<scope>:<model_id_2>"]
const arr = (await this.get(key, CacheGetType.TYPE_ARRAY)) || [];
log(`RedisCacheMgr::getList: getting list with key ${key}`);
const isNoneList = arr.length && arr[0] === 'NONE';
const isNoneList = arr.length && arr.includes('NONE');
if (isNoneList) {
return Promise.resolve({
@ -248,7 +248,7 @@ export default class RedisCacheMgr extends CacheMgr {
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
log(`RedisCacheMgr::appendToList: append key ${key} to ${listKey}`);
let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (list.length && list[0] === 'NONE') {
if (list.length && list.includes('NONE')) {
list = [];
await this.del(listKey);
}

2
packages/nocodb/src/cache/RedisMockCacheMgr.ts vendored

@ -40,7 +40,7 @@ export default class RedisMockCacheMgr extends CacheMgr {
}
// @ts-ignore
async get(key: string, type: string, config?: any): Promise<any> {
async get(key: string, type: string): Promise<any> {
log(`RedisMockCacheMgr::get: getting key ${key} with type ${type}`);
if (type === CacheGetType.TYPE_ARRAY) {
return this.client.smembers(key);

1
packages/nocodb/src/controllers/api-tokens.controller.ts

@ -9,7 +9,6 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {

1
packages/nocodb/src/controllers/attachments.controller.ts

@ -30,7 +30,6 @@ export class AttachmentsController {
@UploadedFiles() files: Array<any>,
@Body() body: any,
@Request() req: any,
@Query('path') path: string,
) {
const attachments = await this.attachmentsService.upload({
files: files,

1
packages/nocodb/src/controllers/audits.controller.ts

@ -10,7 +10,6 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {

1
packages/nocodb/src/controllers/bases.controller.ts

@ -10,7 +10,6 @@ import {
UseGuards,
} from '@nestjs/common';
import { BaseReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {

1
packages/nocodb/src/controllers/bulk-data-alias.controller.ts

@ -10,7 +10,6 @@ import {
Response,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/columns.controller.ts

@ -11,7 +11,6 @@ import {
UseGuards,
} from '@nestjs/common';
import { ColumnReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/data-alias-export.controller.ts

@ -1,6 +1,5 @@
import { Controller, Get, Request, Response, UseGuards } from '@nestjs/common';
import * as XLSX from 'xlsx';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

3
packages/nocodb/src/controllers/data-alias-nested.controller.ts

@ -8,7 +8,6 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,
@ -131,7 +130,6 @@ export class DataAliasNestedController {
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('refRowId') refRowId: string,
@Param('relationType') relationType: string,
) {
await this.dataAliasNestedService.relationDataRemove({
columnName: columnName,
@ -158,7 +156,6 @@ export class DataAliasNestedController {
@Param('projectName') projectName: string,
@Param('tableName') tableName: string,
@Param('refRowId') refRowId: string,
@Param('relationType') relationType: string,
) {
await this.dataAliasNestedService.relationDataAdd({
columnName: columnName,

1
packages/nocodb/src/controllers/data-alias.controller.ts

@ -11,7 +11,6 @@ import {
Response,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import { parseHrtimeToSeconds } from '../helpers';
import {

2
packages/nocodb/src/controllers/filters.controller.ts

@ -9,14 +9,12 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { FilterReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {
Acl,
ExtractProjectIdMiddleware,
UseAclMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import { FiltersService } from '../services/filters.service';

1
packages/nocodb/src/controllers/form-columns.controller.ts

@ -1,5 +1,4 @@
import { Body, Controller, Param, Patch, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/galleries.controller.ts

@ -9,7 +9,6 @@ import {
UseGuards,
} from '@nestjs/common';
import { GalleryUpdateReqType, ViewCreateReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/grid-columns.controller.ts

@ -1,6 +1,5 @@
import { Body, Controller, Get, Param, Patch, UseGuards } from '@nestjs/common';
import { GridColumnReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/grids.controller.ts

@ -8,7 +8,6 @@ import {
UseGuards,
} from '@nestjs/common';
import { ViewCreateReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/maps.controller.ts

@ -9,7 +9,6 @@ import {
UseGuards,
} from '@nestjs/common';
import { MapUpdateReqType, ViewCreateReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/meta-diffs.controller.ts

@ -6,7 +6,6 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/model-visibilities.controller.ts

@ -8,7 +8,6 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/old-datas/old-datas.controller.ts

@ -11,7 +11,6 @@ import {
Response,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/org-users.controller.ts

@ -10,7 +10,6 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { OrgUserRoles } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';

10
packages/nocodb/src/controllers/plugins.controller.ts

@ -14,11 +14,11 @@ import { Acl } from '../middlewares/extract-project-id/extract-project-id.middle
import { PluginsService } from '../services/plugins.service';
// todo: move to a interceptor
const blockInCloudMw = (_req, res, next) => {
if (process.env.NC_CLOUD === 'true') {
res.status(403).send('Not allowed');
} else next();
};
// const blockInCloudMw = (_req, res, next) => {
// if (process.env.NC_CLOUD === 'true') {
// res.status(403).send('Not allowed');
// } else next();
// };
@Controller()
@UseGuards(GlobalGuard)

1
packages/nocodb/src/controllers/project-users.controller.ts

@ -11,7 +11,6 @@ import {
UseGuards,
} from '@nestjs/common';
import { ProjectUserReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

77
packages/nocodb/src/controllers/projects.controller.ts

@ -11,15 +11,13 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import isDocker from 'is-docker';
import { ProjectReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {
Acl,
ExtractProjectIdMiddleware,
UseAclMiddleware,
UseProjectIdMiddleware,
} from '../middlewares/extract-project-id/extract-project-id.middleware';
import Noco from '../Noco';
import { packageVersion } from '../utils/packageVersion';
@ -31,9 +29,7 @@ import type { ProjectType } from 'nocodb-sdk';
export class ProjectsController {
constructor(private readonly projectsService: ProjectsService) {}
@UseAclMiddleware({
permissionName: 'projectList',
})
@Acl('projectList')
@Get('/api/v1/db/meta/projects/')
async list(@Query() queryParams: Record<string, any>, @Request() req) {
const projects = await this.projectsService.projectList({
@ -57,7 +53,7 @@ export class ProjectsController {
PackageVersion: packageVersion,
};
}
@Acl('projectGet')
@Get('/api/v1/db/meta/projects/:projectId')
async projectGet(@Param('projectId') projectId: string) {
const project = await this.projectsService.getProjectWithInfo({
@ -68,7 +64,7 @@ export class ProjectsController {
return project;
}
@Acl('projectUpdate')
@Patch('/api/v1/db/meta/projects/:projectId')
async projectUpdate(
@Param('projectId') projectId: string,
@ -82,6 +78,7 @@ export class ProjectsController {
return project;
}
@Acl('projectDelete')
@Delete('/api/v1/db/meta/projects/:projectId')
async projectDelete(@Param('projectId') projectId: string) {
const deleted = await this.projectsService.projectSoftDelete({
@ -91,6 +88,7 @@ export class ProjectsController {
return deleted;
}
@Acl('projectCreate')
@Post('/api/v1/db/meta/projects')
@HttpCode(200)
async projectCreate(@Body() projectBody: ProjectReqType, @Request() req) {
@ -102,66 +100,3 @@ export class ProjectsController {
return project;
}
}
/*
// // Project CRUD
export async function projectCost(req, res) {
let cost = 0;
const project = await Project.getWithInfo(req.params.projectId);
for (const base of project.bases) {
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
const userCount = await ProjectUser.getUsersCount(req.query);
const recordCount = (await sqlClient.totalRecords())?.data.TotalRecords;
if (recordCount > 100000) {
// 36,000 or $79/user/month
cost = Math.max(36000, 948 * userCount);
} else if (recordCount > 50000) {
// $36,000 or $50/user/month
cost = Math.max(36000, 600 * userCount);
} else if (recordCount > 10000) {
// $240/user/yr
cost = Math.min(240 * userCount, 36000);
} else if (recordCount > 1000) {
// $120/user/yr
cost = Math.min(120 * userCount, 36000);
}
}
T.event({
event: 'a:project:cost',
data: {
cost,
},
});
res.json({ cost });
}
export async function hasEmptyOrNullFilters(req, res) {
res.json(await Filter.hasEmptyOrNullFilters(req.params.projectId));
}
export default (router) => {
router.get(
'/api/v1/db/meta/projects/:projectId/cost',
metaApiMetrics,
ncMetaAclMw(projectCost, 'projectCost')
);
router.get(
'/api/v1/db/meta/projects/:projectId/has-empty-or-null-filters',
metaApiMetrics,
ncMetaAclMw(hasEmptyOrNullFilters, 'hasEmptyOrNullFilters')
);
};
* */

1
packages/nocodb/src/controllers/shared-bases.controller.ts

@ -10,7 +10,6 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GlobalGuard } from '../guards/global/global.guard';
import {
Acl,

1
packages/nocodb/src/controllers/sorts.controller.ts

@ -9,7 +9,6 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { SortReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';

1
packages/nocodb/src/controllers/tables.controller.ts

@ -11,7 +11,6 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { TableReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import extractRolesObj from '../utils/extractRolesObj';

117
packages/nocodb/src/controllers/users/users.controller.ts

@ -1,5 +1,3 @@
import { promisify } from 'util';
import { AuditOperationSubTypes, AuditOperationTypes } from 'nocodb-sdk';
import {
Body,
Controller,
@ -19,23 +17,17 @@ import {
Acl,
ExtractProjectIdMiddleware,
} from '../../middlewares/extract-project-id/extract-project-id.middleware';
import Noco from '../../Noco';
import { GoogleStrategy } from '../../strategies/google.strategy/google.strategy';
import extractRolesObj from '../../utils/extractRolesObj';
import { Audit, User } from '../../models';
import { User } from '../../models';
import {
genJwt,
randomTokenString,
setTokenCookie,
} from '../../services/users/helpers';
import { UsersService } from '../../services/users/users.service';
import extractRolesObj from '../../utils/extractRolesObj';
@Controller()
export class UsersController {
constructor(
private readonly usersService: UsersService,
private googleStrategy: GoogleStrategy,
) {}
constructor(private readonly usersService: UsersService) {}
@Post([
'/auth/user/signup',
@ -59,56 +51,14 @@ export class UsersController {
'/api/v1/auth/token/refresh',
])
@HttpCode(200)
async refreshToken(@Request() req: any, @Request() res: any): Promise<any> {
return await this.usersService.refreshToken({
body: req.body,
req,
res,
});
}
async successfulSignIn({ user, err, info, req, res, auditDescription }) {
try {
if (!user || !user.email) {
if (err) {
return res.status(400).send(err);
}
if (info) {
return res.status(400).send(info);
}
return res.status(400).send({ msg: 'Your signin has failed' });
}
await promisify((req as any).login.bind(req))(user);
const refreshToken = randomTokenString();
if (!user.token_version) {
user.token_version = randomTokenString();
}
await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version: user.token_version,
});
setTokenCookie(res, refreshToken);
await Audit.insert({
op_type: AuditOperationTypes.AUTHENTICATION,
op_sub_type: AuditOperationSubTypes.SIGNIN,
user: user.email,
ip: req.clientIp,
description: auditDescription,
});
res.json({
token: genJwt(user, Noco.getConfig()),
} as any);
} catch (e) {
console.log(e);
throw e;
}
async refreshToken(@Request() req: any, @Response() res: any): Promise<any> {
res.json(
await this.usersService.refreshToken({
body: req.body,
req,
res,
}),
);
}
@Post([
@ -118,8 +68,9 @@ export class UsersController {
])
@UseGuards(AuthGuard('local'))
@HttpCode(200)
async signin(@Request() req) {
return this.usersService.login(req.user);
async signin(@Request() req, @Response() res) {
await this.setRefreshToken({ req, res });
res.json(this.usersService.login(req.user));
}
@Post('/api/v1/auth/user/signout')
@ -136,18 +87,15 @@ export class UsersController {
@Post(`/auth/google/genTokenByCode`)
@HttpCode(200)
@UseGuards(AuthGuard('google'))
async googleSignin(@Request() req) {
return this.usersService.login(req.user);
async googleSignin(@Request() req, @Response() res) {
await this.setRefreshToken({ req, res });
res.json(this.usersService.login(req.user));
}
@Get('/auth/google')
@UseGuards(AuthGuard('google'))
googleAuthenticate(@Request() req) {
// this.googleStrategy.authenticate(req, {
// scope: ['profile', 'email'],
// state: req.query.state,
// callbackURL: req.ncSiteUrl + Noco.getConfig().dashboardPath,
// });
googleAuthenticate() {
// google strategy will take care the request
}
@Get(['/auth/user/me', '/api/v1/db/auth/user/me', '/api/v1/auth/user/me'])
@ -168,7 +116,7 @@ export class UsersController {
@UseGuards(GlobalGuard)
@Acl('passwordChange')
@HttpCode(200)
async passwordChange(@Request() req: any, @Body() body: any): Promise<any> {
async passwordChange(@Request() req: any): Promise<any> {
if (!(req as any).isAuthenticated()) {
NcError.forbidden('Not allowed');
}
@ -188,7 +136,7 @@ export class UsersController {
'/api/v1/auth/password/forgot',
])
@HttpCode(200)
async passwordForgot(@Request() req: any, @Body() body: any): Promise<any> {
async passwordForgot(@Request() req: any): Promise<any> {
await this.usersService.passwordForgot({
siteUrl: (req as any).ncSiteUrl,
body: req.body,
@ -269,4 +217,27 @@ export class UsersController {
return res.status(400).json({ msg: e.message });
}
}
async setRefreshToken({ res, req }) {
const userId = req.user?.id;
if (!userId) return;
const user = await User.get(userId);
if (!user) return;
const refreshToken = randomTokenString();
if (!user.token_version) {
user.token_version = randomTokenString();
}
await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version: user.token_version,
});
setTokenCookie(res, refreshToken);
}
}

3
packages/nocodb/src/controllers/view-columns.controller.ts

@ -8,8 +8,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { ColumnReqType, ViewColumnReqType } from 'nocodb-sdk';
import { AuthGuard } from '@nestjs/passport';
import { ViewColumnReqType } from 'nocodb-sdk';
import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import {

6
packages/nocodb/src/db/BaseModelSql.ts

@ -1613,7 +1613,7 @@ class BaseModelSql extends BaseModel {
}
}
async nestedRead(id, { where, fields: fields1, f, ...rest }, trx = null) {
async nestedRead(id, { fields: fields1, f, ...rest }, trx = null) {
rest = Object.assign({}, this.defaultNestedQueryParams, rest);
const { hm: childs = '', bt: parents = '', mm: many = '' } = rest;
@ -1852,7 +1852,7 @@ class BaseModelSql extends BaseModel {
return null;
}
// @ts-ignore
const { tn, cn, vtn, vcn, vrcn, rtn, rcn } =
const { vtn, vcn, vrcn, rtn, rcn } =
this.manyToManyRelations.find(({ vtn }) => assoc === vtn) || {};
const childModel = this.dbModels[rtn];
@ -1894,7 +1894,7 @@ class BaseModelSql extends BaseModel {
return null;
}
// @ts-ignore
const { tn, cn, vtn, vcn, vrcn, rtn, rcn } =
const { vtn, vcn, vrcn, rtn, rcn } =
this.manyToManyRelations.find(({ vtn }) => assoc === vtn) || {};
const childModel = this.dbModels[rtn];

19
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -19,16 +19,7 @@ import DOMPurify from 'isomorphic-dompurify';
import { v4 as uuidv4 } from 'uuid';
import { NcError } from '../helpers/catchError';
import getAst from '../helpers/getAst';
import {
Audit,
Base,
Column,
Filter,
Model,
Project,
Sort,
View,
} from '../models';
import { Audit, Column, Filter, Model, Project, Sort, View } from '../models';
import { sanitize, unsanitize } from '../helpers/sqlSanitize';
import {
COMPARISON_OPS,
@ -57,7 +48,7 @@ import type {
SelectOption,
} from '../models';
import type { Knex } from 'knex';
import type { BoolType, SortType } from 'nocodb-sdk';
import type { SortType } from 'nocodb-sdk';
dayjs.extend(utc);
dayjs.extend(timezone);
@ -1361,12 +1352,6 @@ class BaseModelSqlv2 {
const columns = await this.model.getColumns();
for (const column of columns) {
switch (column.uidt) {
case UITypes.Rollup:
{
// @ts-ignore
const colOptions: RollupColumn = await column.getColOptions();
}
break;
case UITypes.Lookup:
{
// @ts-ignore

2
packages/nocodb/src/db/CustomKnex.ts

@ -1,6 +1,6 @@
import { Knex, knex } from 'knex';
import { SnowflakeClient } from 'nc-help';
import pg, { types } from 'pg';
import { types } from 'pg';
import dayjs from 'dayjs';
import Filter from '../models/Filter';
import type { FilterType } from 'nocodb-sdk';

2
packages/nocodb/src/db/conditionV2.ts

@ -1,5 +1,5 @@
import { isNumericCol, RelationTypes, UITypes } from 'nocodb-sdk';
import dayjs, { extend } from 'dayjs';
import dayjs from 'dayjs';
// import customParseFormat from 'dayjs/plugin/customParseFormat.js';
import Filter from '../models/Filter';
import { sanitize } from '../helpers/sqlSanitize';

2
packages/nocodb/src/db/sql-client/lib/SqlClientFactory.ts

@ -1,7 +1,5 @@
import fs from 'fs';
import { promisify } from 'util';
import Noco from '../../../Noco';
import SqlClientFactoryEE from './ee/SqlClientFactoryEE';
import MySqlClient from './mysql/MysqlClient';
import MssqlClient from './mssql/MssqlClient';
import OracleClient from './oracle/OracleClient';

1
packages/nocodb/src/interceptors/is-upload-allowed/is-upload-allowed.interceptor.ts

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import { OrgUserRoles, ProjectRoles } from 'nocodb-sdk';
import { throwError } from 'rxjs';
import { NcError } from '../../helpers/catchError';
import Noco from '../../Noco';
import extractRolesObj from '../../utils/extractRolesObj';

9
packages/nocodb/src/middlewares/extract-project-id/extract-project-id.middleware.ts

@ -1,10 +1,7 @@
import { promisify } from 'util';
import { Injectable, SetMetadata, UseInterceptors } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { NextFunction, Request, Response } from 'express';
import { OrgUserRoles } from 'nocodb-sdk';
import passport from 'passport';
import { map, throwError } from 'rxjs';
import { map } from 'rxjs';
import {
Column,
Filter,
@ -19,8 +16,7 @@ import {
} from '../../models';
import extractRolesObj from '../../utils/extractRolesObj';
import projectAcl from '../../utils/projectAcl';
import catchError, { NcError } from '../catchError';
import extractProjectIdAndAuthenticate from '../extractProjectIdAndAuthenticate';
import { NcError } from '../catchError';
import type { Observable } from 'rxjs';
import type {
CallHandler,
@ -216,7 +212,6 @@ export class AclMiddleware implements NestInterceptor {
);
const req = context.switchToHttp().getRequest();
const res = context.switchToHttp().getResponse();
req.customProperty = 'This is a custom property';
const roles: Record<string, boolean> = extractRolesObj(req.user?.roles);

2
packages/nocodb/src/models/Model.ts

@ -16,7 +16,6 @@ import { sanitize } from '../helpers/sqlSanitize';
import { extractProps } from '../helpers/extractProps';
import Audit from './Audit';
import View from './View';
import Base from './Base';
import Column from './Column';
import type { BoolType, TableReqType, TableType } from 'nocodb-sdk';
import type { XKnex } from '../db/CustomKnex';
@ -471,7 +470,6 @@ export default class Model implements TableType {
knex,
) {
const insertObj = {};
const base = await Base.get(this.base_id);
for (const col of await this.getColumns()) {
if (isVirtualCol(col)) continue;
let val =

20
packages/nocodb/src/models/ProjectUser.ts

@ -174,11 +174,6 @@ export default class ProjectUser {
}
static async delete(projectId: string, userId: string, ncMeta = Noco.ncMeta) {
// await NocoCache.deepDel(
// CacheScope.PROJECT_USER,
// `${CacheScope.PROJECT_USER}:${projectId}:${userId}`,
// CacheDelDirection.CHILD_TO_PARENT
// );
const { email } = await ncMeta.metaGet2(null, null, MetaTable.USERS, {
id: userId,
});
@ -194,11 +189,16 @@ export default class ProjectUser {
const { isNoneList } = cachedList;
if (!isNoneList && cachedProjectList?.length) {
cachedProjectList = cachedProjectList.filter((p) => p.id !== projectId);
await NocoCache.setList(
CacheScope.USER_PROJECT,
[userId],
cachedProjectList,
);
// delete the whole list first so that the old one won't be included
await NocoCache.del(`${CacheScope.USER_PROJECT}:${userId}:list`);
if (cachedProjectList.length > 0) {
// set the updated list (i.e. excluding the to-be-deleted project id)
await NocoCache.setList(
CacheScope.USER_PROJECT,
[userId],
cachedProjectList,
);
}
}
await NocoCache.del(`${CacheScope.PROJECT_USER}:${projectId}:${userId}`);

1
packages/nocodb/src/modules/datas/helpers.ts

@ -12,7 +12,6 @@ import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import type { BaseModelSqlv2 } from '../../db/BaseModelSqlv2';
import type LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn';
import type LookupColumn from '../../models/LookupColumn';
import type { Request } from 'express';
export interface PathParams {
projectName: string;

3
packages/nocodb/src/modules/jobs/jobs/at-import/helpers/readAndProcessData.ts

@ -21,14 +21,12 @@ async function readAllData({
fields,
base,
logBasic = (_str) => {},
services,
}: {
table: { title?: string };
fields?;
base: AirtableBase;
logBasic?: (string) => void;
logDetailed?: (string) => void;
services: AirtableImportContext;
}): Promise<EntityMap> {
return new Promise((resolve, reject) => {
let data = null;
@ -229,7 +227,6 @@ export async function importLTARData({
base,
logDetailed,
logBasic,
services,
}));
const modelMeta: any =

5
packages/nocodb/src/modules/users/users.module.ts

@ -1,9 +1,6 @@
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import {
GoogleStrategy,
GoogleStrategyProvider,
} from '../../strategies/google.strategy/google.strategy';
import { GoogleStrategyProvider } from '../../strategies/google.strategy/google.strategy';
import { GlobalModule } from '../global/global.module';
import { UsersService } from '../../services/users/users.service';
import { UsersController } from '../../controllers/users/users.controller';

1
packages/nocodb/src/run/docker.ts

@ -1,4 +1,3 @@
import axios from 'axios';
import cors from 'cors';
import express from 'express';
import Noco from '../Noco';

1
packages/nocodb/src/run/dockerRunPG_CyQuick.ts

@ -1,7 +1,6 @@
import cors from 'cors';
import express from 'express';
import Noco from '../Noco';
import nocobuild from '../nocobuild';
const server = express();
server.enable('trust proxy');

150
packages/nocodb/src/schema/swagger.json

@ -15197,38 +15197,45 @@
},
"comparison_op": {
"description": "Comparison Operator",
"enum": [
"allof",
"anyof",
"blank",
"btw",
"checked",
"empty",
"eq",
"ge",
"gt",
"gte",
"in",
"is",
"isWithin",
"isnot",
"le",
"like",
"lt",
"lte",
"nallof",
"nanyof",
"nbtw",
"neq",
"nlike",
"not",
"notblank",
"notchecked",
"notempty",
"notnull",
"null"
],
"type": "string"
"anyOf": [
{
"enum": [
"allof",
"anyof",
"blank",
"btw",
"checked",
"empty",
"eq",
"ge",
"gt",
"gte",
"in",
"is",
"isWithin",
"isnot",
"le",
"like",
"lt",
"lte",
"nallof",
"nanyof",
"nbtw",
"neq",
"nlike",
"not",
"notblank",
"notchecked",
"notempty",
"notnull",
"null"
],
"type": "string"
},
{
"type": "null"
}
]
},
"comparison_sub_op": {
"anyOf": [
@ -15262,7 +15269,7 @@
"description": "Comparison Sub-Operator"
},
"fk_column_id": {
"$ref": "#/components/schemas/Id",
"$ref": "#/components/schemas/StringOrNull",
"description": "Foreign Key to Column"
},
"fk_hook_id": {
@ -15518,38 +15525,45 @@
"properties": {
"comparison_op": {
"description": "Comparison Operator",
"enum": [
"allof",
"anyof",
"blank",
"btw",
"checked",
"empty",
"eq",
"ge",
"gt",
"gte",
"in",
"is",
"isWithin",
"isnot",
"le",
"like",
"lt",
"lte",
"nallof",
"nanyof",
"nbtw",
"neq",
"nlike",
"not",
"notblank",
"notchecked",
"notempty",
"notnull",
"null"
],
"type": "string"
"anyOf": [
{
"enum": [
"allof",
"anyof",
"blank",
"btw",
"checked",
"empty",
"eq",
"ge",
"gt",
"gte",
"in",
"is",
"isWithin",
"isnot",
"le",
"like",
"lt",
"lte",
"nallof",
"nanyof",
"nbtw",
"neq",
"nlike",
"not",
"notblank",
"notchecked",
"notempty",
"notnull",
"null"
],
"type": "string"
},
{
"type": "null"
}
]
},
"comparison_sub_op": {
"anyOf": [
@ -15583,7 +15597,7 @@
"description": "Comparison Sub-Operator"
},
"fk_column_id": {
"$ref": "#/components/schemas/Id",
"$ref": "#/components/schemas/StringOrNull",
"description": "Foreign Key to Column"
},
"fk_parent_id": {
@ -20000,6 +20014,10 @@
"description": "The roles of the user",
"example": "org-level-viewer",
"type": "string"
},
"token_version": {
"description": "Access token version",
"type": "string"
}
},
"required": ["email", "email_verified", "firstname", "id", "lastname"],

5
packages/nocodb/src/services/bulk-data-alias.service.ts

@ -1,8 +1,5 @@
import { Injectable } from '@nestjs/common';
import { isSystemColumn, UITypes } from 'nocodb-sdk';
import * as XLSX from 'xlsx';
import { NcError } from '../helpers/catchError';
import { Base, Column, Model, Project, View } from '../models';
import { Base, Model } from '../models';
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2';
import { getViewAndModelByAliasOrId } from '../modules/datas/helpers';
import type { PathParams } from '../modules/datas/helpers';

90
packages/nocodb/src/services/public-metas.service.ts

@ -2,8 +2,7 @@ import { Injectable } from '@nestjs/common';
import { ErrorMessages, RelationTypes, UITypes } from 'nocodb-sdk';
import { NcError } from '../helpers/catchError';
import { Base, Column, Model, Project, View } from '../models';
import type { LinkToAnotherRecordColumn } from '../models';
import type { LinkToAnotherRecordType } from 'nocodb-sdk';
import type { LinkToAnotherRecordColumn, LookupColumn } from '../models';
@Injectable()
export class PublicMetasService {
@ -63,23 +62,88 @@ export class PublicMetasService {
// load related table metas
for (const col of view.model.columns) {
if (UITypes.LinkToAnotherRecord === col.uidt) {
const colOpt = await col.getColOptions<LinkToAnotherRecordType>();
relatedMetas[colOpt.fk_related_model_id] = await Model.getWithInfo({
id: colOpt.fk_related_model_id,
});
if (colOpt.type === 'mm') {
relatedMetas[colOpt.fk_mm_model_id] = await Model.getWithInfo({
id: colOpt.fk_mm_model_id,
});
}
}
await this.extractRelatedMetas({ col, relatedMetas });
}
view.relatedMetas = relatedMetas;
return view;
}
private async extractRelatedMetas({
col,
relatedMetas = {},
}: {
col: Column<any>;
relatedMetas: Record<string, Model>;
}) {
if (UITypes.LinkToAnotherRecord === col.uidt) {
await this.extractLTARRelatedMetas({
ltarColOption: await col.getColOptions<LinkToAnotherRecordColumn>(),
relatedMetas,
});
} else if (UITypes.Lookup === col.uidt) {
await this.extractLookupRelatedMetas({
lookupColOption: await col.getColOptions<LookupColumn>(),
relatedMetas,
});
}
}
private async extractLTARRelatedMetas({
ltarColOption,
relatedMetas = {},
}: {
ltarColOption: LinkToAnotherRecordColumn;
relatedMetas: { [key: string]: Model };
}) {
relatedMetas[ltarColOption.fk_related_model_id] = await Model.getWithInfo({
id: ltarColOption.fk_related_model_id,
});
if (ltarColOption.type === 'mm') {
relatedMetas[ltarColOption.fk_mm_model_id] = await Model.getWithInfo({
id: ltarColOption.fk_mm_model_id,
});
}
}
private async extractLookupRelatedMetas({
lookupColOption,
relatedMetas = {},
}: {
lookupColOption: LookupColumn;
relatedMetas: { [key: string]: Model };
}) {
const relationCol = await Column.get({
colId: lookupColOption.fk_relation_column_id,
});
const lookedUpCol = await Column.get({
colId: lookupColOption.fk_lookup_column_id,
});
// extract meta for table which belongs the relation column
// if not already extracted
if (!relatedMetas[relationCol.fk_model_id]) {
relatedMetas[relationCol.fk_model_id] = await Model.getWithInfo({
id: relationCol.fk_model_id,
});
}
// extract meta for table in which looked up column belongs
// if not already extracted
if (!relatedMetas[lookedUpCol.fk_model_id]) {
relatedMetas[lookedUpCol.fk_model_id] = await Model.getWithInfo({
id: lookedUpCol.fk_model_id,
});
}
// extract metas related to the looked up column
await this.extractRelatedMetas({
col: lookedUpCol,
relatedMetas,
});
}
async publicSharedBaseGet(param: { sharedBaseUuid: string }): Promise<any> {
const project = await Project.getByUuid(param.sharedBaseUuid);

4
packages/nocodb/src/services/users/users.service.ts

@ -486,9 +486,9 @@ export class UsersService {
return this.login(user);
}
async login(user: any) {
login(user: UserType) {
return {
token: genJwt(user, Noco.getConfig()), //this.jwtService.sign(payload),
token: genJwt(user, Noco.getConfig()),
};
}

9
packages/nocodb/src/utils/nc-config/NcConfig.ts

@ -40,9 +40,6 @@ export class NcConfig {
publicUrl?: string;
dashboardPath?: string;
// TODO what is this?
envs: any;
queriesFolder: string;
env: string;
workingEnv: string;
@ -81,8 +78,7 @@ export class NcConfig {
ncConfig.env = '_noco';
ncConfig.workingEnv = '_noco';
ncConfig.projectType =
ncConfig?.envs?.[ncConfig.workingEnv]?.db?.[0]?.meta?.api?.type || 'rest';
ncConfig.projectType = 'rest';
if (ncConfig.meta?.db?.connection?.filename) {
ncConfig.meta.db.connection.filename = path.join(
@ -120,12 +116,13 @@ export class NcConfig {
}
if (publicUrl) {
ncConfig.envs['_noco'].publicUrl = publicUrl;
ncConfig.publicUrl = publicUrl;
}
if (dashboardPath) {
ncConfig.dashboardPath = dashboardPath;
} else {
ncConfig.dashboardPath = '/dashboard';
}
try {

1
packages/nocodb/src/version-upgrader/v1-legacy/gql/GqlApiBuilder.ts

@ -1,4 +1,3 @@
import debug from 'debug';
import { Router } from 'express';
import GqlXcSchemaFactory from '../../../db/sql-mgr/code/gql-schema/xc-ts/GqlXcSchemaFactory';
import BaseApiBuilder from '../BaseApiBuilder';

5
packages/nocodb/src/version-upgrader/v1-legacy/rest/RestApiBuilder.ts

@ -1,17 +1,14 @@
import debug from 'debug';
import autoBind from 'auto-bind';
import SwaggerXc from '../../../db/sql-mgr/code/routers/xc-ts/SwaggerXc';
import ExpressXcTsRoutes from '../../../db/sql-mgr/code/routes/xc-ts/ExpressXcTsRoutes';
import NcHelp from '../../../utils/NcHelp';
import BaseApiBuilder, { XcTablesPopulateParams } from '../BaseApiBuilder';
import BaseApiBuilder from '../BaseApiBuilder';
import type { MetaService } from '../../../meta/meta.service';
import type Noco from '../../../Noco';
import type { Router } from 'express';
import type { DbConfig, NcConfig } from '../../../interface/config';
import type NcProjectBuilder from '../NcProjectBuilder';
const log = debug('nc:api:rest');
export class RestApiBuilder extends BaseApiBuilder<Noco> {
public readonly type = 'rest';

79
tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

@ -37,6 +37,75 @@ export class ToolbarFilterPage extends BasePage {
await this.get().locator(`button:has-text("Add Filter")`).first().click();
}
// can reuse code for addFilterGroup and addFilter
// support for subOperation & datatype specific filter operations not supported yet
async addFilterGroup({
title,
operation,
_subOperation: _subOperation,
value,
_locallySaved: _locallySaved = false,
_dataType: _dataType,
_openModal: _openModal = false,
_skipWaitingResponse: _skipWaitingResponse = false, // used for undo (single request, less stable)
filterGroupIndex = 0,
filterLogicalOperator = 'AND',
}: {
title: string;
operation: string;
_subOperation?: string; // for date datatype
value?: string;
_locallySaved?: boolean;
_dataType?: string;
_openModal?: boolean;
_skipWaitingResponse?: boolean;
filterGroupIndex?: number;
filterLogicalOperator?: string;
}) {
await this.get().locator(`button:has-text("Add Filter Group")`).last().click();
const filterDropdown = await this.get().locator('.menu-filter-dropdown').nth(filterGroupIndex);
await filterDropdown.waitFor({ state: 'visible' });
await filterDropdown.locator(`button:has-text("Add Filter")`).first().click();
const selectField = await filterDropdown.locator('.nc-filter-field-select').last();
const selectOperation = await filterDropdown.locator('.nc-filter-operation-select').last();
const selectValue = await filterDropdown.locator('.nc-filter-value-select > input').last();
await selectField.waitFor({ state: 'visible' });
await selectField.click();
const fieldDropdown = await this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list')
.last()
.locator(`div[label="${title}"]:visible`);
await fieldDropdown.waitFor({ state: 'visible' });
await fieldDropdown.click();
await selectOperation.waitFor({ state: 'visible' });
await selectOperation.click();
const operationDropdown = await this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-filter-comp-op')
.last()
.locator(`.ant-select-item:has-text("${operation}")`);
await operationDropdown.waitFor({ state: 'visible' });
await operationDropdown.click();
await selectValue.waitFor({ state: 'visible' });
await selectValue.fill(value);
if (filterGroupIndex) {
if (filterLogicalOperator === 'OR') {
const logicalButton = await this.rootPage.locator('div.flex.bob').nth(filterGroupIndex - 1);
await logicalButton.waitFor({ state: 'visible' });
await logicalButton.click();
const logicalDropdown = await this.rootPage.locator(
'div.ant-select-dropdown.nc-dropdown-filter-logical-op-group'
);
await logicalDropdown.waitFor({ state: 'visible' });
await logicalDropdown.locator(`.ant-select-item:has-text("${filterLogicalOperator}")`).click();
}
}
}
async add({
title,
operation,
@ -65,7 +134,7 @@ export class ToolbarFilterPage extends BasePage {
await this.rootPage.locator('.nc-filter-field-select').last().click();
if (skipWaitingResponse) {
this.rootPage
await this.rootPage
.locator('div.ant-select-dropdown.nc-dropdown-toolbar-field-list')
.locator(`div[label="${title}"]:visible`)
.click();
@ -88,7 +157,7 @@ export class ToolbarFilterPage extends BasePage {
// first() : filter list has >, >=
if (skipWaitingResponse) {
this.rootPage
await this.rootPage
.locator('.nc-dropdown-filter-comp-op')
.locator(`.ant-select-item:has-text("${operation}")`)
.first()
@ -117,7 +186,7 @@ export class ToolbarFilterPage extends BasePage {
// first() : filter list has >, >=
if (skipWaitingResponse) {
this.rootPage
await this.rootPage
.locator('.nc-dropdown-filter-comp-sub-op')
.locator(`.ant-select-item:has-text("${subOperation}")`)
.first()
@ -167,7 +236,7 @@ export class ToolbarFilterPage extends BasePage {
await this.rootPage.locator(`.ant-picker-dropdown:visible`);
if (skipWaitingResponse) {
this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click();
await this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click();
} else {
await this.waitForResponse({
uiAction: () => this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click(),
@ -188,7 +257,7 @@ export class ToolbarFilterPage extends BasePage {
break;
case UITypes.Duration:
if (skipWaitingResponse) {
this.get().locator('.nc-filter-value-select').locator('input').fill(value);
await this.get().locator('.nc-filter-value-select').locator('input').fill(value);
} else {
await this.waitForResponse({
uiAction: () => this.get().locator('.nc-filter-value-select').locator('input').fill(value),

49
tests/playwright/tests/db/filters.spec.ts

@ -1339,3 +1339,52 @@ test.describe('Filter Tests: Toggle button', () => {
await dashboard.settings.toggleNullEmptyFilters();
});
});
test.describe('Filter Tests: Filter groups', () => {
/**
* Steps
*
* 1. Open table
* 2. Verify filter options : should not include NULL & EMPTY options
* 3. Enable `Show NULL & EMPTY in Filter` in Project Settings
* 4. Verify filter options : should include NULL & EMPTY options
* 5. Add NULL & EMPTY filters
* 6. Disable `Show NULL & EMPTY in Filter` in Project Settings : should not be allowed
* 7. Remove the NULL & EMPTY filters
* 8. Disable `Show NULL & EMPTY in Filter` in Project Settings again : should be allowed
*
*/
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: false });
dashboard = new DashboardPage(page, context.project);
toolbar = dashboard.grid.toolbar;
});
test('Filter: Empty filters', async () => {
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.openTable({ title: 'Country', networkResponse: false });
await toolbar.clickFilter({ networkValidation: false });
await toolbar.filter.addFilterGroup({
title: 'Country',
operation: 'is equal',
value: 'Argentina',
});
await toolbar.clickFilter({ networkValidation: false });
await toolbar.clickFilter({ networkValidation: false });
await toolbar.filter.addFilterGroup({
title: 'Country',
operation: 'is equal',
value: 'Indonesia',
filterGroupIndex: 1,
filterLogicalOperator: 'OR',
});
await toolbar.clickFilter({ networkValidation: false });
await validateRowArray({
rowCount: 2,
});
});
});

34
tests/playwright/tests/db/timezone.spec.ts

@ -147,9 +147,16 @@ test.describe('Timezone-XCDB : Japan/Tokyo', () => {
await dashboard.treeView.openTable({ title: 'dateTimeTable' });
// DateTime inserted using API without timezone is converted to UTC
// DateTime inserted using API without timezone is converted to db-timezone (server timezone in case of sqlite)
// Display value is converted to Asia/Tokyo
await dashboard.grid.cell.verifyDateCell({ index: 0, columnHeader: 'DateTime', value: '2021-01-01 09:00' });
const dateInserted = new Date(`2021-01-01 00:00:00${getBrowserTimezoneOffset()}`);
// convert dateInserted to Japan/Tokyo timezone in YYYY-MM-DD HH:mm format
const dateInsertedInJapan = new Date(dateInserted.getTime() + 9 * 60 * 60 * 1000)
.toISOString()
.slice(0, 16)
.replace('T', ' ');
await dashboard.grid.cell.verifyDateCell({ index: 0, columnHeader: 'DateTime', value: dateInsertedInJapan });
// DateTime inserted using API with timezone is converted to UTC
// Display value is converted to Asia/Tokyo
@ -173,8 +180,16 @@ test.describe('Timezone-XCDB : Japan/Tokyo', () => {
test('API Insert, verify API read response', async () => {
if (!isSqlite(context)) return;
const dateInserted = new Date(`2021-01-01 00:00:00${getBrowserTimezoneOffset()}`);
// translate dateInserted to UTC in YYYY-MM-DD HH:mm format
const dateInsertedInUTC = dateInserted.toISOString().replace('T', ' ').replace('Z', '');
// UTC expected response
const dateUTC = ['2021-01-01 00:00:00+00:00', '2021-01-01 00:00:00+00:00', '2021-01-01 00:00:00+00:00'];
const dateUTC = [
`${dateInsertedInUTC.slice(0, 19)}+00:00`,
'2021-01-01 00:00:00+00:00',
'2021-01-01 00:00:00+00:00',
];
const readDate = records.list.map(record => record.DateTime);
@ -231,7 +246,18 @@ test.describe('Timezone-XCDB : Asia/Hong-kong', () => {
// DateTime inserted using API without timezone is converted to UTC
// Display value is converted to Asia/Hong_Kong
await dashboard.grid.cell.verifyDateCell({ index: 0, columnHeader: 'DateTime', value: '2021-01-01 08:00' });
const dateInserted = new Date(`2021-01-01 00:00:00${getBrowserTimezoneOffset()}`);
// convert dateInserted to Asia/Hong-kong timezone using offset
const dateInsertedInHK = new Date(dateInserted.getTime() + 8 * 60 * 60 * 1000)
.toISOString()
.slice(0, 16)
.replace('T', ' ');
await dashboard.grid.cell.verifyDateCell({
index: 0,
columnHeader: 'DateTime',
value: dateInsertedInHK,
});
// DateTime inserted using API with timezone is converted to UTC
// Display value is converted to Asia/Hong_Kong

Loading…
Cancel
Save