Browse Source

Merge branch 'develop' into l10n_develop_2

pull/5321/head
Raju Udava 2 years ago committed by GitHub
parent
commit
88692a6580
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 2
      packages/nc-gui/components.d.ts
  3. 9
      packages/nc-gui/components/account/License.vue
  4. 4
      packages/nc-gui/components/account/SignupSettings.vue
  5. 14
      packages/nc-gui/components/account/Token.vue
  6. 12
      packages/nc-gui/components/account/UserList.vue
  7. 13
      packages/nc-gui/components/account/UsersModal.vue
  8. 13
      packages/nc-gui/components/cell/Checkbox.vue
  9. 4
      packages/nc-gui/components/cell/Currency.vue
  10. 3
      packages/nc-gui/components/cell/DatePicker.vue
  11. 5
      packages/nc-gui/components/cell/DateTimePicker.vue
  12. 7
      packages/nc-gui/components/cell/Decimal.vue
  13. 3
      packages/nc-gui/components/cell/Duration.vue
  14. 9
      packages/nc-gui/components/cell/Float.vue
  15. 2
      packages/nc-gui/components/cell/GeoData.vue
  16. 9
      packages/nc-gui/components/cell/Integer.vue
  17. 3
      packages/nc-gui/components/cell/MultiSelect.vue
  18. 4
      packages/nc-gui/components/cell/Rating.vue
  19. 8
      packages/nc-gui/components/cell/Url.vue
  20. 3
      packages/nc-gui/components/cell/attachment/utils.ts
  21. 10
      packages/nc-gui/components/dashboard/TreeView.vue
  22. 4
      packages/nc-gui/components/dashboard/settings/AuditTab.vue
  23. 31
      packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue
  24. 2
      packages/nc-gui/components/dlg/TableRename.vue
  25. 4
      packages/nc-gui/components/dlg/ViewCreate.vue
  26. 4
      packages/nc-gui/components/erd/View.vue
  27. 2
      packages/nc-gui/components/general/TableIcon.vue
  28. 2
      packages/nc-gui/components/smartsheet/ApiSnippet.vue
  29. 32
      packages/nc-gui/components/smartsheet/Form.vue
  30. 14
      packages/nc-gui/components/smartsheet/Toolbar.vue
  31. 6
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  32. 2
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  33. 11
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  34. 4
      packages/nc-gui/components/smartsheet/sidebar/MenuBottom.vue
  35. 5
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  36. 2
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  37. 3
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  38. 12
      packages/nc-gui/components/smartsheet/sidebar/toolbar/BetaFeatureToggle.vue
  39. 15
      packages/nc-gui/components/smartsheet/sidebar/toolbar/GeodataSwitcher.vue
  40. 2
      packages/nc-gui/components/smartsheet/sidebar/toolbar/index.vue
  41. 9
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  42. 4
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  43. 4
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  44. 7
      packages/nc-gui/components/smartsheet/toolbar/FilterInput.vue
  45. 165
      packages/nc-gui/components/smartsheet/toolbar/QrScannerButton.vue
  46. 8
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  47. 5
      packages/nc-gui/components/smartsheet/toolbar/SharedViewList.vue
  48. 4
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  49. 8
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  50. 6
      packages/nc-gui/components/tabs/auth/UserManagement.vue
  51. 4
      packages/nc-gui/components/tabs/auth/user-management/ShareBase.vue
  52. 16
      packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue
  53. 11
      packages/nc-gui/components/virtual-cell/QrCode.vue
  54. 2
      packages/nc-gui/components/virtual-cell/barcode/Barcode.vue
  55. 26
      packages/nc-gui/components/webhook/Editor.vue
  56. 4
      packages/nc-gui/components/webhook/List.vue
  57. 14
      packages/nc-gui/components/webhook/Test.vue
  58. 22
      packages/nc-gui/composables/useBetaFeatureToggle.ts
  59. 18
      packages/nc-gui/composables/useGlobal/actions.ts
  60. 1
      packages/nc-gui/composables/useGlobal/state.ts
  61. 2
      packages/nc-gui/composables/useGlobal/types.ts
  62. 4
      packages/nc-gui/composables/useKanbanViewStore.ts
  63. 14
      packages/nc-gui/composables/useMapViewDataStore.ts
  64. 4
      packages/nc-gui/composables/useMetas.ts
  65. 5
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  66. 5
      packages/nc-gui/composables/useSharedFormViewStore.ts
  67. 4
      packages/nc-gui/composables/useSharedView.ts
  68. 6
      packages/nc-gui/composables/useTable.ts
  69. 2
      packages/nc-gui/composables/useViewData.ts
  70. 4
      packages/nc-gui/composables/useViewFilters.ts
  71. 4
      packages/nc-gui/composables/useViews.ts
  72. 2
      packages/nc-gui/context/index.ts
  73. 2
      packages/nc-gui/lang/en.json
  74. 6
      packages/nc-gui/lib/types.ts
  75. 4
      packages/nc-gui/middleware/auth.global.ts
  76. 834
      packages/nc-gui/package-lock.json
  77. 5
      packages/nc-gui/package.json
  78. 32
      packages/nc-gui/pages/[projectType]/[projectId]/index.vue
  79. 12
      packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue
  80. 116
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue
  81. 5
      packages/nc-gui/pages/index/index/create-external.vue
  82. 5
      packages/nc-gui/pages/index/index/create.vue
  83. 7
      packages/nc-gui/pages/index/index/index.vue
  84. 7
      packages/nc-gui/plugins/vue-qr-code-scanner.ts
  85. 4
      packages/nc-gui/store/project.ts
  86. 4
      packages/nc-gui/utils/dataUtils.ts
  87. 20
      packages/nc-gui/utils/filterUtils.ts
  88. 1
      packages/nc-gui/utils/index.ts
  89. 17
      packages/nc-gui/utils/parseUtils.ts
  90. 6
      packages/nc-gui/utils/virtualCell.ts
  91. 65
      packages/nocodb-sdk/src/lib/Api.ts
  92. 1
      packages/nocodb-sdk/src/lib/UITypes.ts
  93. 2
      packages/nocodb-sdk/src/lib/columnRules/QrAndBarcodeRules.ts
  94. 14
      packages/nocodb-sdk/src/lib/formulaHelpers.ts
  95. 8
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts
  96. 4
      packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
  97. 16
      packages/nocodb/src/lib/migrations/v2/nc_026_add_enable_scanner_in_form_columns_meta_table.ts
  98. 3
      packages/nocodb/src/lib/models/FormViewColumn.ts
  99. 9
      packages/nocodb/src/lib/services/project.svc.ts
  100. 10
      packages/nocodb/src/lib/version-upgrader/ncFilterUpgrader_0105003.ts
  101. Some files were not shown because too many files have changed in this diff Show More

