Browse Source

Merge branch 'develop' into mvp-mobile-layout-and-code-scanner

pull/5114/head
Daniel Spaude 2 years ago
parent
commit
d50f683acf
No known key found for this signature in database
GPG Key ID: 654A3D1FA4F35FFE
  1. 9
      packages/nc-gui/components/account/License.vue
  2. 4
      packages/nc-gui/components/account/SignupSettings.vue
  3. 14
      packages/nc-gui/components/account/Token.vue
  4. 12
      packages/nc-gui/components/account/UserList.vue
  5. 13
      packages/nc-gui/components/account/UsersModal.vue
  6. 13
      packages/nc-gui/components/cell/Checkbox.vue
  7. 4
      packages/nc-gui/components/cell/Currency.vue
  8. 3
      packages/nc-gui/components/cell/DatePicker.vue
  9. 5
      packages/nc-gui/components/cell/DateTimePicker.vue
  10. 7
      packages/nc-gui/components/cell/Decimal.vue
  11. 3
      packages/nc-gui/components/cell/Duration.vue
  12. 9
      packages/nc-gui/components/cell/Float.vue
  13. 2
      packages/nc-gui/components/cell/GeoData.vue
  14. 9
      packages/nc-gui/components/cell/Integer.vue
  15. 3
      packages/nc-gui/components/cell/MultiSelect.vue
  16. 4
      packages/nc-gui/components/cell/Rating.vue
  17. 8
      packages/nc-gui/components/cell/Url.vue
  18. 3
      packages/nc-gui/components/cell/attachment/utils.ts
  19. 5
      packages/nc-gui/components/dashboard/TreeView.vue
  20. 4
      packages/nc-gui/components/dashboard/settings/AuditTab.vue
  21. 31
      packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue
  22. 2
      packages/nc-gui/components/dlg/TableRename.vue
  23. 4
      packages/nc-gui/components/dlg/ViewCreate.vue
  24. 4
      packages/nc-gui/components/erd/View.vue
  25. 2
      packages/nc-gui/components/general/TableIcon.vue
  26. 2
      packages/nc-gui/components/smartsheet/ApiSnippet.vue
  27. 2
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  28. 2
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  29. 11
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  30. 5
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  31. 9
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  32. 7
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  33. 4
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  34. 5
      packages/nc-gui/components/smartsheet/toolbar/SharedViewList.vue
  35. 6
      packages/nc-gui/components/tabs/auth/UserManagement.vue
  36. 4
      packages/nc-gui/components/tabs/auth/user-management/ShareBase.vue
  37. 12
      packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue
  38. 2
      packages/nc-gui/components/virtual-cell/barcode/Barcode.vue
  39. 26
      packages/nc-gui/components/webhook/Editor.vue
  40. 4
      packages/nc-gui/components/webhook/List.vue
  41. 14
      packages/nc-gui/components/webhook/Test.vue
  42. 12
      packages/nc-gui/composables/useGlobal/actions.ts
  43. 4
      packages/nc-gui/composables/useKanbanViewStore.ts
  44. 6
      packages/nc-gui/composables/useMapViewDataStore.ts
  45. 4
      packages/nc-gui/composables/useMetas.ts
  46. 5
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  47. 4
      packages/nc-gui/composables/useSharedView.ts
  48. 6
      packages/nc-gui/composables/useTable.ts
  49. 4
      packages/nc-gui/composables/useViewFilters.ts
  50. 2
      packages/nc-gui/context/index.ts
  51. 6
      packages/nc-gui/lib/types.ts
  52. 4
      packages/nc-gui/middleware/auth.global.ts
  53. 31
      packages/nc-gui/package-lock.json
  54. 1
      packages/nc-gui/package.json
  55. 2
      packages/nc-gui/pages/[projectType]/[projectId]/index.vue
  56. 5
      packages/nc-gui/pages/index/index/create-external.vue
  57. 5
      packages/nc-gui/pages/index/index/create.vue
  58. 7
      packages/nc-gui/pages/index/index/index.vue
  59. 4
      packages/nc-gui/store/project.ts
  60. 4
      packages/nc-gui/utils/dataUtils.ts
  61. 20
      packages/nc-gui/utils/filterUtils.ts
  62. 1
      packages/nc-gui/utils/index.ts
  63. 17
      packages/nc-gui/utils/parseUtils.ts
  64. 6
      packages/nc-gui/utils/virtualCell.ts
  65. 60
      packages/nocodb-sdk/src/lib/Api.ts
  66. 1
      packages/nocodb-sdk/src/lib/UITypes.ts
  67. 14
      packages/nocodb-sdk/src/lib/formulaHelpers.ts
  68. 8
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts
  69. 9
      packages/nocodb/src/lib/services/project.svc.ts
  70. 10
      packages/nocodb/src/lib/version-upgrader/ncFilterUpgrader_0105003.ts
  71. 156
      packages/nocodb/src/schema/swagger.json
  72. 21
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
  73. 28
      tests/playwright/setup/xcdb-records.ts
  74. 33
      tests/playwright/tests/filters.spec.ts

9
packages/nc-gui/components/account/License.vue

@ -14,9 +14,8 @@ let key = $ref('')
const loadLicense = async () => {
try {
const response = await api.orgLicense.get()
key = response.key
} catch (e) {
key = response.key!
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -25,7 +24,7 @@ const setLicense = async () => {
await api.orgLicense.set({ key: key })
message.success('License key updated')
await loadAppInfo()
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:account:license')
@ -45,5 +44,3 @@ loadLicense()
</div>
</div>
</template>
<style scoped></style>

4
packages/nc-gui/components/account/SignupSettings.vue

@ -12,7 +12,7 @@ const loadSettings = async () => {
try {
const response = await api.orgAppSettings.get()
settings = response
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -21,7 +21,7 @@ const saveSettings = async () => {
try {
await api.orgAppSettings.set(settings)
message.success(t('msg.success.settingsSaved'))
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}

14
packages/nc-gui/components/account/Token.vue

@ -1,4 +1,5 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { Empty, Modal, message } from 'ant-design-vue'
import type { ApiTokenType, RequestParams, UserType } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, useApi, useCopy, useNuxtApp } from '#imports'
@ -42,7 +43,7 @@ const loadTokens = async (page = currentPage, limit = currentLimit) => {
pagination.pageSize = 10
tokens = response.list as UserType[]
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -55,11 +56,10 @@ const deleteToken = async (token: string) => {
type: 'warn',
onOk: async () => {
try {
// todo: delete token
await api.orgTokens.delete(token)
message.success(t('msg.success.tokenDeleted'))
await loadTokens()
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:account:token:delete')
@ -75,7 +75,7 @@ const generateToken = async () => {
message.success(t('msg.success.tokenGenerated'))
selectedTokenData = {}
await loadTokens()
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:api-token:generate')
@ -90,14 +90,12 @@ const copyToken = async (token: string | undefined) => {
message.info(t('msg.info.copiedToClipboard'))
$e('c:api-token:copy')
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
}
const descriptionInput = (el) => {
el?.focus()
}
const descriptionInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script>
<template>

12
packages/nc-gui/components/account/UserList.vue

@ -50,7 +50,7 @@ const loadUsers = async (page = currentPage, limit = currentLimit) => {
pagination.pageSize = 10
users = response.list as UserType[]
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -65,7 +65,7 @@ const updateRole = async (userId: string, roles: Role) => {
message.success(t('msg.success.roleUpdated'))
$e('a:org-user:role-updated', { role: roles })
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -80,7 +80,7 @@ const deleteUser = async (userId: string) => {
message.success(t('msg.success.userDeleted'))
await loadUsers()
$e('a:org-user:user-deleted')
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
},
@ -94,7 +94,7 @@ const resendInvite = async (user: User) => {
// Invite email sent successfully
message.success(t('msg.success.inviteEmailSent'))
await loadUsers()
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -108,7 +108,7 @@ const copyInviteUrl = async (user: User) => {
// Invite URL copied to clipboard
message.success(t('msg.success.inviteURLCopied'))
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
$e('c:user:copy-url')
@ -123,7 +123,7 @@ const copyPasswordResetUrl = async (user: User) => {
// Invite URL copied to clipboard
message.success(t('msg.success.passwordResetURLCopied'))
$e('c:user:copy-url')
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}

13
packages/nc-gui/components/account/UsersModal.vue

@ -1,5 +1,6 @@
<script setup lang="ts">
import type { UserType } from 'nocodb-sdk'
import type { VNodeRef } from '@vue/runtime-core'
import type { OrgUserReqType } from 'nocodb-sdk'
import {
Form,
computed,
@ -72,11 +73,10 @@ const saveUser = async () => {
await formRef.value?.validateFields()
try {
// todo: update sdk(swagger.json)
const res = await $api.orgUsers.add({
roles: usersData.role,
email: usersData.emails,
} as unknown as UserType)
} as unknown as OrgUserReqType)
usersData.invitationToken = res.invite_token
emit('reload')
@ -98,7 +98,7 @@ const copyUrl = async () => {
// Copied shareable base url to clipboard!
message.success(t('msg.success.shareableURLCopied'))
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
$e('c:shared-base:copy-url')
@ -110,9 +110,8 @@ const clickInviteMore = () => {
usersData.role = Role.OrgLevelViewer
usersData.emails = ''
}
const emailInput = ref((el) => {
el?.focus()
})
const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script>
<template>

13
packages/nc-gui/components/cell/Checkbox.vue

@ -1,5 +1,14 @@
<script setup lang="ts">
import { ActiveCellInj, ColumnInj, IsFormInj, ReadonlyInj, getMdiIcon, inject, useSelectedCellKeyupListener } from '#imports'
import {
ActiveCellInj,
ColumnInj,
IsFormInj,
ReadonlyInj,
getMdiIcon,
inject,
parseProp,
useSelectedCellKeyupListener,
} from '#imports'
interface Props {
// If the previous cell value was a text, the initial checkbox value is a string type
@ -35,7 +44,7 @@ const checkboxMeta = $computed(() => {
unchecked: 'mdi-checkbox-blank-circle-outline',
},
color: 'primary',
...(column?.value?.meta || {}),
...parseProp(column?.value?.meta),
}
})

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { ColumnInj, EditModeInj, computed, inject, useVModel } from '#imports'
import { ColumnInj, EditModeInj, computed, inject, parseProp, useVModel } from '#imports'
interface Props {
modelValue: number | null | undefined
@ -35,7 +35,7 @@ const currencyMeta = computed(() => {
return {
currency_locale: 'en-US',
currency_code: 'USD',
...(column.value.meta ? column.value.meta : {}),
...parseProp(column?.value?.meta),
}
})

3
packages/nc-gui/components/cell/DatePicker.vue

@ -8,6 +8,7 @@ import {
computed,
inject,
isDrawerOrModalExist,
parseProp,
ref,
useSelectedCellKeyupListener,
watch,
@ -34,7 +35,7 @@ const editable = inject(EditModeInj, ref(false))
let isDateInvalid = $ref(false)
const dateFormat = $computed(() => columnMeta?.value?.meta?.date_format ?? 'YYYY-MM-DD')
const dateFormat = $computed(() => parseProp(columnMeta?.value?.meta)?.date_format ?? 'YYYY-MM-DD')
let localState = $computed({
get() {

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

@ -7,6 +7,7 @@ import {
dateFormats,
inject,
isDrawerOrModalExist,
parseProp,
ref,
timeFormats,
useProject,
@ -38,8 +39,8 @@ const column = inject(ColumnInj)!
let isDateInvalid = $ref(false)
const dateTimeFormat = $computed(() => {
const dateFormat = column?.value?.meta?.date_format ?? dateFormats[0]
const timeFormat = column?.value?.meta?.time_format ?? timeFormats[0]
const dateFormat = parseProp(column?.value?.meta)?.date_format ?? dateFormats[0]
const timeFormat = parseProp(column?.value?.meta)?.time_format ?? timeFormats[0]
return `${dateFormat} ${timeFormat}`
})

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

@ -3,6 +3,9 @@ import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, inject, useVModel } from '#imports'
interface Props {
// when we set a number, then it is number type
// for sqlite, when we clear a cell or empty the cell, it returns ""
// otherwise, it is null type
modelValue?: number | null | string
}
@ -22,8 +25,10 @@ const _vModel = useVModel(props, 'modelValue', emits)
const vModel = computed({
get: () => _vModel.value,
set: (value: string) => {
set: (value) => {
if (value === '') {
// if we clear / empty a cell in sqlite,
// the value is considered as ''
_vModel.value = null
} else {
_vModel.value = value

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

@ -8,6 +8,7 @@ import {
convertMS2Duration,
durationOptions,
inject,
parseProp,
ref,
} from '#imports'
@ -32,7 +33,7 @@ const durationInMS = ref(0)
const isEdited = ref(false)
const durationType = computed(() => column?.value?.meta?.duration || 0)
const durationType = computed(() => parseProp(column?.value?.meta)?.duration || 0)
const durationPlaceholder = computed(() => durationOptions[durationType.value].title)

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

@ -3,7 +3,10 @@ import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, inject, useVModel } from '#imports'
interface Props {
modelValue?: number | null
// when we set a number, then it is number type
// for sqlite, when we clear a cell or empty the cell, it returns ""
// otherwise, it is null type
modelValue?: number | null | string
}
interface Emits {
@ -22,8 +25,10 @@ const _vModel = useVModel(props, 'modelValue', emits)
const vModel = computed({
get: () => _vModel.value,
set: (value: string) => {
set: (value) => {
if (value === '') {
// if we clear / empty a cell in sqlite,
// the value is considered as ''
_vModel.value = null
} else {
_vModel.value = value

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

@ -56,7 +56,7 @@ const onClickSetCurrentLocation = () => {
isLoading = false
}
const onError: PositionErrorCallback = (err) => {
const onError: PositionErrorCallback = (err: GeolocationPositionError) => {
console.error(`ERROR(${err.code}): ${err.message}`)
isLoading = false
}

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

@ -3,7 +3,10 @@ import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, inject, useVModel } from '#imports'
interface Props {
modelValue?: number | null
// when we set a number, then it is number type
// for sqlite, when we clear a cell or empty the cell, it returns ""
// otherwise, it is null type
modelValue?: number | null | string
}
interface Emits {
@ -22,8 +25,10 @@ const _vModel = useVModel(props, 'modelValue', emits)
const vModel = computed({
get: () => _vModel.value,
set: (value: string) => {
set: (value) => {
if (value === '') {
// if we clear / empty a cell in sqlite,
// the value is considered as ''
_vModel.value = null
} else {
_vModel.value = value

3
packages/nc-gui/components/cell/MultiSelect.vue

@ -260,8 +260,7 @@ async function addIfMissingAndSave() {
} else {
activeOptCreateInProgress.value--
}
} catch (e) {
// todo: handle error
} catch (e: any) {
console.log(e)
activeOptCreateInProgress.value--
message.error(await extractSdkResponseErrorMsg(e))

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

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ActiveCellInj, ColumnInj, computed, inject, useSelectedCellKeyupListener } from '#imports'
import { ActiveCellInj, ColumnInj, computed, inject, parseProp, useSelectedCellKeyupListener } from '#imports'
interface Props {
modelValue?: number | null | undefined
@ -19,7 +19,7 @@ const ratingMeta = computed(() => {
},
color: '#fcb401',
max: 5,
...(column.value?.meta || {}),
...parseProp(column.value?.meta),
}
})

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

@ -8,6 +8,7 @@ import {
inject,
isValidURL,
message,
parseProp,
ref,
useCellUrlConfig,
useI18n,
@ -39,7 +40,7 @@ const vModel = computed({
get: () => value,
set: (val) => {
localState.value = val
if (!column.value.meta?.validate || (val && isValidURL(val)) || !val) {
if (!parseProp(column.value.meta)?.validate || (val && isValidURL(val)) || !val) {
emit('update:modelValue', val)
}
},
@ -63,7 +64,7 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
watch(
() => editEnabled.value,
() => {
if (column.value.meta?.validate && !editEnabled.value && localState.value && !isValidURL(localState.value)) {
if (parseProp(column.value.meta)?.validate && !editEnabled.value && localState.value && !isValidURL(localState.value)) {
message.error(t('msg.error.invalidURL'))
localState.value = undefined
return
@ -126,6 +127,3 @@ watch(
</div>
</div>
</template>
<!--
-->

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

@ -12,6 +12,7 @@ import {
inject,
isImage,
message,
parseProp,
ref,
storeToRefs,
useApi,
@ -102,7 +103,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
const attachmentMeta = {
...defaultAttachmentMeta,
...(typeof column.value?.meta === 'string' ? JSON.parse(column.value.meta) : column.value?.meta),
...parseProp(column?.value?.meta),
}
const newAttachments = []

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

@ -14,6 +14,7 @@ import {
extractSdkResponseErrorMsg,
isDrawerOrModalExist,
isMac,
parseProp,
reactive,
ref,
resolveComponent,
@ -314,7 +315,7 @@ watch(
const setIcon = async (icon: string, table: TableType) => {
try {
table.meta = {
...(table.meta || {}),
...parseProp(table.meta),
icon,
}
tables.value.splice(tables.value.indexOf(table), 1, { ...table })
@ -326,7 +327,7 @@ const setIcon = async (icon: string, table: TableType) => {
})
$e('a:table:icon:navdraw', { icon })
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}

4
packages/nc-gui/components/dashboard/settings/AuditTab.vue

@ -28,8 +28,8 @@ async function loadAudits(page = currentPage, limit = currentLimit) {
isLoading = true
const { list, pageInfo } = await $api.project.auditList(project.value?.id, {
offset: (limit * (page - 1)).toString(),
limit: limit.toString(),
offset: limit * (page - 1),
limit,
})
audits = list

31
packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import type { PluginType } from 'nocodb-sdk'
import type { PluginTestReqType, PluginType } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, message, onMounted, ref, useI18n, useNuxtApp } from '#imports'
const { id } = defineProps<{
@ -64,19 +64,20 @@ const testSettings = async () => {
loadingAction = Action.Test
try {
const res = await $api.plugin.test({
input: pluginFormData,
id: plugin?.id,
category: plugin?.category,
title: plugin?.title,
})
if (res) {
// Successfully tested plugin settings
message.success(t('msg.success.pluginTested'))
} else {
// Invalid credentials
message.info(t('msg.info.invalidCredentials'))
if (plugin) {
const res = await $api.plugin.test({
input: JSON.stringify(pluginFormData),
title: plugin.title,
category: plugin.category,
} as PluginTestReqType)
if (res) {
// Successfully tested plugin settings
message.success(t('msg.success.pluginTested'))
} else {
// Invalid credentials
message.info(t('msg.info.invalidCredentials'))
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@ -106,7 +107,7 @@ const readPluginDetails = async () => {
const res = await $api.plugin.read(id)
const formDetails = JSON.parse(res.input_schema ?? '{}')
const emptyParsedInput = formDetails.array ? [{}] : {}
const parsedInput = res.input ? JSON.parse(res.input) : emptyParsedInput
const parsedInput = typeof res.input === 'string' ? JSON.parse(res.input) : emptyParsedInput
// the type of 'secure' was XcType.SingleLineText in 0.0.1
// and it has been changed to XcType.Checkbox, since 0.0.2

2
packages/nc-gui/components/dlg/TableRename.vue

@ -41,7 +41,7 @@ const { updateTab } = useTabs()
const projectStore = useProject()
const { loadTables, isMysql, isMssql, isPg } = projectStore
const { project } = storeToRefs(projectStore)
const { tables, project } = storeToRefs(projectStore)
const inputEl = $ref<ComponentPublicInstance>()

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

@ -75,9 +75,7 @@ const viewNameRules = [
{
validator: (_: unknown, v: string) =>
new Promise((resolve, reject) => {
views.every((v1) => ((v1 as GridType | KanbanType | GalleryType | MapType).alias || v1.title) !== v)
? resolve(true)
: reject(new Error(`View name should be unique`))
views.every((v1) => v1.title !== v) ? resolve(true) : reject(new Error(`View name should be unique`))
}),
message: 'View name should be unique',
},

4
packages/nc-gui/components/erd/View.vue

@ -40,8 +40,8 @@ const populateTables = async () => {
// if table is provided only get the table and its related tables
localTables = projectTables.value.filter(
(t) =>
t.id === props.table.id ||
props.table.columns?.find(
t.id === props.table?.id ||
props.table?.columns?.find(
(column) =>
column.uidt === UITypes.LinkToAnotherRecord &&
(column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id === t.id,

2
packages/nc-gui/components/general/TableIcon.vue

@ -18,5 +18,3 @@ const { meta: tableMeta } = defineProps<{
<MdiEyeCircleOutline v-else-if="tableMeta?.type === 'view'" class="w-5" />
<MdiTableLarge v-else class="w-5" />
</template>
<style scoped></style>

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

@ -132,7 +132,7 @@ const onCopyToClipboard = async () => {
await copy(code)
// Copied to clipboard
message.info(t('msg.info.copiedToClipboard'))
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
}

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

@ -59,7 +59,7 @@ const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const onlyNameUpdateOnEditColumns = [UITypes.LinkToAnotherRecord, UITypes.Lookup, UITypes.Rollup]
const geoDataToggleCondition = (t) => {
const geoDataToggleCondition = (t: { name: UITypes }) => {
return betaFeatureToggleState.show ? betaFeatureToggleState.show : !t.name.includes(UITypes.GeoData)
}

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

@ -215,7 +215,7 @@ useActiveKeyupListener(
// on alt + s save record
else if (e.code === 'KeyS') {
// remove focus from the active input if any
document.activeElement?.blur()
;(document.activeElement as HTMLElement)?.blur()
e.stopPropagation()

11
packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts

@ -1,5 +1,5 @@
import type { PropType } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import { ColumnInj, MetaInj, defineComponent, h, inject, isBt, isHm, isLookup, isMm, isRollup, ref, toRef } from '#imports'
@ -73,9 +73,9 @@ export default defineComponent({
setup(props) {
const columnMeta = toRef(props, 'columnMeta')
const column = inject(ColumnInj, columnMeta) as Ref<ColumnType & { colOptions: LookupType }>
const column = inject(ColumnInj, columnMeta) as Ref<ColumnType & { colOptions: LookupType | RollupType }>
let relationColumn: ColumnType & { colOptions: LookupType }
let relationColumn: ColumnType
return () => {
if (!column.value) return null
@ -83,12 +83,9 @@ export default defineComponent({
if (column && column.value) {
if (isMm(column.value) || isHm(column.value) || isBt(column.value) || isLookup(column.value) || isRollup(column.value)) {
const meta = inject(MetaInj, ref())
relationColumn = meta.value?.columns?.find(
(c) => c.id === column.value?.colOptions?.fk_relation_column_id,
) as ColumnType & {
colOptions: LinkToAnotherRecordType
}
) as ColumnType
}
}

5
packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue

@ -10,6 +10,7 @@ import {
inject,
message,
onMounted,
parseProp,
ref,
resolveComponent,
useApi,
@ -218,7 +219,7 @@ const setIcon = async (icon: string, view: ViewType) => {
try {
// modify the icon property in meta
view.meta = {
...(view.meta || {}),
...parseProp(view.meta),
icon,
}
@ -227,7 +228,7 @@ const setIcon = async (icon: string, view: ViewType) => {
})
$e('a:view:icon:sidebar', { icon })
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}

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

@ -109,7 +109,7 @@ const filterUpdateCondition = (filter: FilterType, i: number) => {
}
}
saveOrUpdate(filter, i)
filterPrevComparisonOp.value[filter.id] = filter.comparison_op
filterPrevComparisonOp.value[filter.id!] = filter.comparison_op!
$e('a:filter:update', {
logical: filter.logical_op,
comparison: filter.comparison_op,
@ -165,11 +165,10 @@ const selectFilterField = (filter: Filter, index: number) => {
// since the existing one may not be supported for the new field
// e.g. `eq` operator is not supported in checkbox field
// hence, get the first option of the supported operators of the new field
filter.comparison_op = comparisonOpList(col.uidt as UITypes).filter((compOp) =>
isComparisonOpAllowed(filter, compOp),
)?.[0].value
filter.comparison_op = comparisonOpList(col.uidt as UITypes).find((compOp) => isComparisonOpAllowed(filter, compOp))
?.value as FilterType['comparison_op']
if ([UITypes.Date, UITypes.DateTime].includes(col.uidt as UITypes) && !['blank', 'notblank'].includes(filter.comparison_op)) {
if ([UITypes.Date, UITypes.DateTime].includes(col.uidt as UITypes) && !['blank', 'notblank'].includes(filter.comparison_op!)) {
if (filter.comparison_op === 'isWithin') {
filter.comparison_sub_op = 'pastNumberOfDays'
} else {

7
packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue

@ -84,10 +84,11 @@ const checkTypeFunctions = {
type FilterType = keyof typeof checkTypeFunctions
// todo: replace with sqlUis
const { sqlUi } = $(storeToRefs(useProject()))
const { sqlUis } = storeToRefs(useProject())
const abstractType = $computed(() => (column.value?.dt && sqlUi ? sqlUi.getAbstractType(column.value) : null))
const sqlUi = ref(column.value?.base_id ? sqlUis.value[column.value?.base_id] : Object.values(sqlUis.value)[0])
const abstractType = computed(() => column.value && sqlUi.value.getAbstractType(column.value))
const checkType = (filterType: FilterType) => {
const checkTypeFunction = checkTypeFunctions[filterType]

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

@ -197,7 +197,7 @@ const copyLink = async () => {
// Copied to clipboard
message.success(t('msg.info.copiedToClipboard'))
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
}
@ -232,7 +232,7 @@ const copyIframeCode = async () => {
// Copied to clipboard
message.success(t('msg.info.copiedToClipboard'))
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
}

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

@ -5,6 +5,7 @@ import {
extractSdkResponseErrorMsg,
message,
onMounted,
parseProp,
ref,
useCopy,
useDashboard,
@ -70,7 +71,7 @@ const sharedViewUrl = (view: SharedViewType) => {
const renderAllowCSVDownload = (view: SharedViewType) => {
if (view.type === ViewTypes.GRID) {
view.meta = (view.meta && typeof view.meta === 'string' ? JSON.parse(view.meta) : view.meta) as Record<string, any>
view.meta = (view.meta && parseProp(view.meta)) as Record<string, any>
return view.meta?.allowCSVDownload ? '✔' : '❌'
} else {
return 'N/A'
@ -82,7 +83,7 @@ const copyLink = (view: SharedViewType) => {
copy(`${dashboardUrl?.value as string}#${sharedViewUrl(view)}`)
// Copied to clipboard
message.success(t('msg.info.copiedToClipboard'))
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
}

6
packages/nc-gui/components/tabs/auth/UserManagement.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import { OrgUserRoles } from 'nocodb-sdk'
import type { RequestParams } from 'nocodb-sdk'
import type { ProjectUserReqType, RequestParams } from 'nocodb-sdk'
import {
extractSdkResponseErrorMsg,
message,
@ -82,7 +82,7 @@ const inviteUser = async (user: User) => {
user.roles = 'editor'
}
await api.auth.projectUserAdd(project.value.id, user)
await api.auth.projectUserAdd(project.value.id, user as ProjectUserReqType)
// Successfully added user to project
message.success(t('msg.success.userAddedToProject'))
@ -153,7 +153,7 @@ const copyInviteUrl = async (user: User) => {
// Invite URL copied to clipboard
message.success(t('msg.success.inviteURLCopied'))
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
$e('c:user:copy-url')

4
packages/nc-gui/components/tabs/auth/user-management/ShareBase.vue

@ -109,7 +109,7 @@ const copyUrl = async () => {
// Copied shareable base url to clipboard!
message.success(t('msg.success.shareableURLCopied'))
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
@ -137,7 +137,7 @@ style="background: transparent; border: 1px solid #ddd"></iframe>`)
// Copied embeddable html code!
message.success(t('msg.success.embeddableHTMLCodeCopied'))
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
$e('c:shared-base:copy-embed-frame')

12
packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue

@ -1,5 +1,6 @@
<script setup lang="ts">
import type { Input } from 'ant-design-vue'
import type { ProjectUserReqType } from 'nocodb-sdk'
import {
Form,
computed,
@ -109,10 +110,11 @@ const saveUser = async () => {
const res = await $api.auth.projectUserAdd(project.value.id, {
roles: usersData.role,
email: usersData.emails,
project_id: project.value.id,
projectName: project.value.title,
})
usersData.invitationToken = res.invite_token
} as ProjectUserReqType)
// for inviting one user, invite_token will only be returned when invitation email fails to send
// for inviting multiple users, invite_token will be returned anyway
usersData.invitationToken = res?.invite_token
}
emit('reload')
@ -133,7 +135,7 @@ const copyUrl = async () => {
// Copied shareable base url to clipboard!
message.success(t('msg.success.shareableURLCopied'))
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
$e('c:shared-base:copy-url')

2
packages/nc-gui/components/virtual-cell/barcode/Barcode.vue

@ -22,7 +22,7 @@ const showBarcodeModal = () => {
const barcodeMeta = $computed(() => {
return {
barcodeFormat: 'CODE128',
...(column?.value?.meta || {}),
...parseProp(column?.value?.meta),
}
})

26
packages/nc-gui/components/webhook/Editor.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import type { AuditType } from 'nocodb-sdk'
import type { AuditType, HookType } from 'nocodb-sdk'
import {
Form,
MetaInj,
@ -10,6 +10,7 @@ import {
inject,
message,
onMounted,
parseProp,
reactive,
ref,
useApi,
@ -20,7 +21,7 @@ import {
} from '#imports'
interface Props {
hook?: Record<string, any>
hook?: HookType
}
const props = defineProps<Props>()
@ -39,11 +40,13 @@ const meta = inject(MetaInj, ref())
const useForm = Form.useForm
const hook = reactive<Record<string, any>>({
const hook = reactive<
Omit<HookType, 'notification'> & { notification: Record<string, any>; eventOperation: string; condition: boolean }
>({
id: '',
title: '',
event: '',
operation: '',
event: undefined,
operation: undefined,
eventOperation: '',
notification: {
type: 'URL',
@ -256,12 +259,13 @@ function onNotTypeChange(reset = false) {
}
}
function setHook(newHook: any) {
function setHook(newHook: HookType) {
const notification = newHook.notification as Record<string, any>
Object.assign(hook, {
...newHook,
notification: {
...newHook.notification,
payload: newHook.notification.payload,
...notification,
payload: notification.payload,
},
})
}
@ -325,7 +329,7 @@ async function loadPluginList() {
...(p as any),
}
plugin.tags = p.tags ? p.tags.split(',') : []
plugin.parsedInput = typeof p.input === 'string' ? JSON.parse(p.input) : p.input
plugin.parsedInput = parseProp(p.input)
o[plugin.title] = plugin
return o
@ -400,8 +404,8 @@ watch(
if (!hook.eventOperation) return
const [event, operation] = hook.eventOperation.split(' ')
hook.event = event
hook.operation = operation
hook.event = event as HookType['event']
hook.operation = operation as HookType['operation']
},
)

4
packages/nc-gui/components/webhook/List.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { HookType } from 'nocodb-sdk'
import { MetaInj, extractSdkResponseErrorMsg, inject, message, onMounted, ref, useI18n, useNuxtApp } from '#imports'
import { MetaInj, extractSdkResponseErrorMsg, inject, message, onMounted, parseProp, ref, useI18n, useNuxtApp } from '#imports'
const emit = defineEmits(['edit', 'add'])
@ -16,7 +16,7 @@ async function loadHooksList() {
try {
const hookList = (await $api.dbTableWebhook.list(meta.value?.id as string)).list as HookType[]
hooks.value = hookList.map((hook) => {
hook.notification = typeof hook.notification === 'string' ? JSON.parse(hook.notification) : hook.notification
hook.notification = parseProp(hook.notification)
return hook
})
} catch (e: any) {

14
packages/nc-gui/components/webhook/Test.vue

@ -1,8 +1,9 @@
<script setup lang="ts">
import type { HookTestReqType, HookType } from 'nocodb-sdk'
import { MetaInj, extractSdkResponseErrorMsg, inject, message, onMounted, ref, useI18n, useNuxtApp, watch } from '#imports'
interface Props {
hook: Record<string, any>
hook: HookType
}
const { hook } = defineProps<Props>()
@ -33,10 +34,13 @@ async function loadSampleData() {
async function testWebhook() {
try {
await $api.dbTableWebhook.test(meta.value?.id as string, {
hook,
payload: sampleData.value,
})
await $api.dbTableWebhook.test(
meta.value?.id as string,
{
hook,
payload: sampleData.value,
} as HookTestReqType,
)
// Webhook tested successfully
message.success(t('msg.success.webhookTested'))

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

@ -1,4 +1,4 @@
import type { Actions, State } from './types'
import type { Actions, AppInfo, State } from './types'
import { message, useNuxtApp } from '#imports'
export function useGlobalActions(state: State): Actions {
@ -11,10 +11,8 @@ export function useGlobalActions(state: State): Actions {
state.token.value = null
state.user.value = null
try {
if (state.token.value) {
const nuxtApp = useNuxtApp()
await nuxtApp.$api.auth.signout()
}
const nuxtApp = useNuxtApp()
await nuxtApp.$api.auth.signout()
} catch {}
}
@ -52,14 +50,14 @@ export function useGlobalActions(state: State): Actions {
message.error(err.message || t('msg.error.youHaveBeenSignedOut'))
await signOut()
})
.finally(resolve)
.finally(() => resolve())
})
}
const loadAppInfo = async () => {
try {
const nuxtApp = useNuxtApp()
state.appInfo.value = await nuxtApp.$api.utils.appInfo()
state.appInfo.value = (await nuxtApp.$api.utils.appInfo()) as AppInfo
} catch (e) {
console.error(e)
}

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

@ -11,6 +11,7 @@ import {
getHTMLEncodedText,
inject,
message,
parseProp,
provide,
ref,
storeToRefs,
@ -198,8 +199,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
// set groupingField
groupingFieldColumn.value = !isPublic.value
? (meta.value.columns as ColumnType[]).filter((f) => f.id === kanbanMetaData.value.fk_grp_col_id)[0] || {}
: ((typeof sharedView.value?.meta === 'string' ? JSON.parse(sharedView.value?.meta) : sharedView.value?.meta)
.groupingFieldColumn! as ColumnType)
: (parseProp(sharedView.value?.meta).groupingFieldColumn! as ColumnType)
groupingField.value = groupingFieldColumn.value.title!

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

@ -1,5 +1,5 @@
import type { ComputedRef, Ref } from 'vue'
import type { ColumnType, MapType, PaginatedType, ViewType } from 'nocodb-sdk'
import type { ColumnType, MapType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { IsPublicInj, ref, storeToRefs, useInjectionState, useMetas, useProject } from '#imports'
import type { Row } from '~/lib'
@ -15,7 +15,7 @@ const formatData = (list: Record<string, any>[]) =>
const [useProvideMapViewStore, useMapViewStore] = useInjectionState(
(
meta: Ref<MapType | undefined>,
meta: Ref<(MapType & { id: string }) | undefined>,
viewMeta: Ref<(ViewType | MapType | undefined) & { id: string }> | ComputedRef<(ViewType & { id: string }) | undefined>,
shared = false,
where?: ComputedRef<string | undefined>,
@ -104,7 +104,7 @@ const [useProvideMapViewStore, useMapViewStore] = useInjectionState(
if (currentRow.rowMeta) currentRow.rowMeta.saving = true
try {
const { missingRequiredColumns, insertObj } = await populateInsertObject({
meta: metaValue!,
meta: metaValue as TableType,
ltarState,
getMeta,
row,

4
packages/nc-gui/composables/useMetas.ts

@ -1,6 +1,6 @@
import { message } from 'ant-design-vue'
import type { WatchStopHandle } from 'vue'
import type { TableInfoType, TableType } from 'nocodb-sdk'
import type { TableType } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, storeToRefs, useNuxtApp, useProject, useState, watch } from '#imports'
export function useMetas() {
@ -26,7 +26,7 @@ export function useMetas() {
}
// todo: this needs a proper refactor, arbitrary waiting times are usually not a good idea
const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | TableInfoType | null> => {
const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | null> => {
if (!tableIdOrTitle) return null
/** wait until loading is finished if requesting same meta */
if (!force && loadingState.value[tableIdOrTitle]) {

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

@ -2,6 +2,7 @@ import dayjs from 'dayjs'
import type { ColumnType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import type { AppInfo } from '~/composables/useGlobal'
import { parseProp } from '#imports'
export default function convertCellData(
args: { from: UITypes; to: UITypes; value: any; column: ColumnType; appInfo: AppInfo },
@ -73,7 +74,7 @@ export default function convertCellData(
case UITypes.Attachment: {
let parsedVal
try {
parsedVal = typeof value === 'string' ? JSON.parse(value) : value
parsedVal = parseProp(value)
parsedVal = Array.isArray(parsedVal) ? parsedVal : [parsedVal]
} catch (e) {
throw new Error('Invalid attachment data')
@ -94,7 +95,7 @@ export default function convertCellData(
const attachmentMeta = {
...defaultAttachmentMeta,
...(typeof args.column?.meta === 'string' ? JSON.parse(args.column.meta) : args.column?.meta),
...parseProp(args.column?.meta),
}
const attachments = []

4
packages/nc-gui/composables/useSharedView.ts

@ -10,7 +10,7 @@ import type {
ViewType,
} from 'nocodb-sdk'
import { UITypes, ViewTypes } from 'nocodb-sdk'
import { computed, storeToRefs, useGlobal, useMetas, useNuxtApp, useState } from '#imports'
import { computed, parseProp, storeToRefs, useGlobal, useMetas, useNuxtApp, useState } from '#imports'
export function useSharedView() {
const nestedFilters = ref<(FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string })[]>([])
@ -63,7 +63,7 @@ export function useSharedView() {
},
})
try {
allowCSVDownload.value = (typeof viewMeta.meta === 'string' ? JSON.parse(viewMeta.meta) : viewMeta.meta)?.allowCSVDownload
allowCSVDownload.value = parseProp(viewMeta.meta)?.allowCSVDownload
} catch {
allowCSVDownload.value = false
}

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

@ -1,4 +1,4 @@
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import {
Modal,
@ -40,14 +40,14 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void, baseId?
const createTable = async () => {
if (!sqlUi?.value) return
const columns = sqlUi?.value?.getNewTableColumns().filter((col) => {
const columns = sqlUi?.value?.getNewTableColumns().filter((col: ColumnType) => {
if (col.column_name === 'id' && table.columns.includes('id_ag')) {
Object.assign(col, sqlUi?.value?.getDataTypeForUiType({ uidt: UITypes.ID }, 'AG'))
col.dtxp = sqlUi?.value?.getDefaultLengthForDatatype(col.dt)
col.dtxs = sqlUi?.value?.getDefaultScaleForDatatype(col.dt)
return true
}
return table.columns.includes(col.column_name)
return table.columns.includes(col.column_name!)
})
try {

4
packages/nc-gui/composables/useViewFilters.ts

@ -162,10 +162,9 @@ export function useViewFilters(
const placeholderFilter = (): Filter => {
return {
// TODO: fix type
comparison_op: comparisonOpList(options.value?.[0].uidt as UITypes).filter((compOp) =>
isComparisonOpAllowed({ fk_column_id: options.value?.[0].id }, compOp),
)?.[0].value,
)?.[0].value as FilterType['comparison_op'],
value: '',
status: 'create',
logical_op: 'and',
@ -281,7 +280,6 @@ export function useViewFilters(
comparison: filter.comparison_op,
})
} else {
// todo: return type of dbTableFilter is void?
filters.value[i] = await $api.dbTableFilter.create(view.value.id!, {
...filter,
fk_parent_id: parentId,

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

@ -7,7 +7,7 @@ import type { Row, TabItem } from '~/lib'
export const ActiveCellInj: InjectionKey<Ref<boolean>> = Symbol('active-cell')
export const IsPublicInj: InjectionKey<Ref<boolean>> = Symbol('is-public')
export const RowInj: InjectionKey<Ref<Row>> = Symbol('row')
export const ColumnInj: InjectionKey<Ref<ColumnType & { meta: any }>> = Symbol('column-injection')
export const ColumnInj: InjectionKey<Ref<ColumnType>> = Symbol('column-injection')
export const MetaInj: InjectionKey<ComputedRef<TableType>> = Symbol('meta-injection')
export const TabMetaInj: InjectionKey<ComputedRef<TabItem>> = Symbol('tab-meta-injection')
export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['paginationData']> =

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

@ -1,4 +1,4 @@
import type { FilterType, ViewTypes } from 'nocodb-sdk'
import type { FilterType, MetaType, ViewTypes } from 'nocodb-sdk'
import type { I18n } from 'vue-i18n'
import type { Theme as AntTheme } from 'ant-design-vue/es/config-provider'
import type { UploadFile } from 'ant-design-vue'
@ -21,7 +21,7 @@ export interface ProjectMetaInfo {
Platform?: string
Docker?: boolean
Database?: string
ProjectOnRootDB?: string
ProjectOnRootDB?: boolean
RootDB?: string
PackageVersion?: string
}
@ -79,7 +79,7 @@ export interface TabItem {
viewId?: string
sortsState?: Map<string, any>
filterState?: Map<string, any>
meta?: Record<string, any>
meta?: MetaType
}
export interface SharedViewMeta extends Record<string, any> {

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

@ -102,8 +102,8 @@ async function tryGoogleAuth(api: Api<any>, signIn: Actions['signIn']) {
)
signIn(token)
} catch (e) {
message.error({ content: await extractSdkResponseErrorMsg(e) })
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
const newURL = window.location.href.split('?')[0]

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

@ -75,6 +75,7 @@
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0",
"@types/d3-scale": "^4.0.3",
"@types/dagre": "^0.7.48",
"@types/file-saver": "^2.0.5",
"@types/leaflet": "^1.9.0",
@ -3311,11 +3312,26 @@
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-scale": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz",
"integrity": "sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==",
"dev": true,
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-selection": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.3.tgz",
"integrity": "sha512-Mw5cf6nlW1MlefpD9zrshZ+DAWL4IQ5LnWfRheW6xwsdaWOb6IRRu2H7XPAQcyXEx1D7XQWgdoKR83ui1/HlEA=="
},
"node_modules/@types/d3-time": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
"integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==",
"dev": true
},
"node_modules/@types/d3-zoom": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz",
@ -20686,11 +20702,26 @@
"@types/d3-color": "*"
}
},
"@types/d3-scale": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz",
"integrity": "sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==",
"dev": true,
"requires": {
"@types/d3-time": "*"
}
},
"@types/d3-selection": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.3.tgz",
"integrity": "sha512-Mw5cf6nlW1MlefpD9zrshZ+DAWL4IQ5LnWfRheW6xwsdaWOb6IRRu2H7XPAQcyXEx1D7XQWgdoKR83ui1/HlEA=="
},
"@types/d3-time": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
"integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==",
"dev": true
},
"@types/d3-zoom": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz",

1
packages/nc-gui/package.json

@ -98,6 +98,7 @@
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0",
"@types/d3-scale": "^4.0.3",
"@types/dagre": "^0.7.48",
"@types/file-saver": "^2.0.5",
"@types/leaflet": "^1.9.0",

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

@ -145,7 +145,7 @@ const copyProjectInfo = async () => {
// Copied to clipboard
message.info(t('msg.info.copiedToClipboard'))
}
} catch (e) {
} catch (e: any) {
console.error(e)
message.error(e.message)
}

5
packages/nc-gui/pages/index/index/create-external.vue

@ -1,4 +1,5 @@
<script lang="ts" setup>
import type { ProjectType } from 'nocodb-sdk'
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select'
import type { DefaultConnection, ProjectCreateForm } from '#imports'
import {
@ -233,7 +234,7 @@ const createProject = async () => {
const config = { ...formState.dataSource, connection }
const result = await api.project.create({
const result = (await api.project.create({
title: formState.title,
bases: [
{
@ -244,7 +245,7 @@ const createProject = async () => {
},
],
external: true,
})
})) as Partial<ProjectType>
$e('a:project:create:extdb')

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

@ -2,6 +2,7 @@
import type { Form, Input } from 'ant-design-vue'
import type { RuleObject } from 'ant-design-vue/es/form'
import type { VNodeRef } from '@vue/runtime-core'
import type { ProjectType } from 'nocodb-sdk'
import {
extractSdkResponseErrorMsg,
generateUniqueName,
@ -44,9 +45,9 @@ const createProject = async () => {
try {
creating.value = true
const result = await api.project.create({
const result = (await api.project.create({
title: formState.title,
})
})) as Partial<ProjectType>
await navigateTo(`/nc/${result.id}`)
} catch (e: any) {

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

@ -11,6 +11,7 @@ import {
message,
navigateTo,
onBeforeMount,
parseProp,
projectThemeColors,
ref,
themeV2Colors,
@ -80,7 +81,7 @@ const handleProjectColor = async (projectId: string, color: string) => {
const project: ProjectType = await $api.project.read(projectId)
const meta = project?.meta && typeof project.meta === 'string' ? JSON.parse(project.meta) : project.meta || {}
const meta = parseProp(project?.meta)
await $api.project.update(projectId, {
color,
@ -113,7 +114,7 @@ const handleProjectColor = async (projectId: string, color: string) => {
const getProjectPrimary = (project: ProjectType) => {
if (!project) return
const meta = project.meta && typeof project.meta === 'string' ? JSON.parse(project.meta) : project.meta || {}
const meta = parseProp(project.meta)
return meta.theme?.primaryColor || themeV2Colors['royal-blue'].DEFAULT
}
@ -136,7 +137,7 @@ const copyProjectMeta = async () => {
const aggregatedMetaInfo = await $api.utils.aggregatedMetaInfo()
await copy(JSON.stringify(aggregatedMetaInfo))
message.info('Copied aggregated project meta to clipboard')
} catch (e) {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}

4
packages/nc-gui/store/project.ts

@ -98,7 +98,7 @@ export const useProject = defineStore('projectStore', () => {
async function loadProjectMetaInfo(force?: boolean) {
if (!projectMetaInfo.value || force) {
projectMetaInfo.value = await api.project.metaGet(project.value.id!, {}, {})
projectMetaInfo.value = await api.project.metaGet(project.value.id!, {})
}
}
@ -150,7 +150,7 @@ export const useProject = defineStore('projectStore', () => {
if (data.meta && typeof data.meta === 'string') {
await api.project.update(projectId.value, data)
} else {
await api.project.update(projectId.value, { ...data, meta: JSON.stringify(data.meta) })
await api.project.update(projectId.value, { ...data, meta: stringifyProp(data.meta) })
}
}

4
packages/nc-gui/utils/dataUtils.ts

@ -1,5 +1,5 @@
import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableInfoType, TableType } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
export const extractPkFromRow = (row: Record<string, any>, columns: ColumnType[]) => {
return (
@ -21,7 +21,7 @@ export async function populateInsertObject({
}: {
meta: TableType
ltarState: Record<string, any>
getMeta: (tableIdOrTitle: string, force?: boolean) => Promise<TableType | TableInfoType | null>
getMeta: (tableIdOrTitle: string, force?: boolean) => Promise<TableType | null>
row: Record<string, any>
throwError?: boolean
}) {

20
packages/nc-gui/utils/filterUtils.ts

@ -1,7 +1,7 @@
import { UITypes, isNumericCol, numericUITypes } from 'nocodb-sdk'
import { isNumericCol, numericUITypes, UITypes } from 'nocodb-sdk'
const getEqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType)) {
if (isNumericCol(fieldUiType) || fieldUiType === UITypes.Time) {
return '='
} else if (
[UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord, UITypes.Date, UITypes.DateTime].includes(
@ -14,7 +14,7 @@ const getEqText = (fieldUiType: UITypes) => {
}
const getNeqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType)) {
if (isNumericCol(fieldUiType) || fieldUiType === UITypes.Time) {
return '!='
} else if (
[UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord, UITypes.Date, UITypes.DateTime].includes(
@ -112,6 +112,7 @@ export const comparisonOpList = (
UITypes.Collaborator,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
...numericUITypes,
],
},
@ -126,6 +127,7 @@ export const comparisonOpList = (
UITypes.Collaborator,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
...numericUITypes,
],
},
@ -143,6 +145,7 @@ export const comparisonOpList = (
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
...numericUITypes,
],
},
@ -160,6 +163,7 @@ export const comparisonOpList = (
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
...numericUITypes,
],
},
@ -178,6 +182,7 @@ export const comparisonOpList = (
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
],
},
{
@ -195,6 +200,7 @@ export const comparisonOpList = (
UITypes.Lookup,
UITypes.Date,
UITypes.DateTime,
UITypes.Time,
],
},
{
@ -225,25 +231,25 @@ export const comparisonOpList = (
text: getGtText(fieldUiType),
value: 'gt',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime],
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
},
{
text: getLtText(fieldUiType),
value: 'lt',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime],
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
},
{
text: getGteText(fieldUiType),
value: 'gte',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime],
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
},
{
text: getLteText(fieldUiType),
value: 'lte',
ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime],
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
},
{
text: 'is within',

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

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

17
packages/nc-gui/utils/parseUtils.ts

@ -0,0 +1,17 @@
export function parseProp(v: any): any {
if (!v) return {}
try {
return typeof v === 'string' ? JSON.parse(v) : v
} catch {
return {}
}
}
export function stringifyProp(v: any): string | undefined {
if (!v) return undefined
try {
return typeof v === 'string' ? v : JSON.stringify(v)
} catch {
return '{}'
}
}

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

@ -5,13 +5,13 @@ export const isLTAR = (uidt: string, colOptions: unknown): colOptions is LinkToA
uidt === UITypes.LinkToAnotherRecord
export const isHm = (column: ColumnType) =>
isLTAR(column.uidt, column.colOptions) && column.colOptions.type === RelationTypes.HAS_MANY
isLTAR(column.uidt!, column.colOptions) && column.colOptions.type === RelationTypes.HAS_MANY
export const isMm = (column: ColumnType) =>
isLTAR(column.uidt, column.colOptions) && column.colOptions.type === RelationTypes.MANY_TO_MANY
isLTAR(column.uidt!, column.colOptions) && column.colOptions.type === RelationTypes.MANY_TO_MANY
export const isBt = (column: ColumnType) =>
isLTAR(column.uidt, column.colOptions) && column.colOptions.type === RelationTypes.BELONGS_TO
isLTAR(column.uidt!, column.colOptions) && column.colOptions.type === RelationTypes.BELONGS_TO
export const isLookup = (column: ColumnType) => column.uidt === UITypes.Lookup
export const isRollup = (column: ColumnType) => column.uidt === UITypes.Rollup

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

@ -1702,7 +1702,9 @@ export interface PluginTestReqType {
/** Plugin Title */
title: string;
/** Plugin Input as JSON string */
input: string;
input: string | object;
/** @example Email */
category: string;
}
/**
@ -1780,6 +1782,24 @@ export interface ProjectReqType {
title: string;
}
/**
* Model for Project Update Request
*/
export interface ProjectUpdateReqType {
/**
* Primary Theme Color
* @example #24716E
*/
color?: string;
/** Project Meta */
meta?: MetaType;
/**
* Project Title
* @example My Project
*/
title?: string;
}
/**
* Model for Project User Request
*/
@ -2932,10 +2952,21 @@ export class Api<
* @request POST:/api/v1/db/meta/projects/{projectId}/users
* @response `200` `{
\**
* Success Message
* Success Message for inviting single email
* @example The user has been invited successfully
*\
msg?: string,
\** @example 8354ddba-a769-4d64-8397-eccb2e2b3c06 *\
invite_token?: string,
error?: ({
\** @example w@nocodb.com *\
email?: string,
\** @example <ERROR_MESSAGE> *\
error?: string,
})[],
\** @example w@nocodb.com *\
email?: string,
}` OK
* @response `400` `{
@ -2952,10 +2983,20 @@ export class Api<
this.request<
{
/**
* Success Message
* Success Message for inviting single email
* @example The user has been invited successfully
*/
msg?: string;
/** @example 8354ddba-a769-4d64-8397-eccb2e2b3c06 */
invite_token?: string;
error?: {
/** @example w@nocodb.com */
email?: string;
/** @example <ERROR_MESSAGE> */
error?: string;
}[];
/** @example w@nocodb.com */
email?: string;
},
{
/** @example BadRequest [Error]: <ERROR MESSAGE> */
@ -3909,16 +3950,20 @@ export class Api<
* @name Update
* @summary Update Project
* @request PATCH:/api/v1/db/meta/projects/{projectId}
* @response `200` `void` OK
* @response `200` `number` OK
* @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\
msg: string,
}`
*/
update: (projectId: IdType, data: number, params: RequestParams = {}) =>
update: (
projectId: IdType,
data: ProjectUpdateReqType,
params: RequestParams = {}
) =>
this.request<
void,
number,
{
/** @example BadRequest [Error]: <ERROR MESSAGE> */
msg: string;
@ -3928,6 +3973,7 @@ export class Api<
method: 'PATCH',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),
@ -8324,6 +8370,7 @@ export class Api<
ee?: boolean,
ncAttachmentFieldSize?: number,
ncMaxAttachmentsAllowed?: number,
isCloud?: boolean,
}` OK
* @response `400` `{
@ -8352,6 +8399,7 @@ export class Api<
ee?: boolean;
ncAttachmentFieldSize?: number;
ncMaxAttachmentsAllowed?: number;
isCloud?: boolean;
},
{
/** @example BadRequest [Error]: <ERROR MESSAGE> */

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

@ -48,6 +48,7 @@ export const numericUITypes = [
UITypes.Decimal,
UITypes.Rating,
UITypes.Rollup,
UITypes.Year,
];
export function isNumericCol(

14
packages/nocodb-sdk/src/lib/formulaHelpers.ts

@ -179,7 +179,7 @@ export function jsepTreeToFormula(node) {
if (node.type === 'Literal') {
if (typeof node.value === 'string') {
return String.raw`"${escapeDoubleQuotes(node.value)}"`;
return String.raw`"${escapeLiteral(node.value)}"`;
}
return '' + node.value;
}
@ -214,6 +214,14 @@ export function jsepTreeToFormula(node) {
return '';
}
function escapeDoubleQuotes(v: string) {
return v.replace(/"/g, '\\"');
function escapeLiteral(v: string) {
return (
v
// replace \ to \\
.replace(/\\/g, `\\\\`)
// replace " to \"
.replace(/"/g, `\\"`)
// replace ' to \'
.replace(/'/g, `\\'`)
);
}

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

@ -691,7 +691,9 @@ const parseConditionV2 = async (
qb = qb.whereNull(customWhereClause || field);
if (
!isNumericCol(column.uidt) &&
![UITypes.Date, UITypes.DateTime].includes(column.uidt)
![UITypes.Date, UITypes.DateTime, UITypes.Time].includes(
column.uidt
)
) {
qb = qb.orWhere(field, '');
}
@ -707,7 +709,9 @@ const parseConditionV2 = async (
qb = qb.whereNotNull(customWhereClause || field);
if (
!isNumericCol(column.uidt) &&
![UITypes.Date, UITypes.DateTime].includes(column.uidt)
![UITypes.Date, UITypes.DateTime, UITypes.Time].includes(
column.uidt
)
) {
qb = qb.whereNot(field, '');
}

9
packages/nocodb/src/lib/services/project.svc.ts

@ -11,7 +11,7 @@ import Project from '../models/Project';
import syncMigration from '../meta/helpers/syncMigration';
import { populateMeta, validatePayload } from '../meta/api/helpers';
import { extractPropsAndSanitize } from '../meta/helpers/extractProps';
import type { ProjectReqType } from 'nocodb-sdk';
import type { ProjectReqType, ProjectUpdateReqType } from 'nocodb-sdk';
export async function projectCreate(param: {
project: ProjectReqType;
@ -136,8 +136,13 @@ export function sanitizeProject(project: any) {
export async function projectUpdate(param: {
projectId: string;
project: ProjectReqType;
project: ProjectUpdateReqType;
}) {
validatePayload(
'swagger.json#/components/schemas/ProjectUpdateReq',
param.project
);
const project = await Project.getWithInfo(param.projectId);
const data: Partial<Project> = extractPropsAndSanitize(

10
packages/nocodb/src/lib/version-upgrader/ncFilterUpgrader_0105003.ts

@ -5,7 +5,7 @@ import Filter from '../models/Filter';
import type { NcUpgraderCtx } from './NcUpgrader';
import type NcMetaIO from '../meta/NcMetaIO';
// as of 0.105.3, date / datetime filters include `is like` and `is not like` which are not practical
// as of 0.105.3, year, time, date and datetime filters include `is like` and `is not like` which are not practical
// `removeLikeAndNlikeFilters` in this upgrader is simply to remove them
// besides, `null` and `empty` will be migrated to `blank` in `migrateEmptyAndNullFilters`
@ -19,6 +19,9 @@ import type NcMetaIO from '../meta/NcMetaIO';
// - remove `is like` and `is not like`
// - migrate `null` or `empty` filters to `blank`
// - add `exact date` in comparison_sub_op for existing filters `eq` and `neq`
// - Year / Time columns:
// - remove `is like` and `is not like`
// - migrate `null` or `empty` filters to `blank`
function removeLikeAndNlikeFilters(filter: Filter, ncMeta: NcMetaIO) {
const actions = [];
@ -88,6 +91,11 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
...migrateEmptyAndNullFilters(filter, ncMeta),
...migrateEqAndNeqFilters(filter, ncMeta),
]);
} else if ([UITypes.Time, UITypes.Year].includes(col.uidt)) {
await Promise.all([
...removeLikeAndNlikeFilters(filter, ncMeta),
...migrateEmptyAndNullFilters(filter, ncMeta),
]);
}
}
}

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

@ -1461,16 +1461,75 @@
"x-stoplight": {
"id": "waau9tvy75zsd"
},
"description": "Success Message",
"description": "Success Message for inviting single email",
"example": "The user has been invited successfully"
},
"invite_token": {
"type": "string",
"x-stoplight": {
"id": "yx0s35u8ds3p7"
},
"example": "8354ddba-a769-4d64-8397-eccb2e2b3c06"
},
"error": {
"type": "array",
"x-stoplight": {
"id": "yhfi6wzhr6zr1"
},
"items": {
"x-stoplight": {
"id": "ce0hlv3d0f96j"
},
"type": "object",
"properties": {
"email": {
"type": "string",
"x-stoplight": {
"id": "dgnh01j4lxvl1"
},
"example": "w@nocodb.com"
},
"error": {
"type": "string",
"x-stoplight": {
"id": "7dgttqiijg8no"
},
"example": "<ERROR_MESSAGE>"
}
}
}
},
"email": {
"type": "string",
"x-stoplight": {
"id": "08pqst2q30vot"
},
"example": "w@nocodb.com"
}
}
},
"examples": {
"Example 1": {
"Inviting a user without any errors": {
"value": {
"msg": "The user has been invited successfully"
}
},
"Inviting a user but invitation email failed to send": {
"value": {
"invite_token": "8354ddba-a769-4d64-8397-eccb2e2b3c06",
"email": "w@nocodb.com"
}
},
"Inviting multiple users": {
"value": {
"invite_token": "8354ddba-a769-4d64-8397-eccb2e2b3c06",
"error": [
{
"email": "w@nocodb.com",
"error": "<ERROR MESSAGE>"
}
]
}
}
}
}
@ -2149,7 +2208,20 @@
"operationId": "project-update",
"responses": {
"200": {
"description": "OK"
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "number",
"example": 1
},
"examples": {
"Example 1": {
"value": 1
}
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
@ -2160,11 +2232,15 @@
"content": {
"application/json": {
"schema": {
"type": "number"
"$ref": "#/components/schemas/ProjectUpdateReq"
},
"examples": {
"Example 1": {
"value": 1
"value": {
"color": "#24716E",
"meta": null,
"title": "My Project"
}
}
}
}
@ -12211,6 +12287,15 @@
"application/json": {
"schema": {
"$ref": "#/components/schemas/PluginTestReq"
},
"examples": {
"Example 1": {
"value": {
"title": "Plugin Foo",
"input": "{\"bucket\":\"my-bucket\",\"region\":\"us-west-004\",\"access_key\":\"redacted\",\"access_secret\":\"redacted\"}",
"category": "Email"
}
}
}
}
}
@ -12628,6 +12713,12 @@
},
"ncMaxAttachmentsAllowed": {
"type": "number"
},
"isCloud": {
"type": "boolean",
"x-stoplight": {
"id": "bstdkpky2131f"
}
}
}
},
@ -12650,7 +12741,8 @@
"ncSiteUrl": "http://localhost:8080",
"ee": false,
"ncAttachmentFieldSize": 20971520,
"ncMaxAttachmentsAllowed": 10
"ncMaxAttachmentsAllowed": 10,
"isCloud": false
}
}
}
@ -17217,7 +17309,8 @@
"examples": [
{
"title": "Plugin Foo",
"input": "{\"bucket\":\"my-bucket\",\"region\":\"us-west-004\",\"access_key\":\"redacted\",\"access_secret\":\"redacted\"}"
"input": "{\"bucket\":\"my-bucket\",\"region\":\"us-west-004\",\"access_key\":\"redacted\",\"access_secret\":\"redacted\"}",
"category": "Email"
}
],
"title": "Plugin Test Request Model",
@ -17235,12 +17328,20 @@
"description": "Plugin Input as JSON string"
},
{
"description": "Plugin Input"
"description": "Plugin Input",
"type": "object"
}
]
},
"category": {
"x-stoplight": {
"id": "rg3i3ov9rs6d0"
},
"type": "string",
"example": "Email"
}
},
"required": ["title", "input"]
"required": ["title", "input", "category"]
},
"Project": {
"description": "Model for Project",
@ -17472,6 +17573,43 @@
"title": "Project Request Model",
"type": "object"
},
"ProjectUpdateReq": {
"description": "Model for Project Update Request",
"x-stoplight": {
"id": "4zgrec70wyz4c"
},
"examples": [
{
"color": "#24716E",
"meta": null,
"title": "My Project"
}
],
"title": "Project Update Request Model",
"type": "object",
"properties": {
"color": {
"description": "Primary Theme Color",
"example": "#24716E",
"maxLength": 50,
"type": "string"
},
"meta": {
"$ref": "#/components/schemas/Meta",
"description": "Project Meta",
"x-stoplight": {
"id": "m05w9sbwqgul3"
}
},
"title": {
"description": "Project Title",
"example": "My Project",
"maxLength": 128,
"minLength": 1,
"type": "string"
}
}
},
"ProjectUserReq": {
"description": "Model for Project User Request",
"examples": [

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

@ -90,6 +90,26 @@ export class ToolbarFilterPage extends BasePage {
if (value) {
let fillFilter: any = null;
switch (dataType) {
case UITypes.Year:
await this.get().locator('.nc-filter-value-select').click();
await this.rootPage.locator(`.ant-picker-dropdown:visible`);
await this.rootPage.locator(`.ant-picker-cell-inner:has-text("${value}")`).click();
break;
case UITypes.Time:
// eslint-disable-next-line no-case-declarations
const time = value.split(':');
await this.get().locator('.nc-filter-value-select').click();
await this.rootPage.locator(`.ant-picker-dropdown:visible`);
await this.rootPage
.locator(`.ant-picker-time-panel-column:nth-child(1)`)
.locator(`.ant-picker-time-panel-cell:has-text("${time[0]}")`)
.click();
await this.rootPage
.locator(`.ant-picker-time-panel-column:nth-child(2)`)
.locator(`.ant-picker-time-panel-cell:has-text("${time[1]}")`)
.click();
await this.rootPage.locator(`.ant-btn-primary:has-text("Ok")`).click();
break;
case UITypes.Date:
if (opSubType === 'exact date') {
await this.get().locator('.nc-filter-value-select').click();
@ -104,7 +124,6 @@ export class ToolbarFilterPage extends BasePage {
});
await this.toolbar.parent.dashboard.waitForLoaderToDisappear();
await this.toolbar.parent.waitLoading();
break;
}
break;
case UITypes.Duration:

28
tests/playwright/setup/xcdb-records.ts

@ -1,6 +1,6 @@
import { ColumnType, UITypes } from 'nocodb-sdk';
const rowMixedValue = (column: ColumnType, index: number) => {
const rowMixedValue = (column: ColumnType, index: number, db?: string) => {
// Array of country names
const countries = [
'Afghanistan',
@ -36,11 +36,33 @@ const rowMixedValue = (column: ColumnType, index: number) => {
null,
];
// compute timezone offset
const offset = new Date().getTimezoneOffset();
const timezoneOffset =
(db === 'mysql' ? (offset < 0 ? '+' : '-') : offset <= 0 ? '+' : '-') +
String(Math.abs(Math.round(offset / 60))).padStart(2, '0') +
':' +
String(Math.abs(offset % 60)).padStart(2, '0');
// Array of random integers, not more than 10000
const numbers = [33, null, 456, 333, 267, 34, 8754, 3234, 44, 33, null];
const decimals = [33.3, 456.34, 333.3, null, 267.5674, 34.0, 8754.0, 3234.547, 44.2647, 33.98, null];
const duration = [60, 120, 180, 3600, 3660, 3720, null, 3780, 60, 120, null];
const time = [
`1999-01-01 02:02:00${timezoneOffset}`,
`1999-01-01 20:20:20${timezoneOffset}`,
`1999-01-01 04:04:00${timezoneOffset}`,
`1999-01-01 02:02:00${timezoneOffset}`,
`1999-01-01 20:20:20${timezoneOffset}`,
`1999-01-01 18:18:18${timezoneOffset}`,
null,
`1999-01-01 02:02:00${timezoneOffset}`,
`1999-01-01 20:20:20${timezoneOffset}`,
`1999-01-01 18:18:18${timezoneOffset}`,
null,
];
const rating = [0, 1, 2, 3, null, 0, 4, 5, 0, 1, null];
const years = [2023, null, 1956, 2023, 1967, 2024, 1954, 1924, 2044, 1923, null];
// Array of random sample email strings (not more than 100 characters)
const emails = [
@ -131,6 +153,10 @@ const rowMixedValue = (column: ColumnType, index: number) => {
return singleSelect[index % singleSelect.length];
case UITypes.MultiSelect:
return multiSelect[index % multiSelect.length];
case UITypes.Year:
return years[index % years.length];
case UITypes.Time:
return time[index % time.length];
default:
return `test-${index}`;
}

33
tests/playwright/tests/filters.spec.ts

@ -13,11 +13,13 @@ let records: Record<string, any>;
const skipList = {
Number: ['is null', 'is not null'],
Year: ['is null', 'is not null'],
Decimal: ['is null', 'is not null'],
Percent: ['is null', 'is not null'],
Currency: ['is null', 'is not null'],
Rating: ['is null', 'is not null', 'is blank', 'is not blank'],
Duration: ['is null', 'is not null'],
Time: ['is null', 'is not null'],
SingleLineText: [],
MultiLineText: [],
Email: [],
@ -126,6 +128,13 @@ test.describe('Filter Tests: Numerical', () => {
isLikeStringDerived = parseInt(isLikeString.split(':')[0]) * 3600 + parseInt(isLikeString.split(':')[1]) * 60;
}
// convert r[Time] in format 2021-01-01 00:00:00+05.30 to 00:00:00
if (dataType === 'Time') {
records.list.forEach(r => {
if (r[dataType]?.length > 8) r[dataType] = r[dataType]?.split(' ')[1]?.split(/[+-]/)[0];
});
}
const filterList = [
{
op: '=',
@ -150,12 +159,12 @@ test.describe('Filter Tests: Numerical', () => {
{
op: 'is blank',
value: '',
rowCount: records.list.filter(r => r[dataType] === null).length,
rowCount: records.list.filter(r => r[dataType] === null || r[dataType] === undefined).length,
},
{
op: 'is not blank',
value: '',
rowCount: records.list.filter(r => r[dataType] !== null).length,
rowCount: records.list.filter(r => r[dataType] !== null && r[dataType] !== undefined).length,
},
{
op: '>',
@ -255,6 +264,16 @@ test.describe('Filter Tests: Numerical', () => {
title: 'Rating',
uidt: UITypes.Rating,
},
{
column_name: 'Year',
title: 'Year',
uidt: UITypes.Year,
},
{
column_name: 'Time',
title: 'Time',
uidt: UITypes.Time,
},
];
try {
@ -274,6 +293,8 @@ test.describe('Filter Tests: Numerical', () => {
Percent: rowMixedValue(columns[4], i),
Duration: rowMixedValue(columns[5], i),
Rating: rowMixedValue(columns[6], i),
Year: rowMixedValue(columns[7], i),
Time: rowMixedValue(columns[8], i, context.dbType),
};
rowAttributes.push(row);
}
@ -308,6 +329,14 @@ test.describe('Filter Tests: Numerical', () => {
test('Filter: Duration', async () => {
await numBasedFilterTest('Duration', '00:01', '01:03');
});
test('Filter: Year', async () => {
await numBasedFilterTest('Year', '2023', '2024');
});
test('Filter: Time', async () => {
await numBasedFilterTest('Time', '02:02:00', '04:04:00');
});
});
// Text based filters

Loading…
Cancel
Save