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/ nc_minimal_dbs/
test_noco.db 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'] MdiCloseCircleOutline: typeof import('~icons/mdi/close-circle-outline')['default']
MdiCloseThick: typeof import('~icons/mdi/close-thick')['default'] MdiCloseThick: typeof import('~icons/mdi/close-thick')['default']
MdiCodeJson: typeof import('~icons/mdi/code-json')['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'] MdiCodeTags: typeof import('~icons/mdi/code-tags')['default']
MdiCog: typeof import('~icons/mdi/cog')['default'] MdiCog: typeof import('~icons/mdi/cog')['default']
MdiCommentTextOutline: typeof import('~icons/mdi/comment-text-outline')['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'] MdiPlusCircleOutline: typeof import('~icons/mdi/plus-circle-outline')['default']
MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default'] MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default']
MdiPlusThick: typeof import('~icons/mdi/plus-thick')['default'] MdiPlusThick: typeof import('~icons/mdi/plus-thick')['default']
MdiQrcodeScan: typeof import('~icons/mdi/qrcode-scan')['default']
MdiReddit: typeof import('~icons/mdi/reddit')['default'] MdiReddit: typeof import('~icons/mdi/reddit')['default']
MdiRefresh: typeof import('~icons/mdi/refresh')['default'] MdiRefresh: typeof import('~icons/mdi/refresh')['default']
MdiReload: typeof import('~icons/mdi/reload')['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 () => { const loadLicense = async () => {
try { try {
const response = await api.orgLicense.get() const response = await api.orgLicense.get()
key = response.key!
key = response.key } catch (e: any) {
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
} }
@ -25,7 +24,7 @@ const setLicense = async () => {
await api.orgLicense.set({ key: key }) await api.orgLicense.set({ key: key })
message.success('License key updated') message.success('License key updated')
await loadAppInfo() await loadAppInfo()
} catch (e) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
$e('a:account:license') $e('a:account:license')
@ -45,5 +44,3 @@ loadLicense()
</div> </div>
</div> </div>
</template> </template>
<style scoped></style>

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

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

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

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

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

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

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

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

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

@ -1,5 +1,14 @@
<script setup lang="ts"> <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 { interface Props {
// If the previous cell value was a text, the initial checkbox value is a string type // 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', unchecked: 'mdi-checkbox-blank-circle-outline',
}, },
color: 'primary', 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"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' 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 { interface Props {
modelValue: number | null | undefined modelValue: number | null | undefined
@ -35,7 +35,7 @@ const currencyMeta = computed(() => {
return { return {
currency_locale: 'en-US', currency_locale: 'en-US',
currency_code: 'USD', 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, computed,
inject, inject,
isDrawerOrModalExist, isDrawerOrModalExist,
parseProp,
ref, ref,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
watch, watch,
@ -34,7 +35,7 @@ const editable = inject(EditModeInj, ref(false))
let isDateInvalid = $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({ let localState = $computed({
get() { get() {

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

@ -7,6 +7,7 @@ import {
dateFormats, dateFormats,
inject, inject,
isDrawerOrModalExist, isDrawerOrModalExist,
parseProp,
ref, ref,
timeFormats, timeFormats,
useProject, useProject,
@ -38,8 +39,8 @@ const column = inject(ColumnInj)!
let isDateInvalid = $ref(false) let isDateInvalid = $ref(false)
const dateTimeFormat = $computed(() => { const dateTimeFormat = $computed(() => {
const dateFormat = column?.value?.meta?.date_format ?? dateFormats[0] const dateFormat = parseProp(column?.value?.meta)?.date_format ?? dateFormats[0]
const timeFormat = column?.value?.meta?.time_format ?? timeFormats[0] const timeFormat = parseProp(column?.value?.meta)?.time_format ?? timeFormats[0]
return `${dateFormat} ${timeFormat}` 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' import { EditModeInj, inject, useVModel } from '#imports'
interface Props { 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 modelValue?: number | null | string
} }
@ -22,8 +25,10 @@ const _vModel = useVModel(props, 'modelValue', emits)
const vModel = computed({ const vModel = computed({
get: () => _vModel.value, get: () => _vModel.value,
set: (value: string) => { set: (value) => {
if (value === '') { if (value === '') {
// if we clear / empty a cell in sqlite,
// the value is considered as ''
_vModel.value = null _vModel.value = null
} else { } else {
_vModel.value = value _vModel.value = value

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

@ -8,6 +8,7 @@ import {
convertMS2Duration, convertMS2Duration,
durationOptions, durationOptions,
inject, inject,
parseProp,
ref, ref,
} from '#imports' } from '#imports'
@ -32,7 +33,7 @@ const durationInMS = ref(0)
const isEdited = ref(false) 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) 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' import { EditModeInj, inject, useVModel } from '#imports'
interface Props { 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 { interface Emits {
@ -22,8 +25,10 @@ const _vModel = useVModel(props, 'modelValue', emits)
const vModel = computed({ const vModel = computed({
get: () => _vModel.value, get: () => _vModel.value,
set: (value: string) => { set: (value) => {
if (value === '') { if (value === '') {
// if we clear / empty a cell in sqlite,
// the value is considered as ''
_vModel.value = null _vModel.value = null
} else { } else {
_vModel.value = value _vModel.value = value

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

@ -56,7 +56,7 @@ const onClickSetCurrentLocation = () => {
isLoading = false isLoading = false
} }
const onError: PositionErrorCallback = (err) => { const onError: PositionErrorCallback = (err: GeolocationPositionError) => {
console.error(`ERROR(${err.code}): ${err.message}`) console.error(`ERROR(${err.code}): ${err.message}`)
isLoading = false 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' import { EditModeInj, inject, useVModel } from '#imports'
interface Props { 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 { interface Emits {
@ -22,8 +25,10 @@ const _vModel = useVModel(props, 'modelValue', emits)
const vModel = computed({ const vModel = computed({
get: () => _vModel.value, get: () => _vModel.value,
set: (value: string) => { set: (value) => {
if (value === '') { if (value === '') {
// if we clear / empty a cell in sqlite,
// the value is considered as ''
_vModel.value = null _vModel.value = null
} else { } else {
_vModel.value = value _vModel.value = value

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

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

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

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

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

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

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

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

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

@ -14,6 +14,7 @@ import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
isDrawerOrModalExist, isDrawerOrModalExist,
isMac, isMac,
parseProp,
reactive, reactive,
ref, ref,
resolveComponent, resolveComponent,
@ -32,6 +33,8 @@ import {
import MdiView from '~icons/mdi/eye-circle-outline' import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large' import MdiTableLarge from '~icons/mdi/table-large'
const { isMobileMode } = useGlobal()
const { addTab, updateTab } = useTabs() const { addTab, updateTab } = useTabs()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
@ -312,7 +315,7 @@ watch(
const setIcon = async (icon: string, table: TableType) => { const setIcon = async (icon: string, table: TableType) => {
try { try {
table.meta = { table.meta = {
...(table.meta || {}), ...parseProp(table.meta),
icon, icon,
} }
tables.value.splice(tables.value.indexOf(table), 1, { ...table }) 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 }) $e('a:table:icon:navdraw', { icon })
} catch (e) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) 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" /> <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 <GithubButton
v-if="!isMobileMode"
class="ml-2 py-1" class="ml-2 py-1"
href="https://github.com/nocodb/nocodb" href="https://github.com/nocodb/nocodb"
data-icon="octicon-star" 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 isLoading = true
const { list, pageInfo } = await $api.project.auditList(project.value?.id, { const { list, pageInfo } = await $api.project.auditList(project.value?.id, {
offset: (limit * (page - 1)).toString(), offset: limit * (page - 1),
limit: limit.toString(), limit,
}) })
audits = list audits = list

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

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

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

@ -75,9 +75,7 @@ const viewNameRules = [
{ {
validator: (_: unknown, v: string) => validator: (_: unknown, v: string) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
views.every((v1) => ((v1 as GridType | KanbanType | GalleryType | MapType).alias || v1.title) !== v) views.every((v1) => v1.title !== v) ? resolve(true) : reject(new Error(`View name should be unique`))
? resolve(true)
: reject(new Error(`View name should be unique`))
}), }),
message: '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 // if table is provided only get the table and its related tables
localTables = projectTables.value.filter( localTables = projectTables.value.filter(
(t) => (t) =>
t.id === props.table.id || t.id === props.table?.id ||
props.table.columns?.find( props.table?.columns?.find(
(column) => (column) =>
column.uidt === UITypes.LinkToAnotherRecord && column.uidt === UITypes.LinkToAnotherRecord &&
(column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id === t.id, (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" /> <MdiEyeCircleOutline v-else-if="tableMeta?.type === 'view'" class="w-5" />
<MdiTableLarge v-else class="w-5" /> <MdiTableLarge v-else class="w-5" />
</template> </template>
<style scoped></style>

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

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

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

@ -99,6 +99,8 @@ const activeRow = ref('')
const { t } = useI18n() const { t } = useI18n()
const { betaFeatureToggleState } = useBetaFeatureToggle()
const updateView = useDebounceFn( const updateView = useDebounceFn(
() => { () => {
if ((formViewData.value?.subheading?.length || 0) > 255) { 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, () => { onClickOutside(draggableRef, () => {
activeRow.value = '' activeRow.value = ''
}) })
@ -616,6 +622,30 @@ watch(view, (nextView) => {
/> />
</div> </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-form-item class="my-0 w-1/2 !mb-1">
<a-input <a-input
v-model:value="element.label" v-model:value="element.label"
@ -827,7 +857,7 @@ watch(view, (nextView) => {
@apply px-4 min-h-[75px] w-full h-full; @apply px-4 min-h-[75px] w-full h-full;
.nc-attachment { .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 { .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 isPublic = inject(IsPublicInj, ref(false))
const { isMobileMode } = useGlobal()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { isOpen } = useSidebar('nc-right-sidebar') const { isOpen } = useSidebar('nc-right-sidebar')
@ -14,7 +16,8 @@ const { allowCSVDownload } = useSharedView()
<template> <template>
<div <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" style="z-index: 7"
> >
<LazySmartsheetToolbarViewActions <LazySmartsheetToolbarViewActions
@ -41,8 +44,10 @@ const { allowCSVDownload } = useSharedView()
<LazySmartsheetToolbarShareView v-if="(isForm || isGrid || isKanban || isGallery || isMap) && !isPublic" /> <LazySmartsheetToolbarShareView v-if="(isForm || isGrid || isKanban || isGallery || isMap) && !isPublic" />
<LazySmartsheetToolbarQrScannerButton v-if="isMobileMode && (isGrid || isKanban || isGallery)" />
<LazySmartsheetToolbarExport v-if="(!isPublic && !isUIAllowed('dataInsert')) || (isPublic && allowCSVDownload)" /> <LazySmartsheetToolbarExport v-if="(!isPublic && !isUIAllowed('dataInsert')) || (isPublic && allowCSVDownload)" />
<div class="flex-1" /> <div v-if="!isMobileMode" class="flex-1" />
<LazySmartsheetToolbarReload v-if="!isPublic && !isForm" /> <LazySmartsheetToolbarReload v-if="!isPublic && !isForm" />
@ -51,7 +56,7 @@ const { allowCSVDownload } = useSharedView()
<LazySmartsheetToolbarSearchData v-if="(isGrid || isGallery || isKanban) && !isPublic" class="shrink mx-2" /> <LazySmartsheetToolbarSearchData v-if="(isGrid || isGallery || isKanban) && !isPublic" class="shrink mx-2" />
<template v-if="!isOpen && !isPublic"> <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" /> <LazySmartsheetSidebarToolbarToggleDrawer class="mr-2" />
</div> </div>
</template> </template>
@ -66,4 +71,7 @@ const { allowCSVDownload } = useSharedView()
.nc-table-toolbar { .nc-table-toolbar {
border-color: #f0f0f0 !important; border-color: #f0f0f0 !important;
} }
.nc-table-toolbar-mobile {
@apply flex-wrap h-auto py-2;
}
</style> </style>

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

@ -41,6 +41,8 @@ const { $e } = useNuxtApp()
const { appInfo } = useGlobal() const { appInfo } = useGlobal()
const { betaFeatureToggleState } = useBetaFeatureToggle()
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const isForm = inject(IsFormInj, ref(false)) 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 onlyNameUpdateOnEditColumns = [UITypes.LinkToAnotherRecord, UITypes.Lookup, UITypes.Rollup]
const geoDataToggleCondition = (t) => { const geoDataToggleCondition = (t: { name: UITypes }) => {
return geodataToggleState.show ? geodataToggleState.show : !t.name.includes(UITypes.GeoData) return betaFeatureToggleState.show ? betaFeatureToggleState.show : !t.name.includes(UITypes.GeoData)
} }
const uiTypesOptions = computed<typeof uiTypes>(() => { 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 // on alt + s save record
else if (e.code === 'KeyS') { else if (e.code === 'KeyS') {
// remove focus from the active input if any // remove focus from the active input if any
document.activeElement?.blur() ;(document.activeElement as HTMLElement)?.blur()
e.stopPropagation() e.stopPropagation()

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

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

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

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

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

@ -10,6 +10,7 @@ import {
inject, inject,
message, message,
onMounted, onMounted,
parseProp,
ref, ref,
resolveComponent, resolveComponent,
useApi, useApi,
@ -218,7 +219,7 @@ const setIcon = async (icon: string, view: ViewType) => {
try { try {
// modify the icon property in meta // modify the icon property in meta
view.meta = { view.meta = {
...(view.meta || {}), ...parseProp(view.meta),
icon, icon,
} }
@ -227,7 +228,7 @@ const setIcon = async (icon: string, view: ViewType) => {
}) })
$e('a:view:icon:sidebar', { icon }) $e('a:view:icon:sidebar', { icon })
} catch (e) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) 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 { isUIAllowed } = useUIPermission()
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj, ref(false))
/** Is editing the view name enabled */ /** Is editing the view name enabled */
let isEditing = $ref<boolean>(false) let isEditing = $ref<boolean>(false)

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

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

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

@ -109,7 +109,7 @@ const filterUpdateCondition = (filter: FilterType, i: number) => {
} }
} }
saveOrUpdate(filter, i) saveOrUpdate(filter, i)
filterPrevComparisonOp.value[filter.id] = filter.comparison_op filterPrevComparisonOp.value[filter.id!] = filter.comparison_op!
$e('a:filter:update', { $e('a:filter:update', {
logical: filter.logical_op, logical: filter.logical_op,
comparison: filter.comparison_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 // since the existing one may not be supported for the new field
// e.g. `eq` operator is not supported in checkbox field // e.g. `eq` operator is not supported in checkbox field
// hence, get the first option of the supported operators of the new field // hence, get the first option of the supported operators of the new field
filter.comparison_op = comparisonOpList(col.uidt as UITypes).filter((compOp) => filter.comparison_op = comparisonOpList(col.uidt as UITypes).find((compOp) => isComparisonOpAllowed(filter, compOp))
isComparisonOpAllowed(filter, compOp), ?.value as FilterType['comparison_op']
)?.[0].value
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') { if (filter.comparison_op === 'isWithin') {
filter.comparison_sub_op = 'pastNumberOfDays' filter.comparison_sub_op = 'pastNumberOfDays'
} else { } 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 isPublic = inject(IsPublicInj, ref(false))
const { filterAutoSave } = useGlobal() const { filterAutoSave, isMobileMode } = useGlobal()
const filterComp = ref<typeof ColumnFilter>() const filterComp = ref<typeof ColumnFilter>()
@ -76,7 +76,7 @@ useMenuCloseOnEsc(open)
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<MdiFilterOutline /> <MdiFilterOutline />
<!-- Filter --> <!-- 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" /> <MdiMenuDown class="text-grey" />
<span v-if="filtersLength" class="nc-count-badge">{{ filtersLength }}</span> <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 rootFields = inject(FieldsInj)
const { isMobileMode } = useGlobal()
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
@ -157,7 +159,7 @@ useMenuCloseOnEsc(open)
<MdiEyeOffOutline /> <MdiEyeOffOutline />
<!-- Fields --> <!-- 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" /> <MdiMenuDown class="text-grey" />

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

@ -84,10 +84,11 @@ const checkTypeFunctions = {
type FilterType = keyof typeof checkTypeFunctions type FilterType = keyof typeof checkTypeFunctions
// todo: replace with sqlUis const { sqlUis } = storeToRefs(useProject())
const { sqlUi } = $(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 checkType = (filterType: FilterType) => {
const checkTypeFunction = checkTypeFunctions[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 { isSharedBase } = storeToRefs(useProject())
const { isMobileMode } = useGlobal()
let showShareModel = $ref(false) let showShareModel = $ref(false)
const passwordProtected = ref(false) const passwordProtected = ref(false)
@ -195,7 +197,7 @@ const copyLink = async () => {
// Copied to clipboard // Copied to clipboard
message.success(t('msg.info.copiedToClipboard')) message.success(t('msg.info.copiedToClipboard'))
} catch (e) { } catch (e: any) {
message.error(e.message) message.error(e.message)
} }
} }
@ -230,7 +232,7 @@ const copyIframeCode = async () => {
// Copied to clipboard // Copied to clipboard
message.success(t('msg.info.copiedToClipboard')) message.success(t('msg.info.copiedToClipboard'))
} catch (e) { } catch (e: any) {
message.error(e.message) message.error(e.message)
} }
} }
@ -249,7 +251,7 @@ const copyIframeCode = async () => {
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<MdiOpenInNew /> <MdiOpenInNew />
<!-- Share View --> <!-- 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> </div>
</a-button> </a-button>

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

@ -5,6 +5,7 @@ import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
message, message,
onMounted, onMounted,
parseProp,
ref, ref,
useCopy, useCopy,
useDashboard, useDashboard,
@ -70,7 +71,7 @@ const sharedViewUrl = (view: SharedViewType) => {
const renderAllowCSVDownload = (view: SharedViewType) => { const renderAllowCSVDownload = (view: SharedViewType) => {
if (view.type === ViewTypes.GRID) { 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 ? '✔' : '❌' return view.meta?.allowCSVDownload ? '✔' : '❌'
} else { } else {
return 'N/A' return 'N/A'
@ -82,7 +83,7 @@ const copyLink = (view: SharedViewType) => {
copy(`${dashboardUrl?.value as string}#${sharedViewUrl(view)}`) copy(`${dashboardUrl?.value as string}#${sharedViewUrl(view)}`)
// Copied to clipboard // Copied to clipboard
message.success(t('msg.info.copiedToClipboard')) message.success(t('msg.info.copiedToClipboard'))
} catch (e) { } catch (e: any) {
message.error(e.message) 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) => { eventBus.on((event) => {
if (event === SmartsheetStoreEvents.SORT_RELOAD) { if (event === SmartsheetStoreEvents.SORT_RELOAD) {
loadSorts() loadSorts()
@ -76,7 +78,7 @@ useMenuCloseOnEsc(open)
<MdiSort /> <MdiSort />
<!-- Sort --> <!-- 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" /> <MdiMenuDown class="text-grey" />
<span v-if="sorts?.length" class="nc-count-badge">{{ sorts.length }}</span> <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 sharedViewListDlg = ref(false)
const { isMobileMode } = useGlobal()
const isPublicView = inject(IsPublicInj, ref(false)) const isPublicView = inject(IsPublicInj, ref(false))
const isView = false const isView = false
@ -218,7 +220,7 @@ useMenuCloseOnEsc(open)
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item v-if="!isSqlView"> <a-menu-item v-if="!isSqlView && !isMobileMode">
<div <div
v-if="isUIAllowed('webhook') && !isView && !isPublicView" v-if="isUIAllowed('webhook') && !isView && !isPublicView"
v-e="['c:actions:webhook']" v-e="['c:actions:webhook']"
@ -230,7 +232,7 @@ useMenuCloseOnEsc(open)
</div> </div>
</a-menu-item> </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"> <div v-e="['c:snippet:open']" class="py-2 flex gap-2 items-center" @click="showApiSnippetDrawer = true">
<MdiXml class="text-gray-500" /> <MdiXml class="text-gray-500" />
<!-- Get API Snippet --> <!-- Get API Snippet -->
@ -239,7 +241,7 @@ useMenuCloseOnEsc(open)
</a-menu-item> </a-menu-item>
<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" /> <MaterialSymbolsAccountTreeRounded class="text-gray-500" />
{{ $t('title.erdView') }} {{ $t('title.erdView') }}
</div> </div>

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { OrgUserRoles } from 'nocodb-sdk' import { OrgUserRoles } from 'nocodb-sdk'
import type { RequestParams } from 'nocodb-sdk' import type { ProjectUserReqType, RequestParams } from 'nocodb-sdk'
import { import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
message, message,
@ -82,7 +82,7 @@ const inviteUser = async (user: User) => {
user.roles = 'editor' 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 // Successfully added user to project
message.success(t('msg.success.userAddedToProject')) message.success(t('msg.success.userAddedToProject'))
@ -153,7 +153,7 @@ const copyInviteUrl = async (user: User) => {
// Invite URL copied to clipboard // Invite URL copied to clipboard
message.success(t('msg.success.inviteURLCopied')) message.success(t('msg.success.inviteURLCopied'))
} catch (e) { } catch (e: any) {
message.error(e.message) message.error(e.message)
} }
$e('c:user:copy-url') $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! // Copied shareable base url to clipboard!
message.success(t('msg.success.shareableURLCopied')) message.success(t('msg.success.shareableURLCopied'))
} catch (e) { } catch (e: any) {
message.error(e.message) message.error(e.message)
} }
@ -137,7 +137,7 @@ style="background: transparent; border: 1px solid #ddd"></iframe>`)
// Copied embeddable html code! // Copied embeddable html code!
message.success(t('msg.success.embeddableHTMLCodeCopied')) message.success(t('msg.success.embeddableHTMLCodeCopied'))
} catch (e) { } catch (e: any) {
message.error(e.message) message.error(e.message)
} }
$e('c:shared-base:copy-embed-frame') $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"> <script setup lang="ts">
import type { Input } from 'ant-design-vue' import type { Input } from 'ant-design-vue'
import type { ProjectUserReqType } from 'nocodb-sdk'
import { import {
Form, Form,
computed, computed,
@ -40,6 +41,8 @@ const { t } = useI18n()
const { project } = storeToRefs(useProject()) const { project } = storeToRefs(useProject())
const { isMobileMode } = useGlobal()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { copy } = useCopy() const { copy } = useCopy()
@ -107,10 +110,11 @@ const saveUser = async () => {
const res = await $api.auth.projectUserAdd(project.value.id, { const res = await $api.auth.projectUserAdd(project.value.id, {
roles: usersData.role, roles: usersData.role,
email: usersData.emails, email: usersData.emails,
project_id: project.value.id, } as ProjectUserReqType)
projectName: project.value.title,
}) // for inviting one user, invite_token will only be returned when invitation email fails to send
usersData.invitationToken = res.invite_token // for inviting multiple users, invite_token will be returned anyway
usersData.invitationToken = res?.invite_token
} }
emit('reload') emit('reload')
@ -131,7 +135,7 @@ const copyUrl = async () => {
// Copied shareable base url to clipboard! // Copied shareable base url to clipboard!
message.success(t('msg.success.shareableURLCopied')) message.success(t('msg.success.shareableURLCopied'))
} catch (e) { } catch (e: any) {
message.error(e.message) message.error(e.message)
} }
$e('c:shared-base:copy-url') $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-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"> <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 <a-button
type="text" type="text"

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

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

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

@ -22,7 +22,7 @@ const showBarcodeModal = () => {
const barcodeMeta = $computed(() => { const barcodeMeta = $computed(() => {
return { return {
barcodeFormat: 'CODE128', 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"> <script setup lang="ts">
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { AuditType } from 'nocodb-sdk' import type { AuditType, HookType } from 'nocodb-sdk'
import { import {
Form, Form,
MetaInj, MetaInj,
@ -10,6 +10,7 @@ import {
inject, inject,
message, message,
onMounted, onMounted,
parseProp,
reactive, reactive,
ref, ref,
useApi, useApi,
@ -20,7 +21,7 @@ import {
} from '#imports' } from '#imports'
interface Props { interface Props {
hook?: Record<string, any> hook?: HookType
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -39,11 +40,13 @@ const meta = inject(MetaInj, ref())
const useForm = Form.useForm 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: '', id: '',
title: '', title: '',
event: '', event: undefined,
operation: '', operation: undefined,
eventOperation: '', eventOperation: '',
notification: { notification: {
type: 'URL', 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, { Object.assign(hook, {
...newHook, ...newHook,
notification: { notification: {
...newHook.notification, ...notification,
payload: newHook.notification.payload, payload: notification.payload,
}, },
}) })
} }
@ -325,7 +329,7 @@ async function loadPluginList() {
...(p as any), ...(p as any),
} }
plugin.tags = p.tags ? p.tags.split(',') : [] 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 o[plugin.title] = plugin
return o return o
@ -400,8 +404,8 @@ watch(
if (!hook.eventOperation) return if (!hook.eventOperation) return
const [event, operation] = hook.eventOperation.split(' ') const [event, operation] = hook.eventOperation.split(' ')
hook.event = event hook.event = event as HookType['event']
hook.operation = operation hook.operation = operation as HookType['operation']
}, },
) )

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

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

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

@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HookTestReqType, HookType } from 'nocodb-sdk'
import { MetaInj, extractSdkResponseErrorMsg, inject, message, onMounted, ref, useI18n, useNuxtApp, watch } from '#imports' import { MetaInj, extractSdkResponseErrorMsg, inject, message, onMounted, ref, useI18n, useNuxtApp, watch } from '#imports'
interface Props { interface Props {
hook: Record<string, any> hook: HookType
} }
const { hook } = defineProps<Props>() const { hook } = defineProps<Props>()
@ -33,10 +34,13 @@ async function loadSampleData() {
async function testWebhook() { async function testWebhook() {
try { try {
await $api.dbTableWebhook.test(meta.value?.id as string, { await $api.dbTableWebhook.test(
hook, meta.value?.id as string,
payload: sampleData.value, {
}) hook,
payload: sampleData.value,
} as HookTestReqType,
)
// Webhook tested successfully // Webhook tested successfully
message.success(t('msg.success.webhookTested')) 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' import { message, useNuxtApp } from '#imports'
export function useGlobalActions(state: State): Actions { export function useGlobalActions(state: State): Actions {
const setIsMobileMode = (isMobileMode: boolean) => {
state.isMobileMode.value = isMobileMode
}
/** Sign out by deleting the token from localStorage */ /** Sign out by deleting the token from localStorage */
const signOut: Actions['signOut'] = async () => { const signOut: Actions['signOut'] = async () => {
state.token.value = null state.token.value = null
state.user.value = null state.user.value = null
try { try {
if (state.token.value) { const nuxtApp = useNuxtApp()
const nuxtApp = useNuxtApp() await nuxtApp.$api.auth.signout()
await nuxtApp.$api.auth.signout()
}
} catch {} } catch {}
} }
@ -48,18 +50,18 @@ export function useGlobalActions(state: State): Actions {
message.error(err.message || t('msg.error.youHaveBeenSignedOut')) message.error(err.message || t('msg.error.youHaveBeenSignedOut'))
await signOut() await signOut()
}) })
.finally(resolve) .finally(() => resolve())
}) })
} }
const loadAppInfo = async () => { const loadAppInfo = async () => {
try { try {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
state.appInfo.value = await nuxtApp.$api.utils.appInfo() state.appInfo.value = (await nuxtApp.$api.utils.appInfo()) as AppInfo
} catch (e) { } catch (e) {
console.error(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, currentVersion: null,
latestRelease: null, latestRelease: null,
hiddenRelease: null, hiddenRelease: null,
isMobileMode: null,
} }
/** saves a reactive state, any change to these values will write/delete to localStorage */ /** 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 currentVersion: string | null
latestRelease: string | null latestRelease: string | null
hiddenRelease: string | null hiddenRelease: string | null
isMobileMode: boolean | null
} }
export type State = ToRefs<Omit<StoredState, 'token'>> & { export type State = ToRefs<Omit<StoredState, 'token'>> & {
@ -59,6 +60,7 @@ export interface Actions {
signIn: (token: string) => void signIn: (token: string) => void
refreshToken: () => void refreshToken: () => void
loadAppInfo: () => void loadAppInfo: () => void
setIsMobileMode: (isMobileMode: boolean) => void
} }
export type ReadonlyState = Readonly<Pick<State, 'token' | 'user'>> & Omit<State, 'token' | 'user'> 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, getHTMLEncodedText,
inject, inject,
message, message,
parseProp,
provide, provide,
ref, ref,
storeToRefs, storeToRefs,
@ -198,8 +199,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
// set groupingField // set groupingField
groupingFieldColumn.value = !isPublic.value groupingFieldColumn.value = !isPublic.value
? (meta.value.columns as ColumnType[]).filter((f) => f.id === kanbanMetaData.value.fk_grp_col_id)[0] || {} ? (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) : (parseProp(sharedView.value?.meta).groupingFieldColumn! as ColumnType)
.groupingFieldColumn! as ColumnType)
groupingField.value = groupingFieldColumn.value.title! 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 { 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 { IsPublicInj, ref, storeToRefs, useInjectionState, useMetas, useProject } from '#imports'
import type { Row } from '~/lib' 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>[]) => const formatData = (list: Record<string, any>[]) =>
list.map( list.map(
(row) => (row) =>
@ -22,7 +15,7 @@ const formatData = (list: Record<string, any>[]) =>
const [useProvideMapViewStore, useMapViewStore] = useInjectionState( 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>, viewMeta: Ref<(ViewType | MapType | undefined) & { id: string }> | ComputedRef<(ViewType & { id: string }) | undefined>,
shared = false, shared = false,
where?: ComputedRef<string | undefined>, where?: ComputedRef<string | undefined>,
@ -111,7 +104,7 @@ const [useProvideMapViewStore, useMapViewStore] = useInjectionState(
if (currentRow.rowMeta) currentRow.rowMeta.saving = true if (currentRow.rowMeta) currentRow.rowMeta.saving = true
try { try {
const { missingRequiredColumns, insertObj } = await populateInsertObject({ const { missingRequiredColumns, insertObj } = await populateInsertObject({
meta: metaValue!, meta: metaValue as TableType,
ltarState, ltarState,
getMeta, getMeta,
row, row,
@ -162,7 +155,6 @@ const [useProvideMapViewStore, useMapViewStore] = useInjectionState(
geoDataFieldColumn, geoDataFieldColumn,
addEmptyRow, addEmptyRow,
insertRow, insertRow,
geodataToggleState,
syncCount, syncCount,
paginationData, paginationData,
} }

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

@ -1,6 +1,6 @@
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import type { WatchStopHandle } from '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' import { extractSdkResponseErrorMsg, storeToRefs, useNuxtApp, useProject, useState, watch } from '#imports'
export function useMetas() { 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 // 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 if (!tableIdOrTitle) return null
/** wait until loading is finished if requesting same meta */ /** wait until loading is finished if requesting same meta */
if (!force && loadingState.value[tableIdOrTitle]) { 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 type { ColumnType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import type { AppInfo } from '~/composables/useGlobal' import type { AppInfo } from '~/composables/useGlobal'
import { parseProp } from '#imports'
export default function convertCellData( export default function convertCellData(
args: { from: UITypes; to: UITypes; value: any; column: ColumnType; appInfo: AppInfo }, args: { from: UITypes; to: UITypes; value: any; column: ColumnType; appInfo: AppInfo },
@ -73,7 +74,7 @@ export default function convertCellData(
case UITypes.Attachment: { case UITypes.Attachment: {
let parsedVal let parsedVal
try { try {
parsedVal = typeof value === 'string' ? JSON.parse(value) : value parsedVal = parseProp(value)
parsedVal = Array.isArray(parsedVal) ? parsedVal : [parsedVal] parsedVal = Array.isArray(parsedVal) ? parsedVal : [parsedVal]
} catch (e) { } catch (e) {
throw new Error('Invalid attachment data') throw new Error('Invalid attachment data')
@ -94,7 +95,7 @@ export default function convertCellData(
const attachmentMeta = { const attachmentMeta = {
...defaultAttachmentMeta, ...defaultAttachmentMeta,
...(typeof args.column?.meta === 'string' ? JSON.parse(args.column.meta) : args.column?.meta), ...parseProp(args.column?.meta),
} }
const attachments = [] const attachments = []

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

@ -1,7 +1,7 @@
import useVuelidate from '@vuelidate/core' import useVuelidate from '@vuelidate/core'
import { helpers, minLength, required } from '@vuelidate/validators' import { helpers, minLength, required } from '@vuelidate/validators'
import type { Ref } from 'vue' 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 { ErrorMessages, RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { isString } from '@vueuse/core' import { isString } from '@vueuse/core'
import { import {
@ -37,7 +37,8 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const sharedView = ref<ViewType>() const sharedView = ref<ViewType>()
const sharedFormView = ref<FormType>() const sharedFormView = ref<FormType>()
const meta = ref<TableType>() 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 sharedViewMeta = ref<SharedViewMeta>({})
const formResetHook = createEventHook<void>() const formResetHook = createEventHook<void>()

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

@ -10,7 +10,7 @@ import type {
ViewType, ViewType,
} from 'nocodb-sdk' } from 'nocodb-sdk'
import { UITypes, ViewTypes } 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() { export function useSharedView() {
const nestedFilters = ref<(FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string })[]>([]) const nestedFilters = ref<(FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string })[]>([])
@ -63,7 +63,7 @@ export function useSharedView() {
}, },
}) })
try { try {
allowCSVDownload.value = (typeof viewMeta.meta === 'string' ? JSON.parse(viewMeta.meta) : viewMeta.meta)?.allowCSVDownload allowCSVDownload.value = parseProp(viewMeta.meta)?.allowCSVDownload
} catch { } catch {
allowCSVDownload.value = false 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 { UITypes, isSystemColumn } from 'nocodb-sdk'
import { import {
Modal, Modal,
@ -40,14 +40,14 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void, baseId?
const createTable = async () => { const createTable = async () => {
if (!sqlUi?.value) return 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')) { if (col.column_name === 'id' && table.columns.includes('id_ag')) {
Object.assign(col, sqlUi?.value?.getDataTypeForUiType({ uidt: UITypes.ID }, 'AG')) Object.assign(col, sqlUi?.value?.getDataTypeForUiType({ uidt: UITypes.ID }, 'AG'))
col.dtxp = sqlUi?.value?.getDefaultLengthForDatatype(col.dt) col.dtxp = sqlUi?.value?.getDefaultLengthForDatatype(col.dt)
col.dtxs = sqlUi?.value?.getDefaultScaleForDatatype(col.dt) col.dtxs = sqlUi?.value?.getDefaultScaleForDatatype(col.dt)
return true return true
} }
return table.columns.includes(col.column_name) return table.columns.includes(col.column_name!)
}) })
try { try {

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

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

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

@ -162,10 +162,9 @@ export function useViewFilters(
const placeholderFilter = (): Filter => { const placeholderFilter = (): Filter => {
return { return {
// TODO: fix type
comparison_op: comparisonOpList(options.value?.[0].uidt as UITypes).filter((compOp) => comparison_op: comparisonOpList(options.value?.[0].uidt as UITypes).filter((compOp) =>
isComparisonOpAllowed({ fk_column_id: options.value?.[0].id }, compOp), isComparisonOpAllowed({ fk_column_id: options.value?.[0].id }, compOp),
)?.[0].value, )?.[0].value as FilterType['comparison_op'],
value: '', value: '',
status: 'create', status: 'create',
logical_op: 'and', logical_op: 'and',
@ -281,7 +280,6 @@ export function useViewFilters(
comparison: filter.comparison_op, comparison: filter.comparison_op,
}) })
} else { } else {
// todo: return type of dbTableFilter is void?
filters.value[i] = await $api.dbTableFilter.create(view.value.id!, { filters.value[i] = await $api.dbTableFilter.create(view.value.id!, {
...filter, ...filter,
fk_parent_id: parentId, 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 type { MaybeRef } from '@vueuse/core'
import { ref, unref, useNuxtApp, watch } from '#imports' 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 views = ref<ViewType[]>([])
const isLoading = ref(false) const isLoading = ref(false)
@ -26,3 +26,5 @@ export function useViews(meta: MaybeRef<TableType | undefined>) {
return { views, isLoading, loadViews } 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 ActiveCellInj: InjectionKey<Ref<boolean>> = Symbol('active-cell')
export const IsPublicInj: InjectionKey<Ref<boolean>> = Symbol('is-public') export const IsPublicInj: InjectionKey<Ref<boolean>> = Symbol('is-public')
export const RowInj: InjectionKey<Ref<Row>> = Symbol('row') 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 MetaInj: InjectionKey<ComputedRef<TableType>> = Symbol('meta-injection')
export const TabMetaInj: InjectionKey<ComputedRef<TabItem>> = Symbol('tab-meta-injection') export const TabMetaInj: InjectionKey<ComputedRef<TabItem>> = Symbol('tab-meta-injection')
export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['paginationData']> = export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['paginationData']> =

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

@ -210,7 +210,7 @@
"advancedSettings": "Advanced Settings", "advancedSettings": "Advanced Settings",
"codeSnippet": "Code Snippet", "codeSnippet": "Code Snippet",
"keyboardShortcut": "Keyboard Shortcuts", "keyboardShortcut": "Keyboard Shortcuts",
"generateRandomName": "Generate Random Name", "generateRandomName": "Generate Random Name",
"findRowByScanningCode": "Find row by scanning a QR or Barcode" "findRowByScanningCode": "Find row by scanning a QR or Barcode"
}, },
"labels": { "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 { I18n } from 'vue-i18n'
import type { Theme as AntTheme } from 'ant-design-vue/es/config-provider' import type { Theme as AntTheme } from 'ant-design-vue/es/config-provider'
import type { UploadFile } from 'ant-design-vue' import type { UploadFile } from 'ant-design-vue'
@ -21,7 +21,7 @@ export interface ProjectMetaInfo {
Platform?: string Platform?: string
Docker?: boolean Docker?: boolean
Database?: string Database?: string
ProjectOnRootDB?: string ProjectOnRootDB?: boolean
RootDB?: string RootDB?: string
PackageVersion?: string PackageVersion?: string
} }
@ -79,7 +79,7 @@ export interface TabItem {
viewId?: string viewId?: string
sortsState?: Map<string, any> sortsState?: Map<string, any>
filterState?: Map<string, any> filterState?: Map<string, any>
meta?: Record<string, any> meta?: MetaType
} }
export interface SharedViewMeta extends Record<string, any> { 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) signIn(token)
} catch (e) { } catch (e: any) {
message.error({ content: await extractSdkResponseErrorMsg(e) }) message.error(await extractSdkResponseErrorMsg(e))
} }
const newURL = window.location.href.split('?')[0] 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", "unique-names-generator": "^4.7.1",
"v3-infinite-loading": "^1.2.2", "v3-infinite-loading": "^1.2.2",
"validator": "^13.7.0", "validator": "^13.7.0",
"vue-barcode-reader": "^1.0.3",
"vue-dompurify-html": "^3.0.0", "vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.0.3", "vue-github-button": "^3.0.3",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",
"vue-qrcode-reader": "3.1.0-vue3-compatibility.2",
"vue3-contextmenu": "^0.2.12", "vue3-contextmenu": "^0.2.12",
"vue3-text-clamp": "^0.1.1", "vue3-text-clamp": "^0.1.1",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
@ -96,14 +98,17 @@
"@intlify/vite-plugin-vue-i18n": "^6.0.1", "@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@nuxt/image-edge": "^1.0.0-27657146.da85542", "@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"@types/d3-scale": "^4.0.3",
"@types/dagre": "^0.7.48", "@types/dagre": "^0.7.48",
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.5",
"@types/leaflet": "^1.9.0", "@types/leaflet": "^1.9.0",
"@types/leaflet.markercluster": "^1.5.1", "@types/leaflet.markercluster": "^1.5.1",
"@types/papaparse": "^5.3.2", "@types/papaparse": "^5.3.2",
"@types/qrcode": "^1.5.0",
"@types/sortablejs": "^1.13.0", "@types/sortablejs": "^1.13.0",
"@types/tinycolor2": "^1.4.3", "@types/tinycolor2": "^1.4.3",
"@types/validator": "^13.7.10", "@types/validator": "^13.7.10",
"@types/vue-barcode-reader": "^0.0.0",
"@vitest/ui": "^0.18.0", "@vitest/ui": "^0.18.0",
"@vue/compiler-sfc": "^3.2.37", "@vue/compiler-sfc": "^3.2.37",
"@vue/test-utils": "^2.0.2", "@vue/test-utils": "^2.0.2",

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

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import tinycolor from 'tinycolor2' import tinycolor from 'tinycolor2'
import type { TableType } from 'nocodb-sdk'
import { import {
TabType, TabType,
computed, computed,
@ -41,11 +42,13 @@ const { t } = useI18n()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { betaFeatureToggleState } = useBetaFeatureToggle()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const { appInfo, token, signOut, signedIn, user, currentVersion } = useGlobal() const { appInfo, token, signOut, signedIn, user, currentVersion, isMobileMode, setIsMobileMode } = useGlobal()
const projectStore = useProject() const projectStore = useProject()
@ -142,7 +145,7 @@ const copyProjectInfo = async () => {
// Copied to clipboard // Copied to clipboard
message.info(t('msg.info.copiedToClipboard')) message.info(t('msg.info.copiedToClipboard'))
} }
} catch (e) { } catch (e: any) {
console.error(e) console.error(e)
message.error(e.message) message.error(e.message)
} }
@ -360,7 +363,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<!-- Swagger: Rest APIs --> <!-- Swagger: Rest APIs -->
<a-menu-item key="api"> <a-menu-item key="api">
<div <div
v-if="isUIAllowed('apiDocs')" v-if="isUIAllowed('apiDocs') && !isMobileMode"
v-e="['e:api-docs']" v-e="['e:api-docs']"
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.ncSiteUrl)" @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 --> <!-- Copy Auth Token -->
<a-menu-item key="copy"> <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" /> <MdiScriptTextKeyOutline class="group-hover:text-accent" />
{{ $t('activity.account.authToken') }} {{ $t('activity.account.authToken') }}
</div> </div>
@ -383,7 +391,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<!-- Team & Settings --> <!-- Team & Settings -->
<a-menu-item key="teamAndSettings"> <a-menu-item key="teamAndSettings">
<div <div
v-if="isUIAllowed('settings')" v-if="isUIAllowed('settings') && !isMobileMode"
v-e="['c:navdraw:project-settings']" v-e="['c:navdraw:project-settings']"
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click="toggleDialog(true, 'teamAndAuth')" @click="toggleDialog(true, 'teamAndAuth')"
@ -393,6 +401,18 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
</div> </div>
</a-menu-item> </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 --> <!-- Theme -->
<template v-if="isUIAllowed('projectTheme')"> <template v-if="isUIAllowed('projectTheme')">
<a-sub-menu key="theme"> <a-sub-menu key="theme">
@ -470,7 +490,7 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
<a-menu-divider /> <a-menu-divider />
<!-- Preview As --> <!-- 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> <template #title>
<div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group"> <div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<MdiFileEyeOutline class="group-hover:text-accent" /> <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 { closeTab } = tabStore
const { tabs, activeTabIndex, activeTab } = storeToRefs(tabStore) const { tabs, activeTabIndex, activeTab } = storeToRefs(tabStore)
const { isLoading } = useGlobal() const { isLoading, isMobileMode } = useGlobal()
provide(TabMetaInj, activeTab) provide(TabMetaInj, activeTab)
@ -28,6 +28,12 @@ const { isOpen, toggle } = useSidebar('nc-left-sidebar')
function onEdit(targetKey: number, action: 'add' | 'remove' | string) { function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
if (action === 'remove') closeTab(targetKey) if (action === 'remove') closeTab(targetKey)
} }
const hideSidebarOnClickOrTouchIfMobileMode = () => {
if (isMobileMode.value && isOpen.value) {
toggle(false)
}
}
</script> </script>
<template> <template>
@ -81,10 +87,10 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
</div> </div>
<LazyGeneralShareBaseButton class="mb-1px" /> <LazyGeneralShareBaseButton class="mb-1px" />
<LazyGeneralFullScreen class="nc-fullscreen-icon mb-1px" /> <LazyGeneralFullScreen v-if="!isMobileMode" class="nc-fullscreen-icon mb-1px" />
</div> </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}`" /> <NuxtPage :page-key="`${$route.params.projectId}.${$route.name}`" />
</div> </div>
</div> </div>

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

@ -1,5 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { ref } from 'vue'
import { StreamBarcodeReader } from 'vue-barcode-reader'
import { useSharedFormStoreOrThrow } from '#imports' import { useSharedFormStoreOrThrow } from '#imports'
const { sharedFormView, submitForm, v$, formState, notFound, formColumns, submitted, secondsRemain, isLoading } = 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)) 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> </script>
<template> <template>
@ -55,6 +103,20 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
</template> </template>
<template v-else> <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> <GeneralOverlay class="bg-gray-400/75" :model-value="isLoading" inline transition>
<div class="w-full h-full flex items-center justify-center"> <div class="w-full h-full flex items-center justify-center">
<a-spin size="large" /> <a-spin size="large" />
@ -82,24 +144,36 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
</div> </div>
<div> <div>
<LazySmartsheetVirtualCell <div class="flex">
v-if="isVirtualCol(field)" <LazySmartsheetVirtualCell
:model-value="null" v-if="isVirtualCol(field)"
class="mt-0 nc-input nc-cell" :model-value="null"
:data-testid="`nc-form-input-cell-${field.label || field.title}`" class="mt-0 nc-input nc-cell"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`" :data-testid="`nc-form-input-cell-${field.label || field.title}`"
:column="field" :class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
/> :column="field"
/>
<LazySmartsheetCell
v-else <LazySmartsheetCell
v-model="formState[field.title]" v-else
class="nc-input" v-model="formState[field.title]"
:data-testid="`nc-form-input-cell-${field.label || field.title}`" class="nc-input"
:class="`nc-form-input-${field.title?.replaceAll(' ', '')}`" :data-testid="`nc-form-input-cell-${field.label || field.title}`"
:column="field" :class="`nc-form-input-${field.title?.replaceAll(' ', '')}`"
:edit-enabled="true" :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 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"> <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> <style lang="scss" scoped>
:deep(.nc-cell .nc-action-icon) { :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> </style>

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

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ProjectType } from 'nocodb-sdk'
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select' import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select'
import type { DefaultConnection, ProjectCreateForm } from '#imports' import type { DefaultConnection, ProjectCreateForm } from '#imports'
import { import {
@ -233,7 +234,7 @@ const createProject = async () => {
const config = { ...formState.dataSource, connection } const config = { ...formState.dataSource, connection }
const result = await api.project.create({ const result = (await api.project.create({
title: formState.title, title: formState.title,
bases: [ bases: [
{ {
@ -244,7 +245,7 @@ const createProject = async () => {
}, },
], ],
external: true, external: true,
}) })) as Partial<ProjectType>
$e('a:project:create:extdb') $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 { Form, Input } from 'ant-design-vue'
import type { RuleObject } from 'ant-design-vue/es/form' import type { RuleObject } from 'ant-design-vue/es/form'
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import type { ProjectType } from 'nocodb-sdk'
import { import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
generateUniqueName, generateUniqueName,
@ -44,9 +45,9 @@ const createProject = async () => {
try { try {
creating.value = true creating.value = true
const result = await api.project.create({ const result = (await api.project.create({
title: formState.title, title: formState.title,
}) })) as Partial<ProjectType>
await navigateTo(`/nc/${result.id}`) await navigateTo(`/nc/${result.id}`)
} catch (e: any) { } catch (e: any) {

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

@ -11,6 +11,7 @@ import {
message, message,
navigateTo, navigateTo,
onBeforeMount, onBeforeMount,
parseProp,
projectThemeColors, projectThemeColors,
ref, ref,
themeV2Colors, themeV2Colors,
@ -80,7 +81,7 @@ const handleProjectColor = async (projectId: string, color: string) => {
const project: ProjectType = await $api.project.read(projectId) 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, { await $api.project.update(projectId, {
color, color,
@ -113,7 +114,7 @@ const handleProjectColor = async (projectId: string, color: string) => {
const getProjectPrimary = (project: ProjectType) => { const getProjectPrimary = (project: ProjectType) => {
if (!project) return 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 return meta.theme?.primaryColor || themeV2Colors['royal-blue'].DEFAULT
} }
@ -136,7 +137,7 @@ const copyProjectMeta = async () => {
const aggregatedMetaInfo = await $api.utils.aggregatedMetaInfo() const aggregatedMetaInfo = await $api.utils.aggregatedMetaInfo()
await copy(JSON.stringify(aggregatedMetaInfo)) await copy(JSON.stringify(aggregatedMetaInfo))
message.info('Copied aggregated project meta to clipboard') message.info('Copied aggregated project meta to clipboard')
} catch (e) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) 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) { async function loadProjectMetaInfo(force?: boolean) {
if (!projectMetaInfo.value || force) { 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') { if (data.meta && typeof data.meta === 'string') {
await api.project.update(projectId.value, data) await api.project.update(projectId.value, data)
} else { } 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 { 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[]) => { export const extractPkFromRow = (row: Record<string, any>, columns: ColumnType[]) => {
return ( return (
@ -21,7 +21,7 @@ export async function populateInsertObject({
}: { }: {
meta: TableType meta: TableType
ltarState: Record<string, any> 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> row: Record<string, any>
throwError?: boolean 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) => { const getEqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType)) { if (isNumericCol(fieldUiType) || fieldUiType === UITypes.Time) {
return '=' return '='
} else if ( } else if (
[UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord, UITypes.Date, UITypes.DateTime].includes( [UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord, UITypes.Date, UITypes.DateTime].includes(
@ -14,7 +14,7 @@ const getEqText = (fieldUiType: UITypes) => {
} }
const getNeqText = (fieldUiType: UITypes) => { const getNeqText = (fieldUiType: UITypes) => {
if (isNumericCol(fieldUiType)) { if (isNumericCol(fieldUiType) || fieldUiType === UITypes.Time) {
return '!=' return '!='
} else if ( } else if (
[UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord, UITypes.Date, UITypes.DateTime].includes( [UITypes.SingleSelect, UITypes.Collaborator, UITypes.LinkToAnotherRecord, UITypes.Date, UITypes.DateTime].includes(
@ -112,6 +112,7 @@ export const comparisonOpList = (
UITypes.Collaborator, UITypes.Collaborator,
UITypes.Date, UITypes.Date,
UITypes.DateTime, UITypes.DateTime,
UITypes.Time,
...numericUITypes, ...numericUITypes,
], ],
}, },
@ -126,6 +127,7 @@ export const comparisonOpList = (
UITypes.Collaborator, UITypes.Collaborator,
UITypes.Date, UITypes.Date,
UITypes.DateTime, UITypes.DateTime,
UITypes.Time,
...numericUITypes, ...numericUITypes,
], ],
}, },
@ -143,6 +145,7 @@ export const comparisonOpList = (
UITypes.Lookup, UITypes.Lookup,
UITypes.Date, UITypes.Date,
UITypes.DateTime, UITypes.DateTime,
UITypes.Time,
...numericUITypes, ...numericUITypes,
], ],
}, },
@ -160,6 +163,7 @@ export const comparisonOpList = (
UITypes.Lookup, UITypes.Lookup,
UITypes.Date, UITypes.Date,
UITypes.DateTime, UITypes.DateTime,
UITypes.Time,
...numericUITypes, ...numericUITypes,
], ],
}, },
@ -178,6 +182,7 @@ export const comparisonOpList = (
UITypes.Lookup, UITypes.Lookup,
UITypes.Date, UITypes.Date,
UITypes.DateTime, UITypes.DateTime,
UITypes.Time,
], ],
}, },
{ {
@ -195,6 +200,7 @@ export const comparisonOpList = (
UITypes.Lookup, UITypes.Lookup,
UITypes.Date, UITypes.Date,
UITypes.DateTime, UITypes.DateTime,
UITypes.Time,
], ],
}, },
{ {
@ -225,25 +231,25 @@ export const comparisonOpList = (
text: getGtText(fieldUiType), text: getGtText(fieldUiType),
value: 'gt', value: 'gt',
ignoreVal: false, ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime], includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
}, },
{ {
text: getLtText(fieldUiType), text: getLtText(fieldUiType),
value: 'lt', value: 'lt',
ignoreVal: false, ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime], includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
}, },
{ {
text: getGteText(fieldUiType), text: getGteText(fieldUiType),
value: 'gte', value: 'gte',
ignoreVal: false, ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime], includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
}, },
{ {
text: getLteText(fieldUiType), text: getLteText(fieldUiType),
value: 'lte', value: 'lte',
ignoreVal: false, ignoreVal: false,
includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime], includedTypes: [...numericUITypes, UITypes.Date, UITypes.DateTime, UITypes.Time],
}, },
{ {
text: 'is within', text: 'is within',

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

@ -22,3 +22,4 @@ export * from './memStorage'
export * from './browserUtils' export * from './browserUtils'
export * from './geoDataUtils' export * from './geoDataUtils'
export * from './mimeTypeUtils' 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 uidt === UITypes.LinkToAnotherRecord
export const isHm = (column: ColumnType) => 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) => 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) => 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 isLookup = (column: ColumnType) => column.uidt === UITypes.Lookup
export const isRollup = (column: ColumnType) => column.uidt === UITypes.Rollup 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; required?: BoolType;
/** Is this column shown in Form? */ /** Is this column shown in Form? */
show?: BoolType; 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) */ /** Form Column UUID (Not in use) */
uuid?: StringOrNullType; uuid?: StringOrNullType;
} }
@ -1697,7 +1702,9 @@ export interface PluginTestReqType {
/** Plugin Title */ /** Plugin Title */
title: string; title: string;
/** Plugin Input as JSON string */ /** Plugin Input as JSON string */
input: string; input: string | object;
/** @example Email */
category: string;
} }
/** /**
@ -1775,6 +1782,24 @@ export interface ProjectReqType {
title: string; 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 * Model for Project User Request
*/ */
@ -2927,10 +2952,21 @@ export class Api<
* @request POST:/api/v1/db/meta/projects/{projectId}/users * @request POST:/api/v1/db/meta/projects/{projectId}/users
* @response `200` `{ * @response `200` `{
\** \**
* Success Message * Success Message for inviting single email
* @example The user has been invited successfully * @example The user has been invited successfully
*\ *\
msg?: string, 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 }` OK
* @response `400` `{ * @response `400` `{
@ -2947,10 +2983,20 @@ export class Api<
this.request< this.request<
{ {
/** /**
* Success Message * Success Message for inviting single email
* @example The user has been invited successfully * @example The user has been invited successfully
*/ */
msg?: string; 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> */ /** @example BadRequest [Error]: <ERROR MESSAGE> */
@ -3904,16 +3950,20 @@ export class Api<
* @name Update * @name Update
* @summary Update Project * @summary Update Project
* @request PATCH:/api/v1/db/meta/projects/{projectId} * @request PATCH:/api/v1/db/meta/projects/{projectId}
* @response `200` `void` OK * @response `200` `number` OK
* @response `400` `{ * @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\ \** @example BadRequest [Error]: <ERROR MESSAGE> *\
msg: string, msg: string,
}` }`
*/ */
update: (projectId: IdType, data: number, params: RequestParams = {}) => update: (
projectId: IdType,
data: ProjectUpdateReqType,
params: RequestParams = {}
) =>
this.request< this.request<
void, number,
{ {
/** @example BadRequest [Error]: <ERROR MESSAGE> */ /** @example BadRequest [Error]: <ERROR MESSAGE> */
msg: string; msg: string;
@ -3923,6 +3973,7 @@ export class Api<
method: 'PATCH', method: 'PATCH',
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
format: 'json',
...params, ...params,
}), }),
@ -8319,6 +8370,7 @@ export class Api<
ee?: boolean, ee?: boolean,
ncAttachmentFieldSize?: number, ncAttachmentFieldSize?: number,
ncMaxAttachmentsAllowed?: number, ncMaxAttachmentsAllowed?: number,
isCloud?: boolean,
}` OK }` OK
* @response `400` `{ * @response `400` `{
@ -8347,6 +8399,7 @@ export class Api<
ee?: boolean; ee?: boolean;
ncAttachmentFieldSize?: number; ncAttachmentFieldSize?: number;
ncMaxAttachmentsAllowed?: number; ncMaxAttachmentsAllowed?: number;
isCloud?: boolean;
}, },
{ {
/** @example BadRequest [Error]: <ERROR MESSAGE> */ /** @example BadRequest [Error]: <ERROR MESSAGE> */

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

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

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

@ -9,4 +9,6 @@ export const AllowedColumnTypesForQrAndBarcodes = [
UITypes.Email, UITypes.Email,
UITypes.Decimal, UITypes.Decimal,
UITypes.Number, 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 (node.type === 'Literal') {
if (typeof node.value === 'string') { if (typeof node.value === 'string') {
return String.raw`"${escapeDoubleQuotes(node.value)}"`; return String.raw`"${escapeLiteral(node.value)}"`;
} }
return '' + node.value; return '' + node.value;
} }
@ -214,6 +214,14 @@ export function jsepTreeToFormula(node) {
return ''; return '';
} }
function escapeDoubleQuotes(v: string) { function escapeLiteral(v: string) {
return v.replace(/"/g, '\\"'); 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); qb = qb.whereNull(customWhereClause || field);
if ( if (
!isNumericCol(column.uidt) && !isNumericCol(column.uidt) &&
![UITypes.Date, UITypes.DateTime].includes(column.uidt) ![UITypes.Date, UITypes.DateTime, UITypes.Time].includes(
column.uidt
)
) { ) {
qb = qb.orWhere(field, ''); qb = qb.orWhere(field, '');
} }
@ -707,7 +709,9 @@ const parseConditionV2 = async (
qb = qb.whereNotNull(customWhereClause || field); qb = qb.whereNotNull(customWhereClause || field);
if ( if (
!isNumericCol(column.uidt) && !isNumericCol(column.uidt) &&
![UITypes.Date, UITypes.DateTime].includes(column.uidt) ![UITypes.Date, UITypes.DateTime, UITypes.Time].includes(
column.uidt
)
) { ) {
qb = qb.whereNot(field, ''); 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_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_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_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 // Create a custom migration source class
export default class XcMigrationSourcev2 { export default class XcMigrationSourcev2 {
@ -41,6 +42,7 @@ export default class XcMigrationSourcev2 {
'nc_025_add_row_height', 'nc_025_add_row_height',
'nc_026_map_view', 'nc_026_map_view',
'nc_027_add_comparison_sub_op', '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; return nc_026_map_view;
case 'nc_027_add_comparison_sub_op': case 'nc_027_add_comparison_sub_op':
return 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; help?: StringOrNullType;
description?: StringOrNullType; description?: StringOrNullType;
required?: BoolType; required?: BoolType;
enable_scanner?: BoolType;
uuid?: StringOrNullType; uuid?: StringOrNullType;
show?: BoolType; show?: BoolType;
order?: number; order?: number;
@ -68,6 +69,7 @@ export default class FormViewColumn implements FormColumnType {
'help', 'help',
'description', 'description',
'required', 'required',
'enable_scanner',
'meta', 'meta',
]); ]);
@ -166,6 +168,7 @@ export default class FormViewColumn implements FormColumnType {
'show', 'show',
'order', 'order',
'meta', 'meta',
'enable_scanner',
]); ]);
// get existing cache // 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 syncMigration from '../meta/helpers/syncMigration';
import { populateMeta, validatePayload } from '../meta/api/helpers'; import { populateMeta, validatePayload } from '../meta/api/helpers';
import { extractPropsAndSanitize } from '../meta/helpers/extractProps'; import { extractPropsAndSanitize } from '../meta/helpers/extractProps';
import type { ProjectReqType } from 'nocodb-sdk'; import type { ProjectReqType, ProjectUpdateReqType } from 'nocodb-sdk';
export async function projectCreate(param: { export async function projectCreate(param: {
project: ProjectReqType; project: ProjectReqType;
@ -136,8 +136,13 @@ export function sanitizeProject(project: any) {
export async function projectUpdate(param: { export async function projectUpdate(param: {
projectId: string; projectId: string;
project: ProjectReqType; project: ProjectUpdateReqType;
}) { }) {
validatePayload(
'swagger.json#/components/schemas/ProjectUpdateReq',
param.project
);
const project = await Project.getWithInfo(param.projectId); const project = await Project.getWithInfo(param.projectId);
const data: Partial<Project> = extractPropsAndSanitize( 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 { NcUpgraderCtx } from './NcUpgrader';
import type NcMetaIO from '../meta/NcMetaIO'; 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 // `removeLikeAndNlikeFilters` in this upgrader is simply to remove them
// besides, `null` and `empty` will be migrated to `blank` in `migrateEmptyAndNullFilters` // 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` // - remove `is like` and `is not like`
// - migrate `null` or `empty` filters to `blank` // - migrate `null` or `empty` filters to `blank`
// - add `exact date` in comparison_sub_op for existing filters `eq` and `neq` // - 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) { function removeLikeAndNlikeFilters(filter: Filter, ncMeta: NcMetaIO) {
const actions = []; const actions = [];
@ -88,6 +91,11 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
...migrateEmptyAndNullFilters(filter, ncMeta), ...migrateEmptyAndNullFilters(filter, ncMeta),
...migrateEqAndNeqFilters(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