3
.gitignore vendored

@ -88,3 +88,6 @@ mongod
#=========
nc_minimal_dbs/
test_noco.db
# ngrok config
httpbin

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

@ -147,6 +147,7 @@ declare module '@vue/runtime-core' {
MdiCloseCircleOutline: typeof import('~icons/mdi/close-circle-outline')['default']
MdiCloseThick: typeof import('~icons/mdi/close-thick')['default']
MdiCodeJson: typeof import('~icons/mdi/code-json')['default']
MdiCodeScan: typeof import('~icons/mdi/code-scan')['default']
MdiCodeTags: typeof import('~icons/mdi/code-tags')['default']
MdiCog: typeof import('~icons/mdi/cog')['default']
MdiCommentTextOutline: typeof import('~icons/mdi/comment-text-outline')['default']
@ -220,6 +221,7 @@ declare module '@vue/runtime-core' {
MdiPlusCircleOutline: typeof import('~icons/mdi/plus-circle-outline')['default']
MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default']
MdiPlusThick: typeof import('~icons/mdi/plus-thick')['default']
MdiQrcodeScan: typeof import('~icons/mdi/qrcode-scan')['default']
MdiReddit: typeof import('~icons/mdi/reddit')['default']
MdiRefresh: typeof import('~icons/mdi/refresh')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']

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 = []

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

@ -14,6 +14,7 @@ import {
extractSdkResponseErrorMsg,
isDrawerOrModalExist,
isMac,
parseProp,
reactive,
ref,
resolveComponent,
@ -32,6 +33,8 @@ import {
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large'
const { isMobileMode } = useGlobal()
const { addTab, updateTab } = useTabs()
const { $api, $e } = useNuxtApp()
@ -312,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 })
@ -324,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))
}
}
@ -1018,9 +1021,10 @@ const setIcon = async (icon: string, table: TableType) => {
<LazyGeneralHelpAndSupport class="color-transition px-2 text-gray-500 cursor-pointer select-none hover:text-accent" />
<GeneralJoinCloud class="color-transition px-2 text-gray-500 cursor-pointer select-none hover:text-accent" />
<GeneralJoinCloud v-if="!isMobileMode" class="color-transition px-2 text-gray-500 cursor-pointer select-none hover:text-accent" />
<GithubButton
v-if="!isMobileMode"
class="ml-2 py-1"
href="https://github.com/nocodb/nocodb"
data-icon="octicon-star"

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

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

@ -99,6 +99,8 @@ const activeRow = ref('')
const { t } = useI18n()
const { betaFeatureToggleState } = useBetaFeatureToggle()
const updateView = useDebounceFn(
() => {
if ((formViewData.value?.subheading?.length || 0) > 255) {
@ -361,6 +363,10 @@ function handleMouseUp(col: Record<string, any>, hiddenColIndex: number) {
}
}
const columnSupportsScanning = (elementType: UITypes) =>
betaFeatureToggleState.show &&
[UITypes.SingleLineText, UITypes.Number, UITypes.Email, UITypes.URL, UITypes.LongText].includes(elementType)
onClickOutside(draggableRef, () => {
activeRow.value = ''
})
@ -616,6 +622,30 @@ watch(view, (nextView) => {
/>
</div>
<a-form-item v-if="columnSupportsScanning(element.uidt)" class="my-0 w-1/2 !mb-1">
<div class="flex gap-2 items-center">
<span
class="text-gray-500 mr-2 nc-form-input-required"
data-testid="nc-form-input-enable-scanner"
@click="
() => {
element.general.enable_scanner = !element.general.enable_scanner
updateColMeta(element)
}
"
>
{{ $t('general.enableScanner') }}
</span>
<a-switch
v-model:checked="element.enable_scanner"
v-e="['a:form-view:field:mark-enable-scaner']"
size="small"
@change="updateColMeta(element)"
/>
</div>
</a-form-item>
<a-form-item class="my-0 w-1/2 !mb-1">
<a-input
v-model:value="element.label"
@ -827,7 +857,7 @@ watch(view, (nextView) => {
@apply px-4 min-h-[75px] w-full h-full;
.nc-attachment {
@apply md:(w-[50px] h-[50px]) lg:(w-[75px] h-[75px]) min-h-[50px] min-w-[50px];
@apply md: (w-[50px] h-[50px]) lg:(w-[75px] h-[75px]) min-h-[50px] min-w-[50px];
}
.nc-attachment-cell-dropzone {

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

@ -5,6 +5,8 @@ const { isGrid, isForm, isGallery, isKanban, isMap, isSqlView } = useSmartsheetS
const isPublic = inject(IsPublicInj, ref(false))
const { isMobileMode } = useGlobal()
const { isUIAllowed } = useUIPermission()
const { isOpen } = useSidebar('nc-right-sidebar')
@ -14,7 +16,8 @@ const { allowCSVDownload } = useSharedView()
<template>
<div
class="nc-table-toolbar w-full py-1 flex gap-2 items-center h-[var(--toolbar-height)] px-2 border-b overflow-x-hidden"
class="nc-table-toolbar w-full py-1 flex gap-2 items-center px-2 border-b overflow-x-hidden"
:class="{ 'nc-table-toolbar-mobile': isMobileMode, 'h-[var(--toolbar-height)]': !isMobileMode }"
style="z-index: 7"
>
<LazySmartsheetToolbarViewActions
@ -41,8 +44,10 @@ const { allowCSVDownload } = useSharedView()
<LazySmartsheetToolbarShareView v-if="(isForm || isGrid || isKanban || isGallery || isMap) && !isPublic" />
<LazySmartsheetToolbarQrScannerButton v-if="isMobileMode && (isGrid || isKanban || isGallery)" />
<LazySmartsheetToolbarExport v-if="(!isPublic && !isUIAllowed('dataInsert')) || (isPublic && allowCSVDownload)" />
<div class="flex-1" />
<div v-if="!isMobileMode" class="flex-1" />
<LazySmartsheetToolbarReload v-if="!isPublic && !isForm" />
@ -51,7 +56,7 @@ const { allowCSVDownload } = useSharedView()
<LazySmartsheetToolbarSearchData v-if="(isGrid || isGallery || isKanban) && !isPublic" class="shrink mx-2" />
<template v-if="!isOpen && !isPublic">
<div class="border-l-1 pl-3">
<div class="border-l-1 pl-3 nc-views-show-sidebar-button" :class="{ 'ml-auto': isMobileMode }">
<LazySmartsheetSidebarToolbarToggleDrawer class="mr-2" />
</div>
</template>
@ -66,4 +71,7 @@ const { allowCSVDownload } = useSharedView()
.nc-table-toolbar {
border-color: #f0f0f0 !important;
}
.nc-table-toolbar-mobile {
@apply flex-wrap h-auto py-2;
}
</style>

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

@ -41,6 +41,8 @@ const { $e } = useNuxtApp()
const { appInfo } = useGlobal()
const { betaFeatureToggleState } = useBetaFeatureToggle()
const meta = inject(MetaInj, ref())
const isForm = inject(IsFormInj, ref(false))
@ -57,8 +59,8 @@ const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const onlyNameUpdateOnEditColumns = [UITypes.LinkToAnotherRecord, UITypes.Lookup, UITypes.Rollup]
const geoDataToggleCondition = (t) => {
return geodataToggleState.show ? geodataToggleState.show : !t.name.includes(UITypes.GeoData)
const geoDataToggleCondition = (t: { name: UITypes }) => {
return betaFeatureToggleState.show ? betaFeatureToggleState.show : !t.name.includes(UITypes.GeoData)
}
const uiTypesOptions = computed<typeof uiTypes>(() => {

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

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

@ -12,6 +12,8 @@ const { $e } = useNuxtApp()
const { isSqlView } = useSmartsheetStoreOrThrow()
const { betaFeatureToggleState } = useBetaFeatureToggle()
function onOpenModal(type: ViewTypes, title = '') {
$e('c:view:create', { view: type })
emits('openModal', { type, title })
@ -113,7 +115,7 @@ function onOpenModal(type: ViewTypes, title = '') {
</a-tooltip>
</a-menu-item>
<a-menu-item
v-if="geodataToggleState.show"
v-if="betaFeatureToggleState.show"
key="map"
class="group !flex !items-center !my-0 !h-2.5rem nc-create-map-view"
@click="onOpenModal(ViewTypes.MAP)"

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

2
packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -33,7 +33,7 @@ const { $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const isLocked = inject(IsLockedInj)
const isLocked = inject(IsLockedInj, ref(false))
/** Is editing the view name enabled */
let isEditing = $ref<boolean>(false)

3
packages/nc-gui/components/smartsheet/sidebar/index.vue

@ -1,5 +1,6 @@
<script setup lang="ts">
import type { ViewType, ViewTypes } from 'nocodb-sdk'
import type { Ref } from 'vue'
import {
ActiveViewInj,
MetaInj,
@ -48,7 +49,7 @@ const { isOpen } = useSidebar('nc-right-sidebar')
const sidebarCollapsed = computed(() => !isOpen.value)
/** Sidebar ref */
const sidebar = ref()
const sidebar: Ref<Element | null> = ref(null)
/** Watch route param and change active view based on `viewTitle` */
watch(

12
packages/nc-gui/components/smartsheet/sidebar/toolbar/BetaFeatureToggle.vue

@ -0,0 +1,12 @@
<script setup lang="ts">
const { toggleBetaFeature } = useBetaFeatureToggle()
</script>
<template>
<a-tooltip placement="bottomRight">
<template #title>
<span> Toggle Beta Features </span>
</template>
<mdi-test-tube class="cursor-pointer" data-testid="beta-feature-toggle-icon" @click="toggleBetaFeature" />
</a-tooltip>
</template>

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

@ -1,15 +0,0 @@
<script setup lang="ts">
function toggleGeodataFeature() {
geodataToggleState.show = !geodataToggleState.show
localStorage.setItem('geodataToggleState', JSON.stringify(geodataToggleState.show))
}
</script>
<template>
<a-tooltip placement="bottomRight">
<template #title>
<span> Toggle GeoData </span>
</template>
<mdi-map-marker class="cursor-pointer" data-testid="toggle-geodata-feature-icon" @click="toggleGeodataFeature" />
</a-tooltip>
</template>

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

@ -30,7 +30,7 @@ const onClick = () => {
<div class="dot" />
<LazySmartsheetSidebarToolbarGeodataSwitcher />
<LazySmartsheetSidebarToolbarBetaFeatureToggle />
<div class="dot" />
</template>

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 {

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

@ -21,7 +21,7 @@ const activeView = inject(ActiveViewInj, ref())
const isPublic = inject(IsPublicInj, ref(false))
const { filterAutoSave } = useGlobal()
const { filterAutoSave, isMobileMode } = useGlobal()
const filterComp = ref<typeof ColumnFilter>()
@ -76,7 +76,7 @@ useMenuCloseOnEsc(open)
<div class="flex items-center gap-1">
<MdiFilterOutline />
<!-- Filter -->
<span class="text-capitalize !text-xs font-weight-normal">{{ $t('activity.filter') }}</span>
<span v-if="!isMobileMode" class="text-capitalize !text-xs font-weight-normal">{{ $t('activity.filter') }}</span>
<MdiMenuDown class="text-grey" />
<span v-if="filtersLength" class="nc-count-badge">{{ filtersLength }}</span>

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

@ -31,6 +31,8 @@ const reloadViewMetaHook = inject(ReloadViewMetaHookInj, undefined)!
const rootFields = inject(FieldsInj)
const { isMobileMode } = useGlobal()
const isLocked = inject(IsLockedInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false))
@ -157,7 +159,7 @@ useMenuCloseOnEsc(open)
<MdiEyeOffOutline />
<!-- Fields -->
<span class="text-capitalize !text-xs font-weight-normal">{{ $t('objects.fields') }}</span>
<span v-if="!isMobileMode" class="text-capitalize !text-xs font-weight-normal">{{ $t('objects.fields') }}</span>
<MdiMenuDown class="text-grey" />

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]

165
packages/nc-gui/components/smartsheet/toolbar/QrScannerButton.vue

@ -0,0 +1,165 @@
<script setup lang="ts">
import type { SelectProps } from 'ant-design-vue'
import { ref } from 'vue'
import { StreamBarcodeReader } from 'vue-barcode-reader'
import type { ColumnType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { NOCO, storeToRefs } from '#imports'
const meta = inject(MetaInj, ref())
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const { $api } = useNuxtApp()
const { project } = storeToRefs(useProject())
const { isMobileMode } = useGlobal()
const view = inject(ActiveViewInj, ref())
const fieldOptionsOfSupportedColumnsToScanFor = computed<SelectProps['options']>(
() =>
meta?.value
?.columns!.filter((column) => column.uidt && [UITypes.QrCode, UITypes.Barcode].includes(column.uidt))
.map((field) => {
return {
value: field.id,
label: field.title,
}
}) || [],
)
const getColumnToSearchForByBarOrQrCodeColumnId = (columnId: string): ColumnType => {
const qrOrBarcodeColumn = meta.value?.columns?.find((column) => column.id === columnId)
if (!qrOrBarcodeColumn) {
throw new Error('QrCode or BarCode Column not found')
}
let columnIdToSearchFor: string
if (qrOrBarcodeColumn.uidt === UITypes.QrCode) {
columnIdToSearchFor = (qrOrBarcodeColumn.colOptions as any).fk_qr_value_column_id
} else if (qrOrBarcodeColumn.uidt === UITypes.Barcode) {
columnIdToSearchFor = (qrOrBarcodeColumn.colOptions as any).fk_barcode_value_column_id
} else {
throw new Error('Column to scan for is not of supported type')
}
const columnToSearchFor = meta.value?.columns?.find((column) => column.id === columnIdToSearchFor)
if (!columnToSearchFor) {
throw new Error('Column to search for not found')
}
return columnToSearchFor
}
const showCodeScannerOverlay = ref(false)
const idOfSelectedColumnToScanFor = ref('')
const lastScannedCode = ref('')
watch(fieldOptionsOfSupportedColumnsToScanFor, () => {
if (fieldOptionsOfSupportedColumnsToScanFor.value?.every((option) => option.value !== idOfSelectedColumnToScanFor.value)) {
idOfSelectedColumnToScanFor.value = ''
}
})
const scannerIsReady = ref(false)
const onLoaded = async () => {
scannerIsReady.value = true
}
const showScannerField = computed(() => scannerIsReady.value && idOfSelectedColumnToScanFor.value !== '')
const showPleaseSelectColumnMessage = computed(() => !idOfSelectedColumnToScanFor.value)
const showScannerIsLoadingMessage = computed(() => !!idOfSelectedColumnToScanFor.value && !scannerIsReady.value)
const onDecode = async (codeValue: string) => {
if (!showScannerField.value || codeValue === lastScannedCode.value) {
return
}
try {
const selectedColumnToScanFor = getColumnToSearchForByBarOrQrCodeColumnId(idOfSelectedColumnToScanFor.value)
const whereClause = `(${selectedColumnToScanFor?.title},eq,${codeValue})`
const foundRowsForCode = (
await $api.dbViewRow.list(NOCO, project.value.id!, meta.value!.id!, view.value!.title!, {
where: whereClause,
})
).list
if (foundRowsForCode.length !== 1) {
showCodeScannerOverlay.value = true
lastScannedCode.value = codeValue
setTimeout(() => {
lastScannedCode.value = ''
}, 4000)
if (foundRowsForCode.length === 0) {
message.info(t('msg.info.codeScanner.noRowFoundForCode'))
}
if (foundRowsForCode.length > 1) {
message.warn(t('msg.info.codeScanner.moreThanOneRowFoundForCode'))
showCodeScannerOverlay.value = false
lastScannedCode.value = ''
}
return
}
showCodeScannerOverlay.value = false
lastScannedCode.value = ''
const primaryKeyValueForFoundRow = extractPkFromRow(foundRowsForCode[0], meta!.value!.columns!)
router.push({
query: {
...route.query,
rowId: primaryKeyValueForFoundRow,
},
})
} catch (error) {
console.error(error)
}
}
</script>
<template>
<div>
<a-button class="nc-btn-find-row-by-scan nc-toolbar-btn" @click="showCodeScannerOverlay = true">
<div class="flex items-center gap-1">
<MdiQrcodeScan />
<span v-if="!isMobileMode" class="!text-xs font-weight-normal"> {{ $t('activity.findRowByCodeScan') }}</span>
</div>
</a-button>
<a-modal
v-model:visible="showCodeScannerOverlay"
class="nc-overlay-find-row-by-scan"
:closable="false"
width="28rem"
centered
:footer="null"
wrap-class-name="nc-modal-generate-token"
destroy-on-close
@cancel="scannerIsReady = false"
>
<div class="relative flex flex-col h-full">
<div class="text-left text-wrap mt-2 text-xl mb-4">{{ $t('title.findRowByScanningCode') }}</div>
<a-form-item :label="$t('labels.columnToScanFor')" class="nc-dropdown-scanner-column-id">
<a-select
v-model:value="idOfSelectedColumnToScanFor"
class="w-full"
:options="fieldOptionsOfSupportedColumnsToScanFor"
/>
</a-form-item>
<div>
<StreamBarcodeReader v-show="showScannerField" @decode="onDecode" @loaded="onLoaded"></StreamBarcodeReader>
<div v-if="showPleaseSelectColumnMessage" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.info.codeScanner.selectColumn') }}
</div>
<div v-if="showScannerIsLoadingMessage" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.info.codeScanner.loadingScanner') }}
</div>
</div>
</div>
</a-modal>
</div>
</template>

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

@ -35,6 +35,8 @@ const { isUIAllowed } = useUIPermission()
const { isSharedBase } = storeToRefs(useProject())
const { isMobileMode } = useGlobal()
let showShareModel = $ref(false)
const passwordProtected = ref(false)
@ -195,7 +197,7 @@ const copyLink = async () => {
// Copied to clipboard
message.success(t('msg.info.copiedToClipboard'))
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
}
@ -230,7 +232,7 @@ const copyIframeCode = async () => {
// Copied to clipboard
message.success(t('msg.info.copiedToClipboard'))
} catch (e) {
} catch (e: any) {
message.error(e.message)
}
}
@ -249,7 +251,7 @@ const copyIframeCode = async () => {
<div class="flex items-center gap-1">
<MdiOpenInNew />
<!-- Share View -->
<span class="!text-xs font-weight-normal"> {{ $t('activity.shareView') }}</span>
<span v-if="!isMobileMode" class="!text-xs font-weight-normal"> {{ $t('activity.shareView') }}</span>
</div>
</a-button>

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

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

@ -34,6 +34,8 @@ const addSort = () => {
})
}
const { isMobileMode } = useGlobal()
eventBus.on((event) => {
if (event === SmartsheetStoreEvents.SORT_RELOAD) {
loadSorts()
@ -76,7 +78,7 @@ useMenuCloseOnEsc(open)
<MdiSort />
<!-- Sort -->
<span class="text-capitalize !text-xs font-weight-normal">{{ $t('activity.sort') }}</span>
<span v-if="!isMobileMode" class="text-capitalize !text-xs font-weight-normal">{{ $t('activity.sort') }}</span>
<MdiMenuDown class="text-grey" />
<span v-if="sorts?.length" class="nc-count-badge">{{ sorts.length }}</span>

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

@ -26,6 +26,8 @@ const { t } = useI18n()
const sharedViewListDlg = ref(false)
const { isMobileMode } = useGlobal()
const isPublicView = inject(IsPublicInj, ref(false))
const isView = false
@ -218,7 +220,7 @@ useMenuCloseOnEsc(open)
</div>
</a-menu-item>
<a-menu-item v-if="!isSqlView">
<a-menu-item v-if="!isSqlView && !isMobileMode">
<div
v-if="isUIAllowed('webhook') && !isView && !isPublicView"
v-e="['c:actions:webhook']"
@ -230,7 +232,7 @@ useMenuCloseOnEsc(open)
</div>
</a-menu-item>
<a-menu-item v-if="!isSharedBase && !isPublicView">
<a-menu-item v-if="!isSharedBase && !isPublicView && !isMobileMode">
<div v-e="['c:snippet:open']" class="py-2 flex gap-2 items-center" @click="showApiSnippetDrawer = true">
<MdiXml class="text-gray-500" />
<!-- Get API Snippet -->
@ -239,7 +241,7 @@ useMenuCloseOnEsc(open)
</a-menu-item>
<a-menu-item>
<div v-e="['c:erd:open']" class="py-2 flex gap-2 items-center nc-view-action-erd" @click="showErd = true">
<div v-if="!isMobileMode" v-e="['c:erd:open']" class="py-2 flex gap-2 items-center nc-view-action-erd" @click="showErd = true">
<MaterialSymbolsAccountTreeRounded class="text-gray-500" />
{{ $t('title.erdView') }}
</div>

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')

16
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,
@ -40,6 +41,8 @@ const { t } = useI18n()
const { project } = storeToRefs(useProject())
const { isMobileMode } = useGlobal()
const { $api, $e } = useNuxtApp()
const { copy } = useCopy()
@ -107,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')
@ -131,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')
@ -181,7 +185,7 @@ watch(
>
<div class="flex flex-col" data-testid="invite-user-and-share-base-modal">
<div class="flex flex-row justify-between items-center pb-1.5 mb-2 border-b-1 w-full">
<a-typography-title class="select-none" :level="4"> {{ $t('activity.share') }}: {{ project.title }} </a-typography-title>
<a-typography-title v-if="!isMobileMode" class="select-none" :level="4"> {{ $t('activity.share') }}: {{ project.title }} </a-typography-title>
<a-button
type="text"

11
packages/nc-gui/components/virtual-cell/QrCode.vue

@ -1,5 +1,6 @@
<script setup lang="ts">
import { useQRCode } from '@vueuse/integrations/useQRCode'
import type QRCode from 'qrcode'
import { RowHeightInj } from '#imports'
const maxNumberOfAllowedCharsForQrValue = 2000
@ -12,13 +13,23 @@ const tooManyCharsForQrCode = computed(() => qrValue?.value.length > maxNumberOf
const showQrCode = computed(() => qrValue?.value?.length > 0 && !tooManyCharsForQrCode.value)
const qrCodeOptions: QRCode.QRCodeToDataURLOptions = {
errorCorrectionLevel: 'M',
margin: 1,
version: 4,
rendererOpts: {
quality: 1,
},
}
const rowHeight = inject(RowHeightInj)
const qrCode = useQRCode(qrValue, {
...qrCodeOptions,
width: 150,
})
const qrCodeLarge = useQRCode(qrValue, {
...qrCodeOptions,
width: 600,
})

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'))

22
packages/nc-gui/composables/useBetaFeatureToggle.ts

@ -0,0 +1,22 @@
import { reactive } from 'vue'
const storedValue = localStorage.getItem('betaFeatureToggleState')
const initialToggleState = storedValue ? JSON.parse(storedValue) : false
const betaFeatureToggleState = reactive({ show: initialToggleState })
const toggleBetaFeature = () => {
betaFeatureToggleState.show = !betaFeatureToggleState.show
localStorage.setItem('betaFeatureToggleState', JSON.stringify(betaFeatureToggleState.show))
}
const _useBetaFeatureToggle = () => {
return {
betaFeatureToggleState,
toggleBetaFeature,
}
}
const useBetaFeatureToggle = createSharedComposable(_useBetaFeatureToggle)
export { useBetaFeatureToggle }

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

@ -1,16 +1,18 @@
import type { Actions, State } from './types'
import type { Actions, AppInfo, State } from './types'
import { message, useNuxtApp } from '#imports'
export function useGlobalActions(state: State): Actions {
const setIsMobileMode = (isMobileMode: boolean) => {
state.isMobileMode.value = isMobileMode
}
/** Sign out by deleting the token from localStorage */
const signOut: Actions['signOut'] = async () => {
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 {}
}
@ -48,18 +50,18 @@ 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)
}
}
return { signIn, signOut, refreshToken, loadAppInfo }
return { signIn, signOut, refreshToken, loadAppInfo, setIsMobileMode }
}

1
packages/nc-gui/composables/useGlobal/state.ts

@ -66,6 +66,7 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
currentVersion: null,
latestRelease: null,
hiddenRelease: null,
isMobileMode: null,
}
/** saves a reactive state, any change to these values will write/delete to localStorage */

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

@ -36,6 +36,7 @@ export interface StoredState {
currentVersion: string | null
latestRelease: string | null
hiddenRelease: string | null
isMobileMode: boolean | null
}
export type State = ToRefs<Omit<StoredState, 'token'>> & {
@ -59,6 +60,7 @@ export interface Actions {
signIn: (token: string) => void
refreshToken: () => void
loadAppInfo: () => void
setIsMobileMode: (isMobileMode: boolean) => void
}
export type ReadonlyState = Readonly<Pick<State, 'token' | 'user'>> & Omit<State, 'token' | 'user'>

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!

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

@ -1,15 +1,8 @@
import { reactive } from 'vue'
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'
const storedValue = localStorage.getItem('geodataToggleState')
const initialState = storedValue ? JSON.parse(storedValue) : false
export const geodataToggleState = reactive({ show: initialState })
const formatData = (list: Record<string, any>[]) =>
list.map(
(row) =>
@ -22,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>,
@ -111,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,
@ -162,7 +155,6 @@ const [useProvideMapViewStore, useMapViewStore] = useInjectionState(
geoDataFieldColumn,
addEmptyRow,
insertRow,
geodataToggleState,
syncCount,
paginationData,
}

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 = []

5
packages/nc-gui/composables/useSharedFormViewStore.ts

@ -1,7 +1,7 @@
import useVuelidate from '@vuelidate/core'
import { helpers, minLength, required } from '@vuelidate/validators'
import type { Ref } from 'vue'
import type { ColumnType, FormType, LinkToAnotherRecordType, TableType, ViewType } from 'nocodb-sdk'
import type { BoolType, ColumnType, FormType, LinkToAnotherRecordType, StringOrNullType, TableType, ViewType } from 'nocodb-sdk'
import { ErrorMessages, RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { isString } from '@vueuse/core'
import {
@ -37,7 +37,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const sharedView = ref<ViewType>()
const sharedFormView = ref<FormType>()
const meta = ref<TableType>()
const columns = ref<(ColumnType & { required?: boolean; show?: boolean; label?: string })[]>()
const columns =
ref<(ColumnType & { required?: BoolType; show?: BoolType; label?: StringOrNullType; enable_scanner?: BoolType })[]>()
const sharedViewMeta = ref<SharedViewMeta>({})
const formResetHook = createEventHook<void>()

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 {

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

@ -541,6 +541,7 @@ export function useViewData(
changePage,
addEmptyRow,
deleteRow,
deleteRowById,
deleteSelectedRows,
updateOrSaveRow,
selectedAllRecords,
@ -557,7 +558,6 @@ export function useViewData(
removeLastEmptyRow,
removeRowIfNew,
navigateToSiblingRow,
deleteRowById,
getExpandedRowIndex,
}
}

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,

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

@ -2,7 +2,7 @@ import type { TableType, ViewType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core'
import { ref, unref, useNuxtApp, watch } from '#imports'
export function useViews(meta: MaybeRef<TableType | undefined>) {
function useViews(meta: MaybeRef<TableType | undefined>) {
const views = ref<ViewType[]>([])
const isLoading = ref(false)
@ -26,3 +26,5 @@ export function useViews(meta: MaybeRef<TableType | undefined>) {
return { views, isLoading, loadViews }
}
export default useViews

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']> =

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

@ -210,7 +210,7 @@
"advancedSettings": "Advanced Settings",
"codeSnippet": "Code Snippet",
"keyboardShortcut": "Keyboard Shortcuts",
"generateRandomName": "Generate Random Name",
"generateRandomName": "Generate Random Name",
"findRowByScanningCode": "Find row by scanning a QR or Barcode"
},
"labels": {

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]

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

File diff suppressed because it is too large Load Diff

5
packages/nc-gui/package.json

@ -65,9 +65,11 @@
"unique-names-generator": "^4.7.1",
"v3-infinite-loading": "^1.2.2",
"validator": "^13.7.0",
"vue-barcode-reader": "^1.0.3",
"vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.0.3",
"vue-i18n": "^9.2.2",
"vue-qrcode-reader": "3.1.0-vue3-compatibility.2",
"vue3-contextmenu": "^0.2.12",
"vue3-text-clamp": "^0.1.1",
"vuedraggable": "^4.1.0",
@ -96,14 +98,17 @@
"@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",
"@types/leaflet.markercluster": "^1.5.1",
"@types/papaparse": "^5.3.2",
"@types/qrcode": "^1.5.0",
"@types/sortablejs": "^1.13.0",
"@types/tinycolor2": "^1.4.3",
"@types/validator": "^13.7.10",
"@types/vue-barcode-reader": "^0.0.0",
"@vitest/ui": "^0.18.0",
"@vue/compiler-sfc": "^3.2.37",
"@vue/test-utils": "^2.0.2",

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

@ -1,5 +1,6 @@
<script setup lang="ts">
import tinycolor from 'tinycolor2'
import type { TableType } from 'nocodb-sdk'
import {
TabType,
computed,
@ -41,11 +42,13 @@ const { t } = useI18n()
const { $e } = useNuxtApp()
const { betaFeatureToggleState } = useBetaFeatureToggle()
const route = useRoute()
const router = useRouter()
const { appInfo, token, signOut, signedIn, user, currentVersion } = useGlobal()
const { appInfo, token, signOut, signedIn, user, currentVersion, isMobileMode, setIsMobileMode } = useGlobal()
const projectStore = useProject()
@ -142,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)
}
@ -360,7 +363,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<!-- Swagger: Rest APIs -->
<a-menu-item key="api">
<div
v-if="isUIAllowed('apiDocs')"
v-if="isUIAllowed('apiDocs') && !isMobileMode"
v-e="['e:api-docs']"
class="nc-project-menu-item group"
@click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.ncSiteUrl)"
@ -372,7 +375,12 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<!-- Copy Auth Token -->
<a-menu-item key="copy">
<div v-e="['a:navbar:user:copy-auth-token']" class="nc-project-menu-item group" @click.stop="copyAuthToken">
<div
v-if="!isMobileMode"
v-e="['a:navbar:user:copy-auth-token']"
class="nc-project-menu-item group"
@click.stop="copyAuthToken"
>
<MdiScriptTextKeyOutline class="group-hover:text-accent" />
{{ $t('activity.account.authToken') }}
</div>
@ -383,7 +391,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<!-- Team & Settings -->
<a-menu-item key="teamAndSettings">
<div
v-if="isUIAllowed('settings')"
v-if="isUIAllowed('settings') && !isMobileMode"
v-e="['c:navdraw:project-settings']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'teamAndAuth')"
@ -393,6 +401,18 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</div>
</a-menu-item>
<!-- Mobile Mode -->
<a-menu-item v-if="betaFeatureToggleState.show || isMobileMode" key="mobile-mode">
<div
v-e="['e:set-mobile-mode']"
class="nc-project-menu-item group"
@click.stop="setIsMobileMode(!isMobileMode)"
>
<MaterialSymbolsMobileFriendly class="group-hover:text-accent" />
{{ $t('activity.toggleMobileMode') }}
</div>
</a-menu-item>
<!-- Theme -->
<template v-if="isUIAllowed('projectTheme')">
<a-sub-menu key="theme">
@ -470,7 +490,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-menu-divider />
<!-- Preview As -->
<a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as">
<a-sub-menu v-if="isUIAllowed('previewAs') && !isMobileMode" key="preview-as">
<template #title>
<div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<MdiFileEyeOutline class="group-hover:text-accent" />

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

@ -8,7 +8,7 @@ const tabStore = useTabs()
const { closeTab } = tabStore
const { tabs, activeTabIndex, activeTab } = storeToRefs(tabStore)
const { isLoading } = useGlobal()
const { isLoading, isMobileMode } = useGlobal()
provide(TabMetaInj, activeTab)
@ -28,6 +28,12 @@ const { isOpen, toggle } = useSidebar('nc-left-sidebar')
function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
if (action === 'remove') closeTab(targetKey)
}
const hideSidebarOnClickOrTouchIfMobileMode = () => {
if (isMobileMode.value && isOpen.value) {
toggle(false)
}
}
</script>
<template>
@ -81,10 +87,10 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
</div>
<LazyGeneralShareBaseButton class="mb-1px" />
<LazyGeneralFullScreen class="nc-fullscreen-icon mb-1px" />
<LazyGeneralFullScreen v-if="!isMobileMode" class="nc-fullscreen-icon mb-1px" />
</div>
<div class="w-full min-h-[300px] flex-auto">
<div class="w-full min-h-[300px] flex-auto" @click="hideSidebarOnClickOrTouchIfMobileMode">
<NuxtPage :page-key="`${$route.params.projectId}.${$route.name}`" />
</div>
</div>

116
packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue

@ -1,5 +1,8 @@
<script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { ref } from 'vue'
import { StreamBarcodeReader } from 'vue-barcode-reader'
import { useSharedFormStoreOrThrow } from '#imports'
const { sharedFormView, submitForm, v$, formState, notFound, formColumns, submitted, secondsRemain, isLoading } =
@ -17,6 +20,51 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
return !!(required || (columnObj && columnObj.rqd && !columnObj.cdf))
}
const fieldTitleForCurrentScan = ref('')
const scannerIsReady = ref(false)
const showCodeScannerOverlay = ref(false)
const onLoaded = async () => {
scannerIsReady.value = true
}
const showCodeScannerForFieldTitle = (fieldTitle: string) => {
showCodeScannerOverlay.value = true
fieldTitleForCurrentScan.value = fieldTitle
}
const findColumnByTitle = (title: string) => formColumns.value?.find((el: ColumnType) => el.title === title)
const getScannedValueTransformerByFieldType = (fieldType: UITypes) => {
switch (fieldType) {
case UITypes.Number:
return (originalVal: string) => parseInt(originalVal)
default:
return (originalVal: string) => originalVal
}
}
const onDecode = async (scannedCodeValue: string) => {
if (!showCodeScannerOverlay.value) {
return
}
try {
const fieldForCurrentScan = findColumnByTitle(fieldTitleForCurrentScan.value)
if (fieldForCurrentScan == null) {
throw new Error(`Field with title ${fieldTitleForCurrentScan.value} not found`)
}
const transformedVal =
getScannedValueTransformerByFieldType(fieldForCurrentScan.uidt as UITypes)(scannedCodeValue) || scannedCodeValue
formState.value[fieldTitleForCurrentScan.value] = transformedVal
fieldTitleForCurrentScan.value = ''
showCodeScannerOverlay.value = false
} catch (error) {
console.error(error)
}
}
</script>
<template>
@ -55,6 +103,20 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
</template>
<template v-else>
<a-modal
v-model:visible="showCodeScannerOverlay"
:closable="false"
width="28rem"
centered
:footer="null"
wrap-class-name="nc-modal-generate-token"
destroy-on-close
@cancel="scannerIsReady = false"
>
<div class="relative flex flex-col h-full">
<StreamBarcodeReader v-show="scannerIsReady" @decode="onDecode" @loaded="onLoaded"> </StreamBarcodeReader>
</div>
</a-modal>
<GeneralOverlay class="bg-gray-400/75" :model-value="isLoading" inline transition>
<div class="w-full h-full flex items-center justify-center">
<a-spin size="large" />
@ -82,24 +144,36 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
</div>
<div>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(field)"
:model-value="null"
class="mt-0 nc-input nc-cell"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
:column="field"
/>
<LazySmartsheetCell
v-else
v-model="formState[field.title]"
class="nc-input"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
:column="field"
:edit-enabled="true"
/>
<div class="flex">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(field)"
:model-value="null"
class="mt-0 nc-input nc-cell"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
:column="field"
/>
<LazySmartsheetCell
v-else
v-model="formState[field.title]"
class="nc-input"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
:column="field"
:edit-enabled="true"
/>
<a-button
v-if="field.enable_scanner"
class="nc-btn-fill-form-column-by-scan nc-toolbar-btn"
:alt="$t('activity.fillByCodeScan')"
@click="showCodeScannerForFieldTitle(field.title)"
>
<div class="flex items-center gap-1">
<mdi-qrcode-scan class="h-5 w-5" />
</div>
</a-button>
</div>
<div class="flex flex-col gap-2 text-slate-500 dark:text-slate-300 text-[0.75rem] my-2 px-1">
<div v-for="error of v$.localState[field.title]?.$errors" :key="error" class="text-red-500">
@ -136,6 +210,10 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
<style lang="scss" scoped>
:deep(.nc-cell .nc-action-icon) {
@apply !text-white-500 !bg-white/50 !rounded-full !p-1 !text-xs !w-7 !h-7 !flex !items-center !justify-center !cursor-pointer !hover:!bg-white-600 !hover:!text-white-600 !transition;
@apply !text-white-500 !bg-white/50 !rounded-full !p-1 !text-xs !w-7 !h-7 !flex !items-center !justify-center !cursor-pointer !hover: !bg-white-600 !hover: !text-white-600 !transition;
}
.nc-btn-fill-form-column-by-scan {
@apply h-auto;
@apply ml-1;
}
</style>

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

7
packages/nc-gui/plugins/vue-qr-code-scanner.ts

@ -0,0 +1,7 @@
import VueQrcodeReader from 'vue-qrcode-reader'
import { defineNuxtPlugin } from 'nuxt/app'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(VueQrcodeReader)
})

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

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

@ -815,6 +815,11 @@ export interface FormColumnType {
required?: BoolType;
/** Is this column shown in Form? */
show?: BoolType;
/**
* Indicates whether the 'Fill by scan' button is visible for this column or not.
* @example true
*/
enable_scanner?: BoolType;
/** Form Column UUID (Not in use) */
uuid?: StringOrNullType;
}
@ -1697,7 +1702,9 @@ export interface PluginTestReqType {
/** Plugin Title */
title: string;
/** Plugin Input as JSON string */
input: string;
input: string | object;
/** @example Email */
category: string;
}
/**
@ -1775,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
*/
@ -2927,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` `{
@ -2947,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> */
@ -3904,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;
@ -3923,6 +3973,7 @@ export class Api<
method: 'PATCH',
body: data,
type: ContentType.Json,
format: 'json',
...params,
}),
@ -8319,6 +8370,7 @@ export class Api<
ee?: boolean,
ncAttachmentFieldSize?: number,
ncMaxAttachmentsAllowed?: number,
isCloud?: boolean,
}` OK
* @response `400` `{
@ -8347,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(

2
packages/nocodb-sdk/src/lib/columnRules/QrAndBarcodeRules.ts

@ -9,4 +9,6 @@ export const AllowedColumnTypesForQrAndBarcodes = [
UITypes.Email,
UITypes.Decimal,
UITypes.Number,
UITypes.AutoNumber,
UITypes.ID
];

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

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

@ -15,6 +15,7 @@ import * as nc_024_barcode_column_type from './v2/nc_024_barcode_column_type';
import * as nc_025_add_row_height from './v2/nc_025_add_row_height';
import * as nc_026_map_view from './v2/nc_026_map_view';
import * as nc_027_add_comparison_sub_op from './v2/nc_027_add_comparison_sub_op';
import * as nc_028_add_enable_scanner_in_form_columns_meta_table from './v2/nc_026_add_enable_scanner_in_form_columns_meta_table';
// Create a custom migration source class
export default class XcMigrationSourcev2 {
@ -41,6 +42,7 @@ export default class XcMigrationSourcev2 {
'nc_025_add_row_height',
'nc_026_map_view',
'nc_027_add_comparison_sub_op',
'nc_028_add_enable_scanner_in_form_columns_meta_table',
]);
}
@ -84,6 +86,8 @@ export default class XcMigrationSourcev2 {
return nc_026_map_view;
case 'nc_027_add_comparison_sub_op':
return nc_027_add_comparison_sub_op;
case 'nc_028_add_enable_scanner_in_form_columns_meta_table':
return nc_028_add_enable_scanner_in_form_columns_meta_table;
}
}
}

16
packages/nocodb/src/lib/migrations/v2/nc_026_add_enable_scanner_in_form_columns_meta_table.ts

@ -0,0 +1,16 @@
import { MetaTable } from '../../utils/globals';
import type { Knex } from 'knex';
const up = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.FORM_VIEW_COLUMNS, (table) => {
table.boolean('enable_scanner');
});
};
const down = async (knex) => {
await knex.schema.alterTable(MetaTable.FORM_VIEW_COLUMNS, (table) => {
table.dropColumns('enable_scanner');
});
};
export { up, down };

3
packages/nocodb/src/lib/models/FormViewColumn.ts

@ -21,6 +21,7 @@ export default class FormViewColumn implements FormColumnType {
help?: StringOrNullType;
description?: StringOrNullType;
required?: BoolType;
enable_scanner?: BoolType;
uuid?: StringOrNullType;
show?: BoolType;
order?: number;
@ -68,6 +69,7 @@ export default class FormViewColumn implements FormColumnType {
'help',
'description',
'required',
'enable_scanner',
'meta',
]);
@ -166,6 +168,7 @@ export default class FormViewColumn implements FormColumnType {
'show',
'order',
'meta',
'enable_scanner',
]);
// get existing cache

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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save