Browse Source

Merge branch 'nocodb:develop' into develop

pull/5531/head
nith2001 1 year ago committed by GitHub
parent
commit
4f27ce024e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      packages/nc-gui/components/cell/Currency.vue
  2. 2
      packages/nc-gui/components/cell/DateTimePicker.vue
  3. 4
      packages/nc-gui/components/cell/Decimal.vue
  4. 4
      packages/nc-gui/components/cell/Duration.vue
  5. 4
      packages/nc-gui/components/cell/Email.vue
  6. 4
      packages/nc-gui/components/cell/Float.vue
  7. 4
      packages/nc-gui/components/cell/Integer.vue
  8. 4
      packages/nc-gui/components/cell/Percent.vue
  9. 4
      packages/nc-gui/components/cell/Text.vue
  10. 4
      packages/nc-gui/components/cell/TextArea.vue
  11. 4
      packages/nc-gui/components/cell/Url.vue
  12. 29
      packages/nc-gui/components/dashboard/TreeView.vue
  13. 3
      packages/nc-gui/components/dlg/TableCreate.vue
  14. 4
      packages/nc-gui/components/general/EmojiIcons.vue
  15. 2
      packages/nc-gui/components/smartsheet/Form.vue
  16. 29
      packages/nc-gui/components/smartsheet/Grid.vue
  17. 1
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  18. 1
      packages/nc-gui/composables/useMultiSelect/index.ts
  19. 80
      packages/nc-gui/lang/tr.json
  20. 2
      packages/nc-gui/package-lock.json
  21. 4
      packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue
  22. 2
      packages/nc-lib-gui/package.json
  23. 4
      packages/nocodb-sdk/package-lock.json
  24. 2
      packages/nocodb-sdk/package.json
  25. 2
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  26. 2
      packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts
  27. 2
      packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts
  28. 2
      packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts
  29. 2
      packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts
  30. 20
      packages/nocodb/package-lock.json
  31. 4
      packages/nocodb/package.json
  32. 4
      packages/nocodb/src/controllers/caches.controller.ts
  33. 81
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
  34. 49
      tests/playwright/tests/db/filters.spec.ts

4
packages/nc-gui/components/cell/Currency.vue

@ -53,9 +53,9 @@ const currency = computed(() => {
}
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
const submitCurrency = () => {
if (lastSaved.value !== vModel.value) {

2
packages/nc-gui/components/cell/DateTimePicker.vue

@ -18,7 +18,7 @@ import {
interface Props {
modelValue?: string | null
isPk?: boolean
isUpdatedFromCopyNPaste: Record<string, boolean>
isUpdatedFromCopyNPaste?: Record<string, boolean>
}
const { modelValue, isPk, isUpdatedFromCopyNPaste } = defineProps<Props>()

4
packages/nc-gui/components/cell/Decimal.vue

@ -36,9 +36,9 @@ const vModel = computed({
},
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
</script>
<template>

4
packages/nc-gui/components/cell/Duration.vue

@ -74,9 +74,9 @@ const submitDuration = () => {
isEdited.value = false
}
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
</script>
<template>

4
packages/nc-gui/components/cell/Email.vue

@ -35,9 +35,9 @@ const vModel = computed({
const validEmail = computed(() => vModel.value && validateEmail(vModel.value))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,

4
packages/nc-gui/components/cell/Float.vue

@ -36,9 +36,9 @@ const vModel = computed({
},
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
</script>
<template>

4
packages/nc-gui/components/cell/Integer.vue

@ -36,9 +36,9 @@ const vModel = computed({
},
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
function onKeyDown(evt: KeyboardEvent) {
return evt.key === '.' && evt.preventDefault()

4
packages/nc-gui/components/cell/Percent.vue

@ -27,9 +27,9 @@ const vModel = computed({
},
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
</script>
<template>

4
packages/nc-gui/components/cell/Text.vue

@ -23,9 +23,9 @@ const readonly = inject(ReadonlyInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
</script>
<template>

4
packages/nc-gui/components/cell/TextArea.vue

@ -19,9 +19,9 @@ const { showNull } = useGlobal()
const vModel = useVModel(props, 'modelValue', emits, { defaultValue: '' })
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLTextAreaElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLTextAreaElement)?.focus()
</script>
<template>

4
packages/nc-gui/components/cell/Url.vue

@ -63,9 +63,9 @@ const url = computed(() => {
const { cellUrlOptions } = useCellUrlConfig(url)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj)!
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen && (el as HTMLInputElement)?.focus()
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,

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

@ -1,10 +1,11 @@
<script setup lang="ts">
import { nextTick } from '@vue/runtime-core'
import { Icon as IconifyIcon } from '@iconify/vue'
import type { TableType } from 'nocodb-sdk'
import type { Input } from 'ant-design-vue'
import { Dropdown, Tooltip, message } from 'ant-design-vue'
import Sortable from 'sortablejs'
import GithubButton from 'vue-github-button'
import { Icon } from '@iconify/vue'
import type { VNodeRef } from '#imports'
import {
ClientType,
@ -279,6 +280,15 @@ function openAirtableImportDialog(baseId?: string) {
}
}
function scrollToTable(table: TableType) {
// get the table node in the tree view using the data-id attribute
const el = document.querySelector(`.nc-tree-item[data-id="${table?.id}"]`)
// scroll to the table node if found
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
}
}
function openTableCreateDialog(baseId?: string) {
$e('c:table:create:navdraw')
@ -288,6 +298,12 @@ function openTableCreateDialog(baseId?: string) {
'modelValue': isOpen,
'baseId': baseId || bases.value[0].id,
'onUpdate:modelValue': closeDialog,
'onCreate': (table: TableType) => {
// on new table created scroll to the table in the tree view
nextTick(() => {
scrollToTable(table)
})
},
})
function closeDialog() {
@ -405,6 +421,9 @@ const duplicateTable = async (table: TableType) => {
await loadTables()
const newTable = tables.value.find((el) => el.id === data?.result?.id)
if (newTable) addTableTab(newTable)
await nextTick(() => {
scrollToTable(newTable)
})
} else if (status === JobStatus.FAILED) {
message.error('Failed to duplicate table')
await loadTables()
@ -716,12 +735,12 @@ const duplicateTable = async (table: TableType) => {
<div class="flex items-center" @click.stop>
<component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'">
<span v-if="table.meta?.icon" :key="table.meta?.icon" class="nc-table-icon flex items-center">
<Icon
<IconifyIcon
:key="table.meta?.icon"
:data-testid="`nc-icon-${table.meta?.icon}`"
class="text-xl"
:icon="table.meta?.icon"
></Icon>
></IconifyIcon>
</span>
<component
:is="icon(table)"
@ -1040,12 +1059,12 @@ const duplicateTable = async (table: TableType) => {
<div class="flex items-center" @click.stop>
<component :is="isUIAllowed('tableIconCustomisation') ? Tooltip : 'div'">
<span v-if="table.meta?.icon" :key="table.meta?.icon" class="nc-table-icon flex items-center">
<Icon
<IconifyIcon
:key="table.meta?.icon"
:data-testid="`nc-icon-${table.meta?.icon}`"
class="text-xl"
:icon="table.meta?.icon"
></Icon>
></IconifyIcon>
</span>
<component
:is="icon(table)"

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

@ -19,7 +19,7 @@ const props = defineProps<{
baseId: string
}>()
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'create'])
const dialogShow = useVModel(props, 'modelValue', emit)
@ -40,6 +40,7 @@ const { table, createTable, generateUniqueTitle, tables, project } = useTable(as
type: TabType.TABLE,
})
emit('create', table)
dialogShow.value = false
}, props.baseId)

4
packages/nc-gui/components/general/EmojiIcons.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import { Icon as IconifyIcon } from '@iconify/vue'
import InfiniteLoading from 'v3-infinite-loading'
import { emojiIcons } from '#imports'
@ -48,7 +48,7 @@ const selectIcon = (icon?: string) => {
<div class="flex gap-1 flex-wrap w-full flex-shrink overflow-y-auto scrollbar-thin-dull">
<div v-for="icon of filteredIcons" :key="icon" @click="selectIcon(icon)">
<span class="cursor-pointer nc-emoji-item">
<Icon class="text-xl iconify" :icon="`emojione:${icon}`"></Icon>
<IconifyIcon class="text-xl iconify" :icon="`emojione:${icon}`"></IconifyIcon>
</span>
</div>
<InfiniteLoading @infinite="load"><span /></InfiniteLoading>

2
packages/nc-gui/components/smartsheet/Form.vue

@ -837,7 +837,7 @@ watch(view, (nextView) => {
<style scoped lang="scss">
.nc-editable:hover {
.nc-field-remove-icon {
:deep(.nc-field-remove-icon) {
@apply opacity-100;
}
}

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 }) => {

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

@ -188,7 +188,6 @@ useEventListener('keydown', (e: KeyboardEvent) => {
<LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<LazySmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" />
<LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<LazySmartsheetColumnGeoDataOptions v-if="formState.uidt === UITypes.GeoData" v-model:value="formState" />
<LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />

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

@ -208,6 +208,7 @@ export function useMultiSelect(
selectedRange.startRange({ row, col })
selectedRange.endRange({ row, col })
makeActive(row, col)
scrollToActiveCell?.()
isMouseDown = false
}

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",
"version": "0.108.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",

4
packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Icon as IconifyIcon } from '@iconify/vue'
import type { TabItem } from '~/lib'
import { TabType } from '~/lib'
import { TabMetaInj, iconMap, provide, storeToRefs, useGlobal, useSidebar, useTabs } from '#imports'
@ -58,7 +58,7 @@ const hideSidebarOnClickOrTouchIfMobileMode = () => {
<template #tab>
<div class="flex items-center gap-2" data-testid="nc-tab-title">
<div class="flex items-center">
<Icon
<IconifyIcon
v-if="tab.meta?.icon"
:icon="tab.meta?.icon"
class="text-xl"

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

@ -1,6 +1,6 @@
{
"name": "nc-lib-gui",
"version": "0.108.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",
"version": "0.108.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb-sdk",
"version": "0.108.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",
"version": "0.108.1",
"description": "NocoDB SDK",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",

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

@ -832,7 +832,7 @@ export class MssqlUi {
}
static getAbstractType(col): any {
switch ((col.dt || col.dt).toLowerCase()) {
switch (col.dt?.toLowerCase()) {
case 'bigint':
case 'smallint':
case 'bit':

2
packages/nocodb-sdk/src/lib/sqlUi/MysqlUi.ts

@ -871,7 +871,7 @@ export class MysqlUi {
}
static getAbstractType(col): any {
switch (col.dt.toLowerCase()) {
switch (col.dt?.toLowerCase()) {
case 'int':
case 'smallint':
case 'mediumint':

2
packages/nocodb-sdk/src/lib/sqlUi/OracleUi.ts

@ -689,7 +689,7 @@ export class OracleUi {
}
static getAbstractType(col): any {
switch ((col.dt || col.dt).toLowerCase()) {
switch (col.dt?.toLowerCase()) {
case 'integer':
return 'integer';
case 'bfile':

2
packages/nocodb-sdk/src/lib/sqlUi/PgUi.ts

@ -1358,7 +1358,7 @@ export class PgUi {
}
static getAbstractType(col): any {
switch ((col.dt || col.dt).toLowerCase()) {
switch (col.dt?.toLowerCase()) {
case 'anyenum':
return 'enum';
case 'anynonarray':

2
packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts

@ -572,7 +572,7 @@ export class SnowflakeUi {
}
static getAbstractType(col): any {
switch (col.dt.toUpperCase()) {
switch (col.dt?.toUpperCase()) {
case 'NUMBER':
case 'DECIMAL':
case 'NUMERIC':

20
packages/nocodb/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb",
"version": "0.108.0",
"version": "0.108.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb",
"version": "0.108.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",
"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",
"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",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.108.0.tgz",
"integrity": "sha512-ZV4g5ivc5T5rGDZD89UqJtazNOv4Bo/mc/8sf3OXLOPB4ksIp8v63mSIeb+jBE9TRdptRKA+Ml67Rczwa+0SKA==",
"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",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.108.0.tgz",
"integrity": "sha512-ZV4g5ivc5T5rGDZD89UqJtazNOv4Bo/mc/8sf3OXLOPB4ksIp8v63mSIeb+jBE9TRdptRKA+Ml67Rczwa+0SKA==",
"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",
"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",
"nc-lib-gui": "0.108.1",
"nc-plugin": "^0.1.3",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",

4
packages/nocodb/src/controllers/caches.controller.ts

@ -1,8 +1,10 @@
import { Controller, Delete, Get } from '@nestjs/common';
import { Controller, Delete, Get, UseGuards } from '@nestjs/common';
import { GlobalGuard } from '../guards/global/global.guard';
import { Acl } from '../middlewares/extract-project-id/extract-project-id.middleware';
import { CachesService } from '../services/caches.service';
@Controller()
@UseGuards(GlobalGuard)
export class CachesController {
constructor(private readonly cachesService: CachesService) {}

81
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),
@ -298,4 +367,4 @@ export class ToolbarFilterPage extends BasePage {
return opListText;
}
}
}

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

Loading…
Cancel
Save