Browse Source

Merge branch 'develop' into fix/runtime-directive-warnings

pull/6954/head
աɨռɢӄաօռɢ 12 months ago
parent
commit
629a06304c
  1. 2
      .github/uffizzi/docker-compose.uffizzi.yml
  2. 2
      README.md
  3. 2
      docker-compose/mysql/docker-compose.yml
  4. 2
      docker-compose/nginx-proxy-manager/docker-compose.yml
  5. 2
      package.json
  6. 4
      packages/nc-gui/assets/style.scss
  7. 3
      packages/nc-gui/components.d.ts
  8. 8
      packages/nc-gui/components/cell/Json.vue
  9. 12
      packages/nc-gui/components/cell/TextArea.vue
  10. 125
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  11. 7
      packages/nc-gui/components/dlg/AirtableImport.vue
  12. 14
      packages/nc-gui/components/dlg/ColumnDuplicate.vue
  13. 14
      packages/nc-gui/components/dlg/QuickImport.vue
  14. 2
      packages/nc-gui/components/smartsheet/Cell.vue
  15. 44
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  16. 11
      packages/nc-gui/components/smartsheet/details/Fields.vue
  17. 17
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  18. 39
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  19. 28
      packages/nc-gui/components/smartsheet/grid/GroupByLabel.vue
  20. 20
      packages/nc-gui/components/smartsheet/header/Menu.vue
  21. 50
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  22. 69
      packages/nc-gui/components/template/Editor.vue
  23. 6
      packages/nc-gui/components/virtual-cell/Lookup.vue
  24. 8
      packages/nc-gui/components/virtual-cell/components/Header.vue
  25. 2
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  26. 69
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  27. 10
      packages/nc-gui/composables/useColumnCreateStore.ts
  28. 10
      packages/nc-gui/composables/useViewGroupBy.ts
  29. 5
      packages/nc-gui/lang/en.json
  30. 2
      packages/nc-gui/lib/enums.ts
  31. 102
      packages/nc-gui/package.json
  32. 11
      packages/nc-gui/store/sidebar.ts
  33. 22
      packages/nc-gui/utils/validation.ts
  34. 2
      packages/nc-lib-gui/package.json
  35. 2
      packages/noco-docs/docs/030.workspaces/040.actions-on-workspace.md
  36. 2
      packages/noco-docs/docs/040.bases/070.actions-on-base.md
  37. 2
      packages/noco-docs/docs/050.tables/060.actions-on-table.md
  38. 12
      packages/noco-docs/docs/070.fields/040.field-types/050.custom-types/010.attachment.md
  39. 2
      packages/noco-docs/docs/070.fields/040.field-types/060.formula/015.operators.md
  40. 16
      packages/noco-docs/docs/070.fields/040.field-types/060.formula/040.date-functions.md
  41. 2
      packages/noco-docs/docs/070.fields/060.actions-on-field.md
  42. 4
      packages/noco-docs/docs/080.records/070.actions-on-record.md
  43. 4
      packages/noco-docs/docs/090.views/040.view-types/030.form.md
  44. 2
      packages/noco-docs/docs/090.views/090.actions-on-view.md
  45. 6
      packages/noco-docs/docs/150.engineering/060.builds-and-releases.md
  46. 2
      packages/noco-docs/docs/150.engineering/070.translation.md
  47. 7586
      packages/noco-docs/package-lock.json
  48. 30
      packages/noco-docs/package.json
  49. 18
      packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/090.formulas.md
  50. 14
      packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/160.views.md
  51. 6
      packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/200.import-airtable-to-sql-database-within-a-minute-for-free.md
  52. 4
      packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/230.team-and-auth.md
  53. 246
      packages/noco-docs/versioned_docs/version-0.109.7/040.developer-resources/020.rest-apis.md
  54. 2
      packages/noco-docs/versioned_docs/version-0.109.7/040.developer-resources/030.sdk.md
  55. 18
      packages/noco-docs/versioned_docs/version-0.109.7/040.developer-resources/040.webhooks.md
  56. 12
      packages/nocodb-sdk/package.json
  57. 123
      packages/nocodb/package.json
  58. 39
      packages/nocodb/src/controllers/api-docs/api-docs.controller.ts
  59. 66
      packages/nocodb/src/db/BaseModelSqlv2.ts
  60. 46
      packages/nocodb/src/db/conditionV2.ts
  61. 163
      packages/nocodb/src/db/generateBTLookupSelectQuery.ts
  62. 399
      packages/nocodb/src/db/generateLookupSelectQuery.ts
  63. 4
      packages/nocodb/src/db/sortV2.ts
  64. 2
      packages/nocodb/src/helpers/catchError.ts
  65. 9
      packages/nocodb/src/helpers/getAst.ts
  66. 7
      packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts
  67. 4
      packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
  68. 11
      packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts
  69. 1
      packages/nocodb/src/plugins/backblaze/Backblaze.ts
  70. 1
      packages/nocodb/src/plugins/gcs/Gcs.ts
  71. 1
      packages/nocodb/src/plugins/linode/LinodeObjectStorage.ts
  72. 1
      packages/nocodb/src/plugins/mino/Minio.ts
  73. 1
      packages/nocodb/src/plugins/ovhCloud/OvhCloud.ts
  74. 12
      packages/nocodb/src/plugins/s3/S3.ts
  75. 1
      packages/nocodb/src/plugins/scaleway/ScalewayObjectStorage.ts
  76. 1
      packages/nocodb/src/plugins/spaces/Spaces.ts
  77. 1
      packages/nocodb/src/plugins/upcloud/UpoCloud.ts
  78. 1
      packages/nocodb/src/plugins/vultr/Vultr.ts
  79. 30
      packages/nocodb/src/services/api-docs/api-docs.service.ts
  80. 2
      packages/nocodb/src/services/api-docs/swagger/templates/paths.ts
  81. 28
      packages/nocodb/src/services/api-docs/swaggerV2/getPaths.ts
  82. 29
      packages/nocodb/src/services/api-docs/swaggerV2/getSchemas.ts
  83. 67
      packages/nocodb/src/services/api-docs/swaggerV2/getSwaggerColumnMetas.ts
  84. 66
      packages/nocodb/src/services/api-docs/swaggerV2/getSwaggerJSONV2.ts
  85. 128
      packages/nocodb/src/services/api-docs/swaggerV2/swagger-base.json
  86. 10
      packages/nocodb/src/services/api-docs/swaggerV2/templates/headers.ts
  87. 237
      packages/nocodb/src/services/api-docs/swaggerV2/templates/params.ts
  88. 407
      packages/nocodb/src/services/api-docs/swaggerV2/templates/paths.ts
  89. 109
      packages/nocodb/src/services/api-docs/swaggerV2/templates/schemas.ts
  90. 3
      packages/nocodb/src/services/datas.service.ts
  91. 4
      packages/nocodb/src/services/tables.service.ts
  92. 5
      packages/nocodb/src/utils/globals.ts
  93. 10
      packages/nocodb/tests/unit/rest/tests/groupby.test.ts
  94. 3496
      pnpm-lock.yaml
  95. 3
      renovate.json
  96. 8
      scripts/pkg-executable/package.json
  97. 6
      tests/playwright/pages/Dashboard/Import/ImportTemplate.ts

2
.github/uffizzi/docker-compose.uffizzi.yml

@ -31,7 +31,7 @@ services:
MYSQL_PASSWORD: password MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password MYSQL_ROOT_PASSWORD: password
MYSQL_USER: noco MYSQL_USER: noco
image: "mysql:8.0.32" image: "mysql:8.0.35"
deploy: deploy:
resources: resources:
limits: limits:

2
README.md

@ -64,7 +64,7 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart spreadshe
</a> </a>
--> -->
[![Stargazers repo roster for @nocodb/nocodb](https://reporoster.com/stars/nocodb/nocodb)](https://github.com/nocodb/nocodb/stargazers) [![Stargazers repo roster for @nocodb/nocodb](http://reporoster.com/stars/nocodb/nocodb)](https://github.com/nocodb/nocodb/stargazers)
# Quick try # Quick try

2
docker-compose/mysql/docker-compose.yml

@ -27,7 +27,7 @@ services:
- "-h" - "-h"
- localhost - localhost
timeout: 20s timeout: 20s
image: "mysql:8.0.32" image: "mysql:8.0.35"
restart: always restart: always
volumes: volumes:
- "db_data:/var/lib/mysql" - "db_data:/var/lib/mysql"

2
docker-compose/nginx-proxy-manager/docker-compose.yml

@ -46,7 +46,7 @@ services:
- "-h" - "-h"
- localhost - localhost
timeout: 20s timeout: 20s
image: "mysql:8.0.32" image: "mysql:8.0.35"
networks: networks:
- default - default
restart: always restart: always

2
package.json

@ -18,7 +18,7 @@
"devDependencies": { "devDependencies": {
"fs": "0.0.1-security", "fs": "0.0.1-security",
"lerna": "^7.0.2", "lerna": "^7.0.2",
"husky": "^8.0.0", "husky": "^8.0.3",
"xlsx": "^0.17.4" "xlsx": "^0.17.4"
}, },
"husky": { "husky": {

4
packages/nc-gui/assets/style.scss

@ -678,3 +678,7 @@ input[type='number'] {
@apply xs:(visible opacity-100 !text-gray-500) @apply xs:(visible opacity-100 !text-gray-500)
} }
} }
.ant-message-notice-content {
@apply !rounded-md;
}

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

@ -26,7 +26,6 @@ declare module '@vue/runtime-core' {
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker'] ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADivider: typeof import('ant-design-vue/es')['Divider'] ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown'] ADropdown: typeof import('ant-design-vue/es')['Dropdown']
ADropdownButton: typeof import('ant-design-vue/es')['DropdownButton'] ADropdownButton: typeof import('ant-design-vue/es')['DropdownButton']
AEmpty: typeof import('ant-design-vue/es')['Empty'] AEmpty: typeof import('ant-design-vue/es')['Empty']
@ -47,7 +46,6 @@ declare module '@vue/runtime-core' {
AMenu: typeof import('ant-design-vue/es')['Menu'] AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider'] AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup']
AModal: typeof import('ant-design-vue/es')['Modal'] AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination'] APagination: typeof import('ant-design-vue/es')['Pagination']
APopover: typeof import('ant-design-vue/es')['Popover'] APopover: typeof import('ant-design-vue/es')['Popover']
@ -64,7 +62,6 @@ declare module '@vue/runtime-core' {
ASubMenu: typeof import('ant-design-vue/es')['SubMenu'] ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch'] ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table'] ATable: typeof import('ant-design-vue/es')['Table']
ATableColumn: typeof import('ant-design-vue/es')['TableColumn']
ATabPane: typeof import('ant-design-vue/es')['TabPane'] ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs'] ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag'] ATag: typeof import('ant-design-vue/es')['Tag']

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

@ -72,13 +72,7 @@ const clear = () => {
const formatJson = (json: string) => { const formatJson = (json: string) => {
try { try {
json = json return JSON.stringify(JSON.parse(json))
.trim()
.replace(/^\{\s*|\s*\}$/g, '')
.replace(/\n\s*/g, '')
json = `{${json}}`
return json
} catch (e) { } catch (e) {
console.log(e) console.log(e)
return json return json

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

@ -16,6 +16,7 @@ import {
const props = defineProps<{ const props = defineProps<{
modelValue?: string | number modelValue?: string | number
isFocus?: boolean isFocus?: boolean
virtual?: boolean
}>() }>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
@ -65,6 +66,13 @@ onClickOutside(inputWrapperRef, (e) => {
isVisible.value = false isVisible.value = false
}) })
const onDblClick = () => {
if (!props.virtual) return
isVisible.value = true
editEnabled.value = true
}
</script> </script>
<template> <template>
@ -113,7 +121,9 @@ onClickOutside(inputWrapperRef, (e) => {
class="mr-7 nc-text-area-clamped-text" class="mr-7 nc-text-area-clamped-text"
:style="{ :style="{
'word-break': 'break-word', 'word-break': 'break-word',
'white-space': 'pre-line',
}" }"
@click="onDblClick"
/> />
<span v-else>{{ vModel }}</span> <span v-else>{{ vModel }}</span>
@ -148,7 +158,7 @@ onClickOutside(inputWrapperRef, (e) => {
<a-textarea <a-textarea
ref="inputRef" ref="inputRef"
v-model:value="vModel" v-model:value="vModel"
class="p-1 !pt-1 !pr-3 !border-0 !border-r-0 !focus:outline-transparent nc-scrollbar-md !text-black" class="p-1 !pt-1 !pr-3 !border-0 !border-r-0 !focus:outline-transparent nc-scrollbar-md !text-black !cursor-text"
:placeholder="$t('activity.enterText')" :placeholder="$t('activity.enterText')"
:bordered="false" :bordered="false"
:auto-size="{ minRows: 20, maxRows: 20 }" :auto-size="{ minRows: 20, maxRows: 20 }"

125
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -77,7 +77,7 @@ const tempTitle = ref('')
const activeBaseId = ref('') const activeBaseId = ref('')
const isErdModalOpen = ref<Boolean>(false) const isErdModalOpen = ref<boolean>(false)
const { t } = useI18n() const { t } = useI18n()
@ -116,7 +116,7 @@ const showBaseOption = computed(() => {
return ['airtableImport', 'csvImport', 'jsonImport', 'excelImport'].some((permission) => isUIAllowed(permission)) return ['airtableImport', 'csvImport', 'jsonImport', 'excelImport'].some((permission) => isUIAllowed(permission))
}) })
const enableEditMode = () => { function enableEditMode() {
editMode.value = true editMode.value = true
tempTitle.value = base.value.title! tempTitle.value = base.value.title!
nextTick(() => { nextTick(() => {
@ -126,7 +126,7 @@ const enableEditMode = () => {
}) })
} }
const updateProjectTitle = async () => { async function updateProjectTitle() {
if (!tempTitle.value) return if (!tempTitle.value) return
try { try {
@ -139,14 +139,15 @@ const updateProjectTitle = async () => {
$e('a:base:rename') $e('a:base:rename')
useTitle(`${base.value?.title}`) useTitle(`${base.value?.title}`)
} catch (e: any) { }
catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
} }
const { copy } = useCopy(true) const { copy } = useCopy(true)
const copyProjectInfo = async () => { async function copyProjectInfo() {
try { try {
if ( if (
await copy( await copy(
@ -158,7 +159,8 @@ const copyProjectInfo = async () => {
// Copied to clipboard // Copied to clipboard
message.info(t('msg.info.copiedToClipboard')) message.info(t('msg.info.copiedToClipboard'))
} }
} catch (e: any) { }
catch (e: any) {
console.error(e) console.error(e)
message.error(e.message) message.error(e.message)
} }
@ -168,7 +170,7 @@ defineExpose({
enableEditMode, enableEditMode,
}) })
const setIcon = async (icon: string, base: BaseType) => { async function setIcon(icon: string, base: BaseType) {
try { try {
const meta = { const meta = {
...((base.meta as object) || {}), ...((base.meta as object) || {}),
@ -178,7 +180,8 @@ const setIcon = async (icon: string, base: BaseType) => {
basesStore.updateProject(base.id!, { meta: JSON.stringify(meta) }) basesStore.updateProject(base.id!, { meta: JSON.stringify(meta) })
$e('a:base:icon:navdraw', { icon }) $e('a:base:icon:navdraw', { icon })
} catch (e: any) { }
catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
} }
@ -224,7 +227,7 @@ function openTableCreateDialog(sourceIndex?: number | undefined) {
} }
const isAddNewProjectChildEntityLoading = ref(false) const isAddNewProjectChildEntityLoading = ref(false)
const addNewProjectChildEntity = async () => { async function addNewProjectChildEntity() {
if (isAddNewProjectChildEntityLoading.value) return if (isAddNewProjectChildEntityLoading.value) return
isAddNewProjectChildEntityLoading.value = true isAddNewProjectChildEntityLoading.value = true
@ -243,12 +246,13 @@ const addNewProjectChildEntity = async () => {
if (!base.value.isExpanded && base.value.type !== NcProjectType.DB) { if (!base.value.isExpanded && base.value.type !== NcProjectType.DB) {
base.value.isExpanded = true base.value.isExpanded = true
} }
} finally { }
finally {
isAddNewProjectChildEntityLoading.value = false isAddNewProjectChildEntityLoading.value = false
} }
} }
const onProjectClick = async (base: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) => { async function onProjectClick(base: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) {
if (!base) { if (!base) {
return return
} }
@ -260,7 +264,8 @@ const onProjectClick = async (base: NcProject, ignoreNavigation?: boolean, toggl
if (toggleIsExpanded) { if (toggleIsExpanded) {
base.isExpanded = !base.isExpanded base.isExpanded = !base.isExpanded
} else { }
else {
base.isExpanded = true base.isExpanded = true
} }
@ -310,7 +315,8 @@ function openErdView(source: SourceType) {
const contextMenuBase = computed(() => { const contextMenuBase = computed(() => {
if (contextMenuTarget.type === 'source') { if (contextMenuTarget.type === 'source') {
return contextMenuTarget.value return contextMenuTarget.value
} else if (contextMenuTarget.type === 'table') { }
else if (contextMenuTarget.type === 'table') {
const source = base.value?.sources?.find((b) => b.id === contextMenuTarget.value.source_id) const source = base.value?.sources?.find((b) => b.id === contextMenuTarget.value.source_id)
if (source) return source if (source) return source
} }
@ -347,17 +353,17 @@ onKeyStroke('Escape', () => {
const isDuplicateDlgOpen = ref(false) const isDuplicateDlgOpen = ref(false)
const selectedProjectToDuplicate = ref() const selectedProjectToDuplicate = ref()
const duplicateProject = (base: BaseType) => { function duplicateProject(base: BaseType) {
selectedProjectToDuplicate.value = base selectedProjectToDuplicate.value = base
isDuplicateDlgOpen.value = true isDuplicateDlgOpen.value = true
} }
const tableDelete = () => { function tableDelete() {
isTableDeleteDialogVisible.value = true isTableDeleteDialogVisible.value = true
$e('c:table:delete') $e('c:table:delete')
} }
const projectDelete = () => { function projectDelete() {
isProjectDeleteDialogVisible.value = true isProjectDeleteDialogVisible.value = true
$e('c:project:delete') $e('c:project:delete')
} }
@ -449,15 +455,78 @@ const projectDelete = () => {
<GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" /> <GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" />
</NcButton> </NcButton>
<template #overlay> <NcMenuItem
<NcMenu v-if="isUIAllowed('baseDuplicate', { roles: [stringifyRolesObj(orgRoles), baseRole].join() })"
class="nc-scrollbar-md" data-testid="nc-sidebar-base-duplicate"
:style="{ @click="duplicateProject(base)"
maxHeight: '70vh', >
overflow: 'overlay', <div v-e="['c:base:duplicate']" class="flex gap-2 items-center">
}" <GeneralIcon icon="duplicate" class="text-gray-700" />
:data-testid="`nc-sidebar-base-${base.title}-options`" {{ $t('general.duplicate') }}
@click="isOptionsOpen = false" </div>
</NcMenuItem>
<NcDivider v-if="['baseDuplicate', 'baseRename'].some((permission) => isUIAllowed(permission))" />
<!-- Copy Project Info -->
<NcMenuItem
v-if="!isEeUI"
key="copy"
data-testid="nc-sidebar-base-copy-base-info"
@click.stop="copyProjectInfo"
>
<div v-e="['c:base:copy-proj-info']" class="flex gap-2 items-center">
<GeneralIcon icon="copy" class="group-hover:text-black" />
{{ $t('activity.account.projInfo') }}
</div>
</NcMenuItem>
<!-- ERD View -->
<NcMenuItem
key="erd"
data-testid="nc-sidebar-base-relations"
@click="openErdView(base?.sources?.[0]!)"
>
<div v-e="['c:base:erd']" class="flex gap-2 items-center">
<GeneralIcon icon="erd" />
{{ $t('title.relations') }}
</div>
</NcMenuItem>
<!-- Swagger: Rest APIs -->
<NcMenuItem
v-if="isUIAllowed('apiDocs')"
key="api"
v-e="['c:base:api-docs']"
data-testid="nc-sidebar-base-rest-apis"
@click.stop="
() => {
$e('c:base:api-docs')
openLink(`/api/v2/meta/bases/${base.id}/swagger`, appInfo.ncSiteUrl)
}
"
>
<div v-e="['c:base:api-docs']" class="flex gap-2 items-center">
<GeneralIcon icon="snippet" class="group-hover:text-black !max-w-3.9" />
{{ $t('activity.account.swagger') }}
</div>
</NcMenuItem>
</template>
<template v-if="base.sources && base.sources[0] && showBaseOption">
<NcDivider />
<DashboardTreeViewBaseOptions v-model:base="base" :source="base.sources[0]" />
</template>
<NcDivider v-if="['baseMiscSettings', 'baseDelete'].some((permission) => isUIAllowed(permission))" />
<NcMenuItem
v-if="isUIAllowed('baseMiscSettings')"
key="teamAndSettings"
v-e="['c:base:settings']"
data-testid="nc-sidebar-base-settings"
class="nc-sidebar-base-base-settings"
@click="toggleDialog(true, 'teamAndAuth', undefined, base.id)"
> >
<template v-if="!isSharedBase"> <template v-if="!isSharedBase">
<NcMenuItem v-if="isUIAllowed('baseRename')" data-testid="nc-sidebar-project-rename" @click="enableEditMode"> <NcMenuItem v-if="isUIAllowed('baseRename')" data-testid="nc-sidebar-project-rename" @click="enableEditMode">
@ -610,6 +679,7 @@ const projectDelete = () => {
:class="{ '!rotate-180': isActive }" :class="{ '!rotate-180': isActive }"
/> />
</div> </div>
</template> </template>
<a-collapse-panel :key="`collapse-${source.id}`"> <a-collapse-panel :key="`collapse-${source.id}`">
<template #header> <template #header>
@ -703,6 +773,7 @@ const projectDelete = () => {
<DashboardTreeViewTableList :base="base" :source-index="sourceIndex" /> <DashboardTreeViewTableList :base="base" :source-index="sourceIndex" />
</div> </div>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</div> </div>
</div> </div>
@ -743,6 +814,7 @@ const projectDelete = () => {
</template> </template>
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>
<DlgTableDelete <DlgTableDelete
v-if="contextMenuTarget.value?.id && base?.id" v-if="contextMenuTarget.value?.id && base?.id"
@ -750,8 +822,11 @@ const projectDelete = () => {
:table-id="contextMenuTarget.value?.id" :table-id="contextMenuTarget.value?.id"
:base-id="base?.id" :base-id="base?.id"
/> />
<DlgProjectDelete v-model:visible="isProjectDeleteDialogVisible" :base-id="base?.id" /> <DlgProjectDelete v-model:visible="isProjectDeleteDialogVisible" :base-id="base?.id" />
<DlgProjectDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" /> <DlgProjectDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
<GeneralModal v-model:visible="isErdModalOpen" size="large"> <GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]"> <div class="h-[80vh]">
<LazyDashboardSettingsErd :source-id="activeBaseId" /> <LazyDashboardSettingsErd :source-id="activeBaseId" />

7
packages/nc-gui/components/dlg/AirtableImport.vue

@ -90,8 +90,10 @@ const onStatus = async (status: JobStatus, data?: any) => {
refreshCommandPalette() refreshCommandPalette()
// TODO: add tab of the first table // TODO: add tab of the first table
} else if (status === JobStatus.FAILED) { } else if (status === JobStatus.FAILED) {
await loadTables()
goBack.value = true goBack.value = true
pushProgress(data.error.message, status) pushProgress(data.error.message, status)
refreshCommandPalette()
} }
} }
@ -115,7 +117,10 @@ const { validateInfos } = useForm(syncSource, validators)
const disableImportButton = computed(() => !syncSource.value.details.apiKey || !syncSource.value.details.syncSourceUrlOrId) const disableImportButton = computed(() => !syncSource.value.details.apiKey || !syncSource.value.details.syncSourceUrlOrId)
const isLoading = ref(false)
async function saveAndSync() { async function saveAndSync() {
isLoading.value = true
await createOrUpdate() await createOrUpdate()
await sync() await sync()
} }
@ -178,6 +183,7 @@ async function listenForUpdates(id?: string) {
} }
} else { } else {
listeningForUpdates.value = false listeningForUpdates.value = false
isLoading.value = false
} }
}, },
) )
@ -494,6 +500,7 @@ onMounted(async () => {
v-e="['c:sync-airtable:save-and-sync']" v-e="['c:sync-airtable:save-and-sync']"
type="primary" type="primary"
class="nc-btn-airtable-import" class="nc-btn-airtable-import"
:loading="isLoading"
:disabled="disableImportButton" :disabled="disableImportButton"
@click="saveAndSync" @click="saveAndSync"
> >

14
packages/nc-gui/components/dlg/ColumnDuplicate.vue

@ -17,10 +17,6 @@ const dialogShow = useVModel(props, 'modelValue', emit)
const { $e, $poller } = useNuxtApp() const { $e, $poller } = useNuxtApp()
const basesStore = useBases()
const { createProject: _createProject } = basesStore
const { activeTable: _activeTable } = storeToRefs(useTablesStore()) const { activeTable: _activeTable } = storeToRefs(useTablesStore())
const reloadDataHook = inject(ReloadViewDataHookInj) const reloadDataHook = inject(ReloadViewDataHookInj)
@ -101,7 +97,9 @@ onKeyStroke('Enter', () => {
} }
}) })
const isEaster = ref(false) defineExpose({
duplicate: _duplicate,
})
</script> </script>
<template> <template>
@ -118,11 +116,9 @@ const isEaster = ref(false)
@keydown.esc="dialogShow = false" @keydown.esc="dialogShow = false"
> >
<div> <div>
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster"> <div class="prose-xl font-bold self-center">{{ $t('general.duplicate') }} {{ $t('objects.column') }}</div>
{{ $t('general.duplicate') }} {{ $t('objects.column') }}
</div>
<div class="mt-4">{{ $t('msg.warning.duplicateProject') }}</div> <div class="mt-4">Are you sure you want to duplicate the field?</div>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div> <div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>

14
packages/nc-gui/components/dlg/QuickImport.vue

@ -172,7 +172,9 @@ const disablePreImportButton = computed(() => {
} }
}) })
const disableImportButton = computed(() => !templateEditorRef.value?.isValid) const isError = ref(false)
const disableImportButton = computed(() => !templateEditorRef.value?.isValid || isError.value)
const disableFormatJsonButton = computed(() => !jsonEditorRef.value?.isValid) const disableFormatJsonButton = computed(() => !jsonEditorRef.value?.isValid)
@ -530,6 +532,14 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
preImportLoading.value = false preImportLoading.value = false
} }
} }
const onError = () => {
isError.value = true
}
const onChange = () => {
isError.value = false
}
</script> </script>
<template> <template>
@ -558,6 +568,8 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
:import-worker="importWorker" :import-worker="importWorker"
class="nc-quick-import-template-editor" class="nc-quick-import-template-editor"
@import="handleImport" @import="handleImport"
@error="onError"
@change="onChange"
/> />
<a-tabs v-else v-model:activeKey="activeKey" hide-add type="editable-card" tab-position="top"> <a-tabs v-else v-model:activeKey="activeKey" hide-add type="editable-card" tab-position="top">

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

@ -216,7 +216,7 @@ onUnmounted(() => {
> >
<template v-if="column"> <template v-if="column">
<template v-if="intersected"> <template v-if="intersected">
<LazyCellTextArea v-if="isTextArea(column)" v-model="vModel" /> <LazyCellTextArea v-if="isTextArea(column)" v-model="vModel" :virtual="props.virtual" />
<LazyCellGeoData v-else-if="isGeoData(column)" v-model="vModel" /> <LazyCellGeoData v-else-if="isGeoData(column)" v-model="vModel" />
<LazyCellCheckbox v-else-if="isBoolean(column, abstractType)" v-model="vModel" /> <LazyCellCheckbox v-else-if="isBoolean(column, abstractType)" v-model="vModel" />
<LazyCellAttachment v-else-if="isAttachment(column)" v-model="vModel" :row-index="props.rowIndex" /> <LazyCellAttachment v-else-if="isAttachment(column)" v-model="vModel" :row-index="props.rowIndex" />

44
packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -29,7 +29,7 @@ const { optionsMagic: _optionsMagic } = useNocoEe()
const optionsWrapperDomRef = ref<HTMLElement>() const optionsWrapperDomRef = ref<HTMLElement>()
const options = ref<(Option & { status?: 'remove' })[]>([]) const options = ref<(Option & { status?: 'remove'; index?: number })[]>([])
const isAddingOption = ref(false) const isAddingOption = ref(false)
@ -38,7 +38,7 @@ const OPTIONS_PAGE_COUNT = 20
const loadedOptionAnchor = ref(OPTIONS_PAGE_COUNT) const loadedOptionAnchor = ref(OPTIONS_PAGE_COUNT)
const isReverseLazyLoad = ref(false) const isReverseLazyLoad = ref(false)
const renderedOptions = ref<(Option & { status?: 'remove' })[]>([]) const renderedOptions = ref<(Option & { status?: 'remove'; index?: number })[]>([])
const savedDefaultOption = ref<Option | null>(null) const savedDefaultOption = ref<Option | null>(null)
const savedCdf = ref<string | null>(null) const savedCdf = ref<string | null>(null)
@ -98,6 +98,12 @@ onMounted(() => {
options.value = vModel.value.colOptions.options options.value = vModel.value.colOptions.options
let indexCounter = 0
options.value.map((el) => {
el.index = indexCounter++
return el
})
loadedOptionAnchor.value = Math.min(loadedOptionAnchor.value, options.value.length) loadedOptionAnchor.value = Math.min(loadedOptionAnchor.value, options.value.length)
renderedOptions.value = [...options.value].slice(0, loadedOptionAnchor.value) renderedOptions.value = [...options.value].slice(0, loadedOptionAnchor.value)
@ -135,6 +141,7 @@ const addNewOption = () => {
const tempOption = { const tempOption = {
title: '', title: '',
color: getNextColor(), color: getNextColor(),
index: options.value.length,
} }
options.value.push(tempOption) options.value.push(tempOption)
@ -168,11 +175,30 @@ const addNewOption = () => {
// } // }
const syncOptions = () => { const syncOptions = () => {
vModel.value.colOptions.options = renderedOptions.value.filter((op) => op.status !== 'remove') vModel.value.colOptions.options = options.value
.filter((op) => op.status !== 'remove')
.sort((a, b) => {
const renderA = renderedOptions.value.findIndex((el) => a.index !== undefined && el.index === a.index)
const renderB = renderedOptions.value.findIndex((el) => a.index !== undefined && el.index === b.index)
if (renderA === -1 || renderB === -1) return 0
return renderA - renderB
})
.map((op) => {
const { index: _i, status: _s, ...rest } = op
return rest
})
} }
const removeRenderedOption = (index: number) => { const removeRenderedOption = (index: number) => {
renderedOptions.value[index].status = 'remove' const renderedOption = renderedOptions.value[index]
if (renderedOption.index === undefined || isNaN(renderedOption.index)) return
const option = options.value[renderedOption.index]
renderedOption.status = 'remove'
option.status = 'remove'
syncOptions() syncOptions()
const optionId = renderedOptions.value[index]?.id const optionId = renderedOptions.value[index]?.id
@ -193,7 +219,15 @@ const optionChanged = (changedId: string) => {
} }
const undoRemoveRenderedOption = (index: number) => { const undoRemoveRenderedOption = (index: number) => {
renderedOptions.value[index].status = undefined const renderedOption = renderedOptions.value[index]
if (renderedOption.index === undefined || isNaN(renderedOption.index)) return
const option = options.value[renderedOption.index]
renderedOption.status = undefined
option.status = undefined
syncOptions() syncOptions()
const optionId = renderedOptions.value[index]?.id const optionId = renderedOptions.value[index]?.id

11
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { diff } from 'deep-object-diff' import { diff } from 'deep-object-diff'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { UITypes, isSystemColumn } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { onKeyDown, useMagicKeys } from '@vueuse/core' import { onKeyDown, useMagicKeys } from '@vueuse/core'
import type { ColumnType, SelectOptionsType } from 'nocodb-sdk' import type { ColumnType, SelectOptionsType } from 'nocodb-sdk'
@ -774,8 +774,15 @@ const onFieldOptionUpdate = () => {
" "
/> />
<NcCheckbox v-else :disabled="true" class="opacity-0" :checked="true" /> <NcCheckbox v-else :disabled="true" class="opacity-0" :checked="true" />
<SmartsheetHeaderVirtualCellIcon
v-if="field && isVirtualCol(fieldState(field) || field)"
:column-meta="fieldState(field) || field"
:class="{
'text-brand-500': compareCols(field, activeField),
}"
/>
<SmartsheetHeaderCellIcon <SmartsheetHeaderCellIcon
v-if="field" v-else
:column-meta="fieldState(field) || field" :column-meta="fieldState(field) || field"
:class="{ :class="{
'text-brand-500': compareCols(field, activeField), 'text-brand-500': compareCols(field, activeField),

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

@ -41,11 +41,13 @@ interface Props {
showNextPrevIcons?: boolean showNextPrevIcons?: boolean
firstRow?: boolean firstRow?: boolean
lastRow?: boolean lastRow?: boolean
closeAfterSave?: boolean
newRecordHeader?: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev']) const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev', 'createdRecord'])
const { activeView } = storeToRefs(useViewsStore()) const { activeView } = storeToRefs(useViewsStore())
@ -201,6 +203,12 @@ const save = async () => {
reloadTrigger?.trigger() reloadTrigger?.trigger()
} }
isUnsavedFormExist.value = false isUnsavedFormExist.value = false
if (props.closeAfterSave) {
isExpanded.value = false
}
emits('createdRecord', _row.value.row)
} }
const isPreventChangeModalOpen = ref(false) const isPreventChangeModalOpen = ref(false)
@ -486,12 +494,17 @@ export default {
<div v-if="isLoading"> <div v-if="isLoading">
<a-skeleton-input class="!h-8 !sm:mr-14 !w-52 mt-1 !rounded-md !overflow-hidden" active size="small" /> <a-skeleton-input class="!h-8 !sm:mr-14 !w-52 mt-1 !rounded-md !overflow-hidden" active size="small" />
</div> </div>
<div
v-if="row.rowMeta?.new || props.newRecordHeader"
class="flex items-center truncate font-bold text-gray-800 text-xl"
>
{{ props.newRecordHeader ?? $t('activity.newRecord') }}
</div>
<div v-else-if="displayValue && !row.rowMeta?.new" class="flex items-center font-bold text-gray-800 text-xl w-64"> <div v-else-if="displayValue && !row.rowMeta?.new" class="flex items-center font-bold text-gray-800 text-xl w-64">
<span class="truncate"> <span class="truncate">
{{ displayValue }} {{ displayValue }}
</span> </span>
</div> </div>
<div v-if="row.rowMeta?.new" class="flex items-center truncate font-bold text-gray-800 text-xl">New Record</div>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<NcButton <NcButton

39
packages/nc-gui/components/smartsheet/grid/GroupBy.vue

@ -1,8 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import tinycolor from 'tinycolor2' import tinycolor from 'tinycolor2'
import { UITypes } from 'nocodb-sdk'
import Table from './Table.vue' import Table from './Table.vue'
import GroupBy from './GroupBy.vue' import GroupBy from './GroupBy.vue'
import GroupByTable from './GroupByTable.vue' import GroupByTable from './GroupByTable.vue'
import GroupByLabel from './GroupByLabel.vue'
import { GROUP_BY_VARS, computed, ref } from '#imports' import { GROUP_BY_VARS, computed, ref } from '#imports'
import type { Group, Row } from '#imports' import type { Group, Row } from '#imports'
@ -134,6 +136,27 @@ const onScroll = (e: Event) => {
if (!vGroup.value.root) return if (!vGroup.value.root) return
_scrollLeft.value = (e.target as HTMLElement).scrollLeft _scrollLeft.value = (e.target as HTMLElement).scrollLeft
} }
// a method to parse group key if grouped column type is LTAR or Lookup
// in these 2 scenario it will return json array or `___` separated value
const parseKey = (group) => {
const key = group.key.toString()
// parse json array key if it's a lookup or link to another record
if ((key && group.column?.uidt === UITypes.Lookup) || group.column?.uidt === UITypes.LinkToAnotherRecord) {
try {
const parsedKey = JSON.parse(key)
return parsedKey
} catch {
// if parsing try to split it by `___` (for sqlite)
return key.split('___')
}
}
return [key]
}
const shouldRenderCell = (column) =>
[UITypes.Lookup, UITypes.Attachment, UITypes.Barcode, UITypes.QrCode, UITypes.Links].includes(column?.uidt)
</script> </script>
<template> <template>
@ -227,6 +250,15 @@ const onScroll = (e: Event) => {
</span> </span>
</a-tag> </a-tag>
</template> </template>
<div
v-else-if="!(grp.key in GROUP_BY_VARS.VAR_TITLES) && shouldRenderCell(grp.column)"
class="flex min-w-[100px] flex-wrap"
>
<template v-for="(val, ind) of parseKey(grp)" :key="ind">
<GroupByLabel v-if="val" :column="grp.column" :model-value="val" />
<span v-else class="text-gray-400">No mapped value</span>
</template>
</div>
<a-tag <a-tag
v-else v-else
:key="`panel-tag-${grp.column.id}-${grp.key}`" :key="`panel-tag-${grp.column.id}-${grp.key}`"
@ -247,7 +279,12 @@ const onScroll = (e: Event) => {
'font-weight': 500, 'font-weight': 500,
}" }"
> >
{{ grp.key in GROUP_BY_VARS.VAR_TITLES ? GROUP_BY_VARS.VAR_TITLES[grp.key] : grp.key }} <template v-if="grp.key in GROUP_BY_VARS.VAR_TITLES">{{
GROUP_BY_VARS.VAR_TITLES[grp.key]
}}</template>
<template v-else>
{{ parseKey(grp)?.join(', ') }}
</template>
</span> </span>
</a-tag> </a-tag>
</div> </div>

28
packages/nc-gui/components/smartsheet/grid/GroupByLabel.vue

@ -0,0 +1,28 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { isVirtualCol } from 'nocodb-sdk'
defineProps<{
column: ColumnType
modelValue: any
}>()
provide(ReadonlyInj, true)
</script>
<template>
<div class="pointer-events-none">
<LazySmartsheetRow :row="{ row: { [column.title]: modelValue }, rowMeta: {} }">
<LazySmartsheetVirtualCell v-if="isVirtualCol(column)" :model-value="modelValue" class="!text-gray-600" :column="column" />
<LazySmartsheetCell
v-else
:model-value="modelValue"
class="!text-gray-600"
:column="column"
:edit-enabled="false"
:read-only="true"
/>
</LazySmartsheetRow>
</div>
</template>

20
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnReqType, ColumnType } from 'nocodb-sdk' import type { ColumnReqType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk' import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
@ -111,8 +111,8 @@ const sortByColumn = async (direction: 'asc' | 'desc') => {
} }
const isDuplicateDlgOpen = ref(false) const isDuplicateDlgOpen = ref(false)
const selectedColumnToDuplicate = ref<ColumnType>()
const selectedColumnExtra = ref<any>() const selectedColumnExtra = ref<any>()
const duplicateDialogRef = ref<any>()
const duplicateVirtualColumn = async () => { const duplicateVirtualColumn = async () => {
let columnCreatePayload = {} let columnCreatePayload = {}
@ -165,7 +165,7 @@ const duplicateVirtualColumn = async () => {
const openDuplicateDlg = async () => { const openDuplicateDlg = async () => {
if (!column?.value) return if (!column?.value) return
if (column.value.uidt && [UITypes.Formula, UITypes.Lookup, UITypes.Rollup].includes(column.value.uidt as UITypes)) { if (column.value.uidt && [UITypes.Lookup, UITypes.Rollup].includes(column.value.uidt as UITypes)) {
duplicateVirtualColumn() duplicateVirtualColumn()
} else { } else {
const gridViewColumnList = (await $api.dbViewColumn.list(view.value?.id as string)).list const gridViewColumnList = (await $api.dbViewColumn.list(view.value?.id as string)).list
@ -186,8 +186,15 @@ const openDuplicateDlg = async () => {
view_id: view.value!.id as string, view_id: view.value!.id as string,
}, },
} }
selectedColumnToDuplicate.value = column.value
if (column.value.uidt === UITypes.Formula) {
nextTick(() => {
duplicateDialogRef?.value?.duplicate()
})
} else {
isDuplicateDlgOpen.value = true isDuplicateDlgOpen.value = true
}
isOpen.value = false isOpen.value = false
} }
} }
@ -373,9 +380,10 @@ const onInsertAfter = () => {
</a-dropdown> </a-dropdown>
<SmartsheetHeaderDeleteColumnModal v-model:visible="showDeleteColumnModal" /> <SmartsheetHeaderDeleteColumnModal v-model:visible="showDeleteColumnModal" />
<DlgColumnDuplicate <DlgColumnDuplicate
v-if="selectedColumnToDuplicate" v-if="column"
ref="duplicateDialogRef"
v-model="isDuplicateDlgOpen" v-model="isDuplicateDlgOpen"
:column="selectedColumnToDuplicate" :column="column"
:extra="selectedColumnExtra" :extra="selectedColumnExtra"
/> />
</template> </template>

50
packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
IsLockedInj, IsLockedInj,
@ -15,20 +15,10 @@ import {
useNuxtApp, useNuxtApp,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useViewColumnsOrThrow, useViewColumnsOrThrow,
watch,
} from '#imports' } from '#imports'
const groupingUidt = [ const excludedGroupingUidt = [UITypes.Attachment]
UITypes.SingleSelect,
UITypes.MultiSelect,
UITypes.Checkbox,
UITypes.Date,
UITypes.SingleLineText,
UITypes.Number,
UITypes.Rollup,
UITypes.Lookup,
UITypes.Links,
UITypes.Formula,
]
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref()) const view = inject(ActiveViewInj, ref())
@ -62,16 +52,16 @@ const groupedByColumnIds = computed(() => groupBy.value.map((g) => g.fk_column_i
const { eventBus } = useSmartsheetStoreOrThrow() const { eventBus } = useSmartsheetStoreOrThrow()
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const btLookups = ref([]) const supportedLookups = ref([])
const fieldsToGroupBy = computed(() => { const fieldsToGroupBy = computed(() => {
const fields = meta.value?.columns || [] const fields = meta.value?.columns || []
return fields.filter((field) => { return fields.filter((field) => {
if (!groupingUidt.includes(field.uidt as UITypes)) return false if (excludedGroupingUidt.includes(field.uidt as UITypes)) return false
if (field.uidt === UITypes.Lookup) { if (field.uidt === UITypes.Lookup) {
return btLookups.value.includes(field.id) return supportedLookups.value.includes(field.id)
} }
return true return true
@ -161,25 +151,18 @@ watch(open, () => {
} }
}) })
const loadBtLookups = async () => { const loadAllowedLookups = async () => {
const filteredLookupCols = [] const filteredLookupCols = []
try { try {
for (const col of meta.value?.columns || []) { for (const col of meta.value?.columns || []) {
if (col.uidt !== UITypes.Lookup) continue if (col.uidt !== UITypes.Lookup) continue
let nextCol = col let nextCol = col
let btLookup = true // check the lookup column is supported type or not
while (nextCol && nextCol.uidt === UITypes.Lookup) {
// check all the relation of nested lookup columns is bt or not
// include the column only if all only if all relations are bt
while (btLookup && nextCol && nextCol.uidt === UITypes.Lookup) {
const lookupRelation = (await getMeta(nextCol.fk_model_id))?.columns?.find( const lookupRelation = (await getMeta(nextCol.fk_model_id))?.columns?.find(
(c) => c.id === (nextCol.colOptions as LookupType).fk_relation_column_id, (c) => c.id === (nextCol.colOptions as LookupType).fk_relation_column_id,
) )
if ((lookupRelation.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO) {
btLookup = false
continue
}
const relatedTableMeta = await getMeta((lookupRelation.colOptions as LinkToAnotherRecordType).fk_related_model_id) const relatedTableMeta = await getMeta((lookupRelation.colOptions as LinkToAnotherRecordType).fk_related_model_id)
@ -190,22 +173,25 @@ const loadBtLookups = async () => {
// if next column is same as root lookup column then break the loop // if next column is same as root lookup column then break the loop
// since it's going to be a circular loop, and ignore the column // since it's going to be a circular loop, and ignore the column
if (nextCol.id === col.id) { if (nextCol.id === col.id) {
btLookup = false
break break
} }
} }
if (btLookup) filteredLookupCols.push(col.id) if (nextCol.uidt !== UITypes.Attachment) filteredLookupCols.push(col.id)
} }
btLookups.value = filteredLookupCols supportedLookups.value = filteredLookupCols
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
} }
onMounted(async () => { onMounted(async () => {
await loadBtLookups() await loadAllowedLookups()
})
watch(meta, async () => {
await loadAllowedLookups()
}) })
</script> </script>
@ -242,9 +228,7 @@ onMounted(async () => {
<LazySmartsheetToolbarFieldListAutoCompleteDropdown <LazySmartsheetToolbarFieldListAutoCompleteDropdown
v-model="group.fk_column_id" v-model="group.fk_column_id"
class="caption nc-sort-field-select" class="caption nc-sort-field-select"
:columns=" :columns="fieldsToGroupBy"
fieldsToGroupBy.filter((f) => (f.id && !groupedByColumnIds.includes(f.id)) || f.id === group.fk_column_id)
"
:allow-empty="true" :allow-empty="true"
@change="saveGroupBy" @change="saveGroupBy"
@click.stop @click.stop

69
packages/nc-gui/components/template/Editor.vue

@ -34,12 +34,13 @@ import {
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useTabs, useTabs,
validateTableName,
} from '#imports' } from '#imports'
const { quickImportType, baseTemplate, importData, importColumns, importDataOnly, maxRowsToParse, sourceId, importWorker } = const { quickImportType, baseTemplate, importData, importColumns, importDataOnly, maxRowsToParse, sourceId, importWorker } =
defineProps<Props>() defineProps<Props>()
const emit = defineEmits(['import']) const emit = defineEmits(['import', 'error', 'change'])
dayjs.extend(utc) dayjs.extend(utc)
@ -95,6 +96,8 @@ const importingTips = ref<Record<string, string>>({})
const checkAllRecord = ref<boolean[]>([]) const checkAllRecord = ref<boolean[]>([])
const formError = ref()
const uiTypeOptions = ref<Option[]>( const uiTypeOptions = ref<Option[]>(
(Object.keys(UITypes) as (keyof typeof UITypes)[]) (Object.keys(UITypes) as (keyof typeof UITypes)[])
.filter( .filter(
@ -124,11 +127,14 @@ const data = reactive<{
const validators = computed(() => const validators = computed(() =>
data.tables.reduce<Record<string, [ReturnType<typeof fieldRequiredValidator>]>>((acc: Record<string, any>, table, tableIdx) => { data.tables.reduce<Record<string, [ReturnType<typeof fieldRequiredValidator>]>>((acc: Record<string, any>, table, tableIdx) => {
acc[`tables.${tableIdx}.table_name`] = [fieldRequiredValidator()] acc[`tables.${tableIdx}.table_name`] = [validateTableName]
hasSelectColumn.value[tableIdx] = false hasSelectColumn.value[tableIdx] = false
table.columns?.forEach((column, columnIdx) => { table.columns?.forEach((column, columnIdx) => {
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [fieldRequiredValidator(), fieldLengthValidator()] acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [
fieldRequiredValidator(),
fieldLengthValidator(base.value?.sources?.[0].type || ClientType.MYSQL),
]
acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()] acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()]
if (isSelect(column)) { if (isSelect(column)) {
hasSelectColumn.value[tableIdx] = true hasSelectColumn.value[tableIdx] = true
@ -139,10 +145,12 @@ const validators = computed(() =>
}, {}), }, {}),
) )
const { validate, validateInfos } = useForm(data, validators) const { validate, validateInfos, modelRef } = useForm(data, validators)
const isValid = ref(!importDataOnly) const isValid = ref(!importDataOnly)
const formRef = ref()
watch( watch(
() => srcDestMapping.value, () => srcDestMapping.value,
() => { () => {
@ -674,6 +682,39 @@ function handleUIDTChange(column, table) {
]) ])
} }
} }
const setErrorState = (errorsFields: any[]) => {
const errorMap: any = {}
for (const error of errorsFields) {
errorMap[error.name] = error.errors
}
formError.value = errorMap
}
watch(formRef, () => {
setTimeout(async () => {
try {
await validate()
emit('change')
formError.value = null
} catch (e: any) {
emit('error', e)
setErrorState(e.errorFields)
}
}, 500)
})
watch(modelRef, async () => {
try {
await validate()
emit('change')
formError.value = null
} catch (e: any) {
emit('error', e)
setErrorState(e.errorFields)
}
})
</script> </script>
<template> <template>
@ -694,7 +735,7 @@ function handleUIDTChange(column, table) {
<a-collapse v-if="data.tables && data.tables.length" v-model:activeKey="expansionPanel" class="template-collapse" accordion> <a-collapse v-if="data.tables && data.tables.length" v-model:activeKey="expansionPanel" class="template-collapse" accordion>
<a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx"> <a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx">
<template #header> <template #header>
<span class="font-weight-bold text-lg flex items-center gap-2"> <span class="font-weight-bold text-lg flex items-center gap-2 truncate">
<component :is="iconMap.table" class="text-primary" /> <component :is="iconMap.table" class="text-primary" />
{{ table.table_name }} {{ table.table_name }}
</span> </span>
@ -769,7 +810,7 @@ function handleUIDTChange(column, table) {
</a-card> </a-card>
<a-card v-else> <a-card v-else>
<a-form :model="data" name="template-editor-form" @keydown.enter="emit('import')"> <a-form ref="formRef" :model="data" name="template-editor-form" @keydown.enter="emit('import')">
<p v-if="data.tables && quickImportType === 'excel'" class="text-center"> <p v-if="data.tables && quickImportType === 'excel'" class="text-center">
{{ data.tables.length }} sheet{{ data.tables.length > 1 ? 's' : '' }} {{ data.tables.length }} sheet{{ data.tables.length > 1 ? 's' : '' }}
available for import available for import
@ -783,22 +824,24 @@ function handleUIDTChange(column, table) {
> >
<a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx"> <a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx">
<template #header> <template #header>
<a-form-item v-if="editableTn[tableIdx]" v-bind="validateInfos[`tables.${tableIdx}.table_name`]" no-style> <a-form-item v-bind="validateInfos[`tables.${tableIdx}.table_name`]" no-style>
<div class="flex flex-col w-full">
<a-input <a-input
v-model:value.lazy="table.table_name" v-model:value="table.table_name"
class="max-w-xs font-weight-bold text-lg" class="font-weight-bold text-lg"
size="large" size="large"
hide-details hide-details
:bordered="false" :bordered="false"
@click.stop @click.stop
@blur="handleEditableTnChange(tableIdx)" @blur="handleEditableTnChange(tableIdx)"
@keydown.enter="handleEditableTnChange(tableIdx)" @keydown.enter="handleEditableTnChange(tableIdx)"
@dblclick="setEditableTn(tableIdx, true)"
/> />
<div v-if="formError?.[`tables.${tableIdx}.table_name`]" class="text-red-500 ml-3">
{{ formError?.[`tables.${tableIdx}.table_name`].join('\n') }}
</div>
</div>
</a-form-item> </a-form-item>
<span v-else class="font-weight-bold text-lg flex items-center gap-2" @click="setEditableTn(tableIdx, true)">
<component :is="iconMap.table" class="text-primary" />
{{ table.table_name }}
</span>
</template> </template>
<template #extra> <template #extra>

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

@ -98,7 +98,7 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
<template> <template>
<div <div
class="h-full w-full" class="h-full w-full nc-lookup-cell"
:style="{ height: rowHeight && rowHeight !== 1 ? `${rowHeight * 2}rem` : `2.85rem` }" :style="{ height: rowHeight && rowHeight !== 1 ? `${rowHeight * 2}rem` : `2.85rem` }"
@dblclick="activateShowEditNonEditableFieldWarning" @dblclick="activateShowEditNonEditableFieldWarning"
> >
@ -206,4 +206,8 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
@apply bg-gray-200; @apply bg-gray-200;
} }
} }
.nc-lookup-cell .nc-text-area-clamped-text {
@apply !mr-1;
}
</style> </style>

8
packages/nc-gui/components/virtual-cell/components/Header.vue

@ -5,9 +5,9 @@ import FileIcon from '~icons/nc-icons/file'
import { iconMap } from '#imports' import { iconMap } from '#imports'
const { relation, relatedTableTitle, displayValue, showHeader, tableTitle } = defineProps<{ const { relation, relatedTableTitle, displayValue, header, tableTitle } = defineProps<{
relation: string relation: string
showHeader?: boolean header?: string | null
tableTitle: string tableTitle: string
relatedTableTitle: string relatedTableTitle: string
displayValue?: string displayValue?: string
@ -54,12 +54,12 @@ const relationMeta = computed(() => {
<template> <template>
<div class="flex sm:justify-between relative pb-2 items-center"> <div class="flex sm:justify-between relative pb-2 items-center">
<div v-if="!isMobileMode" class="flex text-base font-bold justify-start items-center min-w-36"> <div v-if="!isMobileMode" class="flex text-base font-bold justify-start items-center min-w-36">
{{ showHeader ? 'Linked Records' : '' }} {{ header ?? '' }}
</div> </div>
<div class="flex flex-row sm:w-[calc(100%-16rem)] xs:w-full items-center justify-center gap-2 xs:(h-full)"> <div class="flex flex-row sm:w-[calc(100%-16rem)] xs:w-full items-center justify-center gap-2 xs:(h-full)">
<div class="flex sm:justify-end w-[calc(50%-1.5rem)] xs:(w-[calc(50%-1.5rem)] h-full)"> <div class="flex sm:justify-end w-[calc(50%-1.5rem)] xs:(w-[calc(50%-1.5rem)] h-full)">
<div <div
class="flex max-w-full xs:w-full flex-shrink-0 xs:(h-full) rounded-md gap-1 text-brand-500 items-center bg-gray-100 px-2 py-1" class="flex max-w-full xs:w-full flex-shrink-0 xs:(h-full) rounded-md gap-1 text-gray-700 items-center bg-gray-100 px-2 py-1"
> >
<FileIcon class="w-4 h-4 min-w-4" /> <FileIcon class="w-4 h-4 min-w-4" />
<span class="truncate"> <span class="truncate">

2
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -192,7 +192,7 @@ const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
:relation="relation" :relation="relation"
:linked-records="childrenListCount" :linked-records="childrenListCount"
:table-title="meta?.title" :table-title="meta?.title"
:show-header="true" :header="$t('activity.linkedRecords')"
:related-table-title="relatedTableMeta?.title" :related-table-title="relatedTableMeta?.title"
:display-value="row.row[displayValueProp]" :display-value="row.row[displayValueProp]"
/> />

69
packages/nc-gui/components/virtual-cell/components/ListItems.vue

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk' import { RelationTypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import InboxIcon from '~icons/nc-icons/inbox' import InboxIcon from '~icons/nc-icons/inbox'
import { import {
@ -29,6 +29,8 @@ const { isSharedBase } = storeToRefs(useBase())
const filterQueryRef = ref() const filterQueryRef = ref()
const { t } = useI18n()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { const {
@ -53,6 +55,8 @@ const { addLTARRef, isNew, removeLTARRef, state: rowState } = useSmartsheetRowSt
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const isExpandedFormCloseAfterSave = ref(false)
isChildrenExcludedLoading.value = true isChildrenExcludedLoading.value = true
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
@ -112,7 +116,8 @@ const newRowState = computed(() => {
if (isNew.value) return {} if (isNew.value) return {}
const colOpt = (injectedColumn?.value as ColumnType)?.colOptions as LinkToAnotherRecordType const colOpt = (injectedColumn?.value as ColumnType)?.colOptions as LinkToAnotherRecordType
const colInRelatedTable: ColumnType | undefined = relatedTableMeta?.value?.columns?.find((col) => { const colInRelatedTable: ColumnType | undefined = relatedTableMeta?.value?.columns?.find((col) => {
if (col.uidt !== UITypes.LinkToAnotherRecord) return false // Links as for the case of 'mm' we need the 'Links' column
if (!isLinksOrLTAR(col)) return false
const colOpt1 = col?.colOptions as LinkToAnotherRecordType const colOpt1 = col?.colOptions as LinkToAnotherRecordType
if (colOpt1?.fk_related_model_id !== meta.value.id) return false if (colOpt1?.fk_related_model_id !== meta.value.id) return false
@ -157,6 +162,10 @@ const relation = computed(() => {
watch(expandedFormDlg, () => { watch(expandedFormDlg, () => {
if (!expandedFormDlg.value) { if (!expandedFormDlg.value) {
isExpandedFormCloseAfterSave.value = false
if (!isForm.value) {
loadChildrenList()
}
loadChildrenExcludedList(rowState.value) loadChildrenExcludedList(rowState.value)
} }
}) })
@ -173,6 +182,42 @@ const onClick = (refRow: any, id: string) => {
linkRow(refRow, Number.parseInt(id)) linkRow(refRow, Number.parseInt(id))
} }
} }
const addNewRecord = () => {
expandedFormRow.value = {}
expandedFormDlg.value = true
isExpandedFormCloseAfterSave.value = true
}
const onCreatedRecord = (record: any) => {
const msgVNode = h(
'div',
{
class: 'ml-1 inline-flex flex-col gap-1 items-start',
},
[
h(
'span',
{
class: 'font-semibold',
},
t('activity.recordCreatedLinked'),
),
h(
'span',
{
class: 'text-gray-500',
},
t('activity.gotSavedLinkedSuccessfully', {
tableName: relatedTableMeta.value?.title,
recordTitle: record[relatedTableDisplayValueProp.value],
}),
),
],
)
message.success(msgVNode)
}
</script> </script>
<template> <template>
@ -191,14 +236,14 @@ const onClick = (refRow: any, id: string) => {
:table-title="meta?.title" :table-title="meta?.title"
:related-table-title="relatedTableMeta?.title" :related-table-title="relatedTableMeta?.title"
:display-value="row.row[displayValueProp]" :display-value="row.row[displayValueProp]"
:header="$t('activity.addNewLink')"
/> />
<div class="!xs:hidden my-3 bg-gray-50 border-gray-50 border-b-2"></div>
<div class="flex mt-2 mb-2 items-center gap-2"> <div class="flex mt-2 mb-2 items-center gap-2">
<div <div
class="flex items-center border-1 p-1 rounded-md w-full border-gray-200" class="flex items-center border-1 p-1 rounded-md w-full border-gray-200"
:class="{ '!border-primary': childrenExcludedListPagination.query.length !== 0 || isFocused }" :class="{ '!border-primary': childrenExcludedListPagination.query.length !== 0 || isFocused }"
> >
<MdiMagnify class="w-5 h-5 ml-2" /> <MdiMagnify class="w-5 h-5 ml-2 text-gray-500" />
<a-input <a-input
ref="filterQueryRef" ref="filterQueryRef"
v-model:value="childrenExcludedListPagination.query" v-model:value="childrenExcludedListPagination.query"
@ -223,12 +268,7 @@ const onClick = (refRow: any, id: string) => {
type="secondary" type="secondary"
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
class="!text-brand-500" class="!text-brand-500"
@click=" @click="addNewRecord"
() => {
expandedFormRow = {}
expandedFormDlg = true
}
"
> >
<div class="flex items-center gap-1 px-4"><MdiPlus v-if="!isMobileMode" /> {{ $t('activity.newRecord') }}</div> <div class="flex items-center gap-1 px-4"><MdiPlus v-if="!isMobileMode" /> {{ $t('activity.newRecord') }}</div>
</NcButton> </NcButton>
@ -344,6 +384,15 @@ const onClick = (refRow: any, id: string) => {
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])" :row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
:state="newRowState" :state="newRowState"
use-meta-fields use-meta-fields
:close-after-save="isExpandedFormCloseAfterSave"
:new-record-header="
isExpandedFormCloseAfterSave
? $t('activity.tableNameCreateNewRecord', {
tableName: relatedTableMeta?.title,
})
: undefined
"
@created-record="onCreatedRecord"
/> />
</Suspense> </Suspense>
</NcModal> </NcModal>

10
packages/nc-gui/composables/useColumnCreateStore.ts

@ -40,6 +40,8 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const { sqlUis } = storeToRefs(baseStore) const { sqlUis } = storeToRefs(baseStore)
const { bases } = storeToRefs(useBases())
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { getMeta } = useMetas() const { getMeta } = useMetas()
@ -64,6 +66,12 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isXcdbBaseFunc(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]), isXcdbBaseFunc(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]),
) )
const source = computed(() =>
meta.value && meta.value.source_id && meta.value.base_id
? bases.value.get(meta.value?.base_id as string)?.sources?.find((s) => s.id === meta.value!.source_id)
: undefined,
)
const idType = null const idType = null
const additionalValidations = ref<ValidationsObj>({}) const additionalValidations = ref<ValidationsObj>({})
@ -128,7 +136,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
}) })
}, },
}, },
fieldLengthValidator(), fieldLengthValidator(source.value?.type || ClientType.MYSQL),
], ],
uidt: [ uidt: [
{ {

10
packages/nc-gui/composables/useViewGroupBy.ts

@ -89,6 +89,12 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
if (col.uidt === UITypes.Checkbox) { if (col.uidt === UITypes.Checkbox) {
return value ? GROUP_BY_VARS.TRUE : GROUP_BY_VARS.FALSE return value ? GROUP_BY_VARS.TRUE : GROUP_BY_VARS.FALSE
} }
// convert to JSON string if non-string value
if (value && typeof value === 'object') {
value = JSON.stringify(value)
}
return value ?? GROUP_BY_VARS.NULL return value ?? GROUP_BY_VARS.NULL
} }
@ -144,13 +150,13 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
const calculateNestedWhere = (nestedIn: GroupNestedIn[], existing = '') => { const calculateNestedWhere = (nestedIn: GroupNestedIn[], existing = '') => {
return nestedIn.reduce((acc, curr) => { return nestedIn.reduce((acc, curr) => {
if (curr.key === GROUP_BY_VARS.NULL) { if (curr.key === GROUP_BY_VARS.NULL) {
acc += `${acc.length ? '~and' : ''}(${curr.title},blank)` acc += `${acc.length ? '~and' : ''}(${curr.title},gb_null)`
} else if (curr.column_uidt === UITypes.Checkbox) { } else if (curr.column_uidt === UITypes.Checkbox) {
acc += `${acc.length ? '~and' : ''}(${curr.title},${curr.key === GROUP_BY_VARS.TRUE ? 'checked' : 'notchecked'})` acc += `${acc.length ? '~and' : ''}(${curr.title},${curr.key === GROUP_BY_VARS.TRUE ? 'checked' : 'notchecked'})`
} else if ([UITypes.Date, UITypes.DateTime].includes(curr.column_uidt as UITypes)) { } else if ([UITypes.Date, UITypes.DateTime].includes(curr.column_uidt as UITypes)) {
acc += `${acc.length ? '~and' : ''}(${curr.title},eq,exactDate,${curr.key})` acc += `${acc.length ? '~and' : ''}(${curr.title},eq,exactDate,${curr.key})`
} else { } else {
acc += `${acc.length ? '~and' : ''}(${curr.title},eq,${curr.key})` acc += `${acc.length ? '~and' : ''}(${curr.title},gb_eq,${curr.key})`
} }
return acc return acc
}, existing) }, existing)

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

@ -713,6 +713,8 @@
"inviteTeam": "Invite Team", "inviteTeam": "Invite Team",
"inviteUser": "Invite User", "inviteUser": "Invite User",
"inviteToken": "Invite Token", "inviteToken": "Invite Token",
"linkedRecords": "Linked Records",
"addNewLink": "Add New Link",
"newUser": "New User", "newUser": "New User",
"editUser": "Edit user", "editUser": "Edit user",
"deleteUser": "Remove user from base", "deleteUser": "Remove user from base",
@ -799,6 +801,9 @@
"linkRecord": "Link record", "linkRecord": "Link record",
"addNewRecord": "Add new record", "addNewRecord": "Add new record",
"newRecord": "New record", "newRecord": "New record",
"tableNameCreateNewRecord": "{tableName}: Create new record",
"gotSavedLinkedSuccessfully": "{tableName} '{recordTitle}' got saved & linked successfully",
"recordCreatedLinked": "Record Created & Linked",
"useConnectionUrl": "Use Connection URL", "useConnectionUrl": "Use Connection URL",
"toggleCommentsDraw": "Toggle comments draw", "toggleCommentsDraw": "Toggle comments draw",
"expandRecord": "Expand Record", "expandRecord": "Expand Record",

2
packages/nc-gui/lib/enums.ts

@ -25,7 +25,7 @@ export enum Language {
id = 'Bahasa Indonesia', id = 'Bahasa Indonesia',
it = 'Italiano', it = 'Italiano',
ja = '日本語', ja = '日本語',
ko = '한국', ko = '한국',
lv = 'Latviešu', lv = 'Latviešu',
nl = 'Nederlandse', nl = 'Nederlandse',
no = 'Norsk', no = 'Norsk',

102
packages/nc-gui/package.json

@ -37,42 +37,42 @@
}, },
"dependencies": { "dependencies": {
"@braks/revue-draggable": "^0.4.3", "@braks/revue-draggable": "^0.4.3",
"@ckpack/vue-color": "^1.2.0", "@ckpack/vue-color": "^1.5.0",
"@iconify/vue": "^4.1.1", "@iconify/vue": "^4.1.1",
"@pinia/nuxt": "^0.4.11", "@pinia/nuxt": "^0.4.11",
"@vue-flow/additional-components": "^1.2.0", "@vue-flow/additional-components": "^1.3.3",
"@vue-flow/core": "^1.3.0", "@vue-flow/core": "^1.26.0",
"@vuelidate/core": "^2.0.0-alpha.44", "@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.0-alpha.31", "@vuelidate/validators": "^2.0.4",
"@vueuse/core": "^10.2.1", "@vueuse/core": "^10.2.1",
"@vueuse/integrations": "^10.2.1", "@vueuse/integrations": "^10.2.1",
"ant-design-vue": "^3.2.11", "ant-design-vue": "^3.2.20",
"chart.js": "^4.3.0", "chart.js": "^4.4.0",
"crossoriginworker": "^1.1.0", "crossoriginworker": "^1.1.0",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"dayjs": "^1.11.9", "dayjs": "^1.11.10",
"deep-object-diff": "^1.1.9", "deep-object-diff": "^1.1.9",
"emoji-mart-vue-fast": "^15.0.0", "emoji-mart-vue-fast": "^15.0.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"httpsnippet": "^2.0.0", "httpsnippet": "^2.0.0",
"jsbarcode": "^3.11.5", "jsbarcode": "^3.11.6",
"jsep": "^1.3.6", "jsep": "^1.3.8",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"leaflet": "^1.9.2", "leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"locale-codes": "^1.3.1", "locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"monaco-sql-languages": "^0.11.0", "monaco-sql-languages": "^0.11.0",
"nocodb-sdk": "workspace:^", "nocodb-sdk": "workspace:^",
"papaparse": "^5.3.2", "papaparse": "^5.4.1",
"parse-github-url": "^1.0.2", "parse-github-url": "^1.0.2",
"pinia": "^2.1.4", "pinia": "^2.1.7",
"qrcode": "^1.5.1", "qrcode": "^1.5.3",
"rfdc": "^1.3.0", "rfdc": "^1.3.0",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"socket.io-client": "^4.5.1", "socket.io-client": "^4.7.2",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"splitpanes": "^3.1.5", "splitpanes": "^3.1.5",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
@ -84,56 +84,56 @@
"vue-dompurify-html": "^3.0.0", "vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.1.0", "vue-github-button": "^3.1.0",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",
"vue-qrcode-reader": "3.1.0-vue3-compatibility.2", "vue-qrcode-reader": "3.1.8",
"vue3-calendar-heatmap": "^2.0.0", "vue3-calendar-heatmap": "^2.0.5",
"vue3-contextmenu": "^0.2.12", "vue3-contextmenu": "^0.2.12",
"vue3-grid-layout-next": "^1.0.5", "vue3-grid-layout-next": "^1.0.6",
"vue3-text-clamp": "^0.1.1", "vue3-text-clamp": "^0.1.2",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.26.0", "@antfu/eslint-config": "^0.26.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@iconify-json/ant-design": "^1.1.9", "@iconify-json/ant-design": "^1.1.10",
"@iconify-json/bi": "^1.1.18", "@iconify-json/bi": "^1.1.20",
"@iconify-json/carbon": "^1.1.20", "@iconify-json/carbon": "^1.1.21",
"@iconify-json/cil": "^1.1.5", "@iconify-json/cil": "^1.1.5",
"@iconify-json/clarity": "^1.1.9", "@iconify-json/clarity": "^1.1.9",
"@iconify-json/eva": "^1.1.7", "@iconify-json/eva": "^1.1.7",
"@iconify-json/ic": "^1.1.14", "@iconify-json/ic": "^1.1.14",
"@iconify-json/ion": "^1.1.12", "@iconify-json/ion": "^1.1.12",
"@iconify-json/la": "^1.1.5", "@iconify-json/la": "^1.1.5",
"@iconify-json/logos": "^1.1.34", "@iconify-json/logos": "^1.1.38",
"@iconify-json/lucide": "^1.1.119", "@iconify-json/lucide": "^1.1.141",
"@iconify-json/material-symbols": "^1.1.57", "@iconify-json/material-symbols": "^1.1.63",
"@iconify-json/mdi": "^1.1.54", "@iconify-json/mdi": "^1.1.55",
"@iconify-json/mi": "^1.1.5", "@iconify-json/mi": "^1.1.5",
"@iconify-json/ph": "^1.1.6", "@iconify-json/ph": "^1.1.6",
"@iconify-json/ri": "^1.1.12", "@iconify-json/ri": "^1.1.12",
"@iconify-json/simple-icons": "^1.1.67", "@iconify-json/simple-icons": "^1.1.78",
"@iconify-json/system-uicons": "^1.1.9", "@iconify-json/system-uicons": "^1.1.9",
"@iconify-json/tabler": "^1.1.89", "@iconify-json/tabler": "^1.1.96",
"@iconify-json/vscode-icons": "^1.1.28", "@iconify-json/vscode-icons": "^1.1.29",
"@intlify/unplugin-vue-i18n": "^0.12.2", "@intlify/unplugin-vue-i18n": "^0.12.3",
"@nuxt/image-edge": "^1.0.0-rc.1-28217290.de85e17", "@nuxt/image-edge": "1.0.0-28336957.57c0f74",
"@types/d3-scale": "^4.0.3", "@types/d3-scale": "^4.0.8",
"@types/dagre": "^0.7.48", "@types/dagre": "^0.7.52",
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.7",
"@types/leaflet": "^1.9.0", "@types/leaflet": "^1.9.8",
"@types/leaflet.markercluster": "^1.5.1", "@types/leaflet.markercluster": "^1.5.4",
"@types/papaparse": "^5.3.2", "@types/papaparse": "^5.3.11",
"@types/parse-github-url": "^1.0.0", "@types/parse-github-url": "^1.0.3",
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.5.5",
"@types/showdown": "^2.0.0", "@types/showdown": "^2.0.4",
"@types/sortablejs": "^1.13.0", "@types/sortablejs": "^1.13.0",
"@types/splitpanes": "^2.2.1", "@types/splitpanes": "^2.2.5",
"@types/tinycolor2": "^1.4.3", "@types/tinycolor2": "^1.4.6",
"@types/validator": "^13.7.10", "@types/validator": "^13.11.6",
"@types/vue-barcode-reader": "^0.0.0", "@types/vue-barcode-reader": "^0.0.3",
"@unocss/nuxt": "^0.51.12", "@unocss/nuxt": "^0.51.13",
"@vitest/ui": "^0.18.0", "@vitest/ui": "^0.18.1",
"@vue/compiler-sfc": "^3.2.37", "@vue/compiler-sfc": "^3.3.8",
"@vue/test-utils": "^2.0.2", "@vue/test-utils": "^2.0.2",
"@vueuse/nuxt": "^10.2.1", "@vueuse/nuxt": "^10.2.1",
"@windicss/plugin-animations": "^1.0.9", "@windicss/plugin-animations": "^1.0.9",
@ -142,11 +142,11 @@
"eslint": "^8.33.0", "eslint": "^8.33.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"happy-dom": "^6.0.3", "happy-dom": "^6.0.4",
"nuxt": "^3.8.1", "nuxt": "^3.8.1",
"nuxt-windicss": "^2.6.1", "nuxt-windicss": "^2.6.1",
"prettier": "^2.7.1", "prettier": "^2.8.8",
"sass": "^1.63.4", "sass": "^1.69.5",
"ts-loader": "^9.4.4", "ts-loader": "^9.4.4",
"unplugin-icons": "^0.14.15", "unplugin-icons": "^0.14.15",
"unplugin-vue-components": "^0.22.12", "unplugin-vue-components": "^0.22.12",

11
packages/nc-gui/store/sidebar.ts

@ -42,6 +42,16 @@ export const useSidebarStore = defineStore('sidebarStore', () => {
const leftSidebarWidth = computed(() => (width.value * mobileNormalizedSidebarSize.value) / 100) const leftSidebarWidth = computed(() => (width.value * mobileNormalizedSidebarSize.value) / 100)
const nonHiddenMobileSidebarSize = computed(() => {
if (isMobileMode.value) {
return 100
}
return leftSideBarSize.value.current ?? leftSideBarSize.value.old
})
const nonHiddenLeftSidebarWidth = computed(() => (width.value * nonHiddenMobileSidebarSize.value) / 100)
return { return {
isLeftSidebarOpen, isLeftSidebarOpen,
isRightSidebarOpen, isRightSidebarOpen,
@ -50,6 +60,7 @@ export const useSidebarStore = defineStore('sidebarStore', () => {
leftSidebarState, leftSidebarState,
leftSidebarWidth, leftSidebarWidth,
mobileNormalizedSidebarSize, mobileNormalizedSidebarSize,
nonHiddenLeftSidebarWidth,
} }
}) })

22
packages/nc-gui/utils/validation.ts

@ -13,6 +13,10 @@ export const validateTableName = {
return reject(new Error(t('msg.error.tableNameRequired'))) return reject(new Error(t('msg.error.tableNameRequired')))
} }
if (value.length > 52) {
return reject(new Error(t('msg.error.columnNameExceedsCharacters', { value: 52 })))
}
// exclude . / \ // exclude . / \
// rest all characters allowed // rest all characters allowed
// https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/acreldb/n0rfg6x1shw0ppn1cwhco6yn09f7.htm#:~:text=By%20default%2C%20MySQL%20encloses%20column,not%20truncate%20a%20longer%20name. // https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/acreldb/n0rfg6x1shw0ppn1cwhco6yn09f7.htm#:~:text=By%20default%2C%20MySQL%20encloses%20column,not%20truncate%20a%20longer%20name.
@ -98,17 +102,21 @@ export const fieldRequiredValidator = () => {
} }
} }
export const fieldLengthValidator = () => { export const fieldLengthValidator = (sqlClientType: string) => {
return { return {
validator: (rule: any, value: any) => { validator: (rule: any, value: any) => {
const { t } = getI18n().global const { t } = getI18n().global
// mysql allows 64 characters for column_name // no limit for sqlite but set as 255
// postgres allows 59 characters for column_name let fieldLengthLimit = 255
// mssql allows 128 characters for column_name
// sqlite allows any number of characters for column_name if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
// We allow 255 for all databases, truncate will be handled by backend for column_name fieldLengthLimit = 64
const fieldLengthLimit = 255 } else if (sqlClientType === 'pg') {
fieldLengthLimit = 59
} else if (sqlClientType === 'mssql') {
fieldLengthLimit = 128
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (value?.length > fieldLengthLimit) { if (value?.length > fieldLengthLimit) {

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

@ -1,6 +1,6 @@
{ {
"name": "nc-lib-gui", "name": "nc-lib-gui",
"version": "0.202.5", "version": "0.202.7",
"description": "NocoDB GUI", "description": "NocoDB GUI",
"author": { "author": {
"name": "NocoDB", "name": "NocoDB",

2
packages/noco-docs/docs/030.workspaces/040.actions-on-workspace.md

@ -20,7 +20,7 @@ To update the workspace name:
## Delete workspace ## Delete workspace
If you determine that a workspace is no longer necessary, you have the option to permanently remove it from your settings. Deleting a workspace will delete all the bases and data associated with it. If you determine that a workspace is no longer necessary, you have the option to permanently remove it from your settings. Deleting a workspace will delete all the bases and data associated with it.
:::danger :::info
**This action cannot be undone.** **This action cannot be undone.**
::: :::

2
packages/noco-docs/docs/040.bases/070.actions-on-base.md

@ -69,7 +69,7 @@ To duplicate a base, you can follow these straightforward steps:
If you determine that a base is no longer necessary, you have the option to permanently remove it from your workspace. Deleting a base will delete all the tables and data associated with it. If you determine that a base is no longer necessary, you have the option to permanently remove it from your workspace. Deleting a base will delete all the tables and data associated with it.
:::danger :::info
**This action cannot be undone.** **This action cannot be undone.**
::: :::

2
packages/noco-docs/docs/050.tables/060.actions-on-table.md

@ -46,7 +46,7 @@ A new table will be generated, mirroring the original table's schema and content
## Delete table ## Delete table
:::danger :::info
**This action cannot be undone.** **This action cannot be undone.**
::: :::

12
packages/noco-docs/docs/070.fields/040.field-types/050.custom-types/010.attachment.md

@ -39,21 +39,21 @@ Expand modal for `Attachment` field displays the list of files uploaded to the f
Expand modal supports the following actions: Expand modal supports the following actions:
### Attach file(s) ### Attach file(s)
- Click on `Attach file(s)` button <1> - Click on `Attach file(s)` button {"<"}1{">"}
- Choose the file(s) to upload - Choose the file(s) to upload
### Delete file ### Delete file
- Click on `x` icon <2> to the top left of the image card to delete the file - Click on `x` icon {"<"}2{">"} to the top left of the image card to delete the file
### Download file ### Download file
- Click on `Download` button <5> to download the file - Click on `Download` button {"<"}5{">"} to download the file
### Bulk Download file(s) ### Bulk Download file(s)
- Select the files by clicking on the checkbox <3> to the top left of the image card - Select the files by clicking on the checkbox {"<"}3{">"} to the top left of the image card
- Click on `Bulk Download` button <4> to download the selected files - Click on `Bulk Download` button {"<"}4{">"} to download the selected files
### Rename file ### Rename file
- Click on `Rename` button <5> to rename the file - Click on `Rename` button {"<"}5{">"} to rename the file
- Enter the new name in the input field - Enter the new name in the input field
- Click on `Rename` button to save the new name - Click on `Rename` button to save the new name

2
packages/noco-docs/docs/070.fields/040.field-types/060.formula/015.operators.md

@ -17,7 +17,7 @@ keywords: ['Fields', 'Field types', 'Formula', 'Create formula field', 'Numeric
:::tip :::tip
To change the order of arithmetic operation, you can use round bracket parenthesis (). To change the order of arithmetic operation, you can use round bracket parenthesis ().
Example: ({field1} + ({field2} * {field3}) / (3 - $field4$ )) Example: `({field1} + ({field2} * {field3}) / (3 - {field4} ))`
::: :::

16
packages/noco-docs/docs/070.fields/040.field-types/060.formula/040.date-functions.md

@ -10,14 +10,14 @@ keywords: ['Fields', 'Field types', 'Formula', 'Date & Time', 'Create formula fi
| Name | Syntax | Sample | Output | Remark | | Name | Syntax | Sample | Output | Remark |
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **NOW** | `NOW()` | `NOW()` | 2022-05-19 17:20:43 | Returns the current time and day | | **NOW** | `NOW()` | `NOW()` | 2022-05-19 17:20:43 | Returns the current time and day |
| | `IF(NOW() < {DATE_COL}, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than {DATE_COL}, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. | | | `IF(NOW() < {DATE_COL}`, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than `{DATE_COL}`, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. |
| **DATEADD** | `DATEADD(date \| datetime, value, ["day" \| "week" \| "month" \| "year"])` | `DATEADD(date, 1, 'day')` | Supposing {DATE_COL} is 2022-03-14. The result is 2022-03-15. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` | | **DATEADD** | `DATEADD(date \| datetime, value, ["day" \| "week" \| "month" \| "year"])` | `DATEADD(date, 1, 'day')` | Supposing `{DATE_COL}` is 2022-03-14. The result is 2022-03-15. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` |
| | | `DATEADD(date, 1, 'week')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2022-03-21 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'week')` | | | | `DATEADD(date, 1, 'week')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2022-03-21 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'week')` |
| | | `DATEADD(date, 1, 'month')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2022-04-14 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'month')` | | | | `DATEADD(date, 1, 'month')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2022-04-14 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'month')` |
| | | `DATEADD(date, 1, 'year')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` | | | | `DATEADD(date, 1, 'year')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. | | | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than `{DATE_COL}` plus 10 days, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. | | | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than `{DATE_COL}` plus 10 days, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. |
| **DATETIME_DIFF** | `DATETIME_DIFF(date, date, ["milliseconds" \| "ms" \| "seconds" \| "s" \| "minutes" \| "m" \| "hours" \| "h" \| "days" \| "d" \| "weeks" \| "w" \| "months" \| "M" \| "quarters" \| "Q" \| "years" \| "y"])` | `DATETIME_DIFF("2022/10/14", "2022/10/15", "second")` | Supposing {DATE_COL_1} is 2017-08-25 and {DATE_COL_2} is 2011-08-25. The result is 86400. | Compares two dates and returns the difference in the unit specified. Positive integers indicate the second date being in the past compared to the first and vice versa for negative ones. | | **DATETIME_DIFF** | `DATETIME_DIFF(date, date, ["milliseconds" \| "ms" \| "seconds" \| "s" \| "minutes" \| "m" \| "hours" \| "h" \| "days" \| "d" \| "weeks" \| "w" \| "months" \| "M" \| "quarters" \| "Q" \| "years" \| "y"])` | `DATETIME_DIFF("2022/10/14", "2022/10/15", "second")` | Supposing `{DATE_COL_1}` is 2017-08-25 and `{DATE_COL_2}` is 2011-08-25. The result is 86400. | Compares two dates and returns the difference in the unit specified. Positive integers indicate the second date being in the past compared to the first and vice versa for negative ones. |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday | | | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday |
| **WEEKDAY** | `WEEKDAY(date, [startDayOfWeek])` | `WEEKDAY(NOW())` | If today is Monday, it returns 0 | Returns the day of the week as an integer between 0 and 6 inclusive starting from Monday by default. You can optionally change the start day of the week by specifying in the second argument | | **WEEKDAY** | `WEEKDAY(date, [startDayOfWeek])` | `WEEKDAY(NOW())` | If today is Monday, it returns 0 | Returns the day of the week as an integer between 0 and 6 inclusive starting from Monday by default. You can optionally change the start day of the week by specifying in the second argument |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday | | | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday |

2
packages/noco-docs/docs/070.fields/060.actions-on-field.md

@ -83,7 +83,7 @@ New field will be created to the right of the original field.
New field will be created to the left of the original field. New field will be created to the left of the original field.
### Delete field ### Delete field
:::danger :::info
**This action cannot be undone.** **This action cannot be undone.**
::: :::

4
packages/noco-docs/docs/080.records/070.actions-on-record.md

@ -54,8 +54,8 @@ On the bulk update modal,
5. Click on the `Bulk Update all` button 5. Click on the `Bulk Update all` button
6. A confirmation dialog will be displayed. Click on `Confirm` to update the records. 6. A confirmation dialog will be displayed. Click on `Confirm` to update the records.
:::danger :::info
This operation cannot be undone. **This action cannot be undone.**
::: :::
![Bulk Update](/img/v2/records/bulk-update-1.png) ![Bulk Update](/img/v2/records/bulk-update-1.png)

4
packages/noco-docs/docs/090.views/040.view-types/030.form.md

@ -28,7 +28,7 @@ Form view builder layout can be divided into 3 sections:
## Form View Operations ## Form View Operations
### Add Form Title & Description ### Add Form Title & Description
In the **Form View** area, click on in input boxes provided for **Title** <1> & **Description** <2> to add/update title & description to the form. In the **Form View** area, click on in input boxes provided for **Title** {"<"}1{">"} & **Description** {"<"}2{">"} to add/update title & description to the form.
![Form Title & Description](/img/v2/views/form-view-title-description.png) ![Form Title & Description](/img/v2/views/form-view-title-description.png)
@ -38,7 +38,7 @@ To add a field to the form, either
- Click on the field in the **Fields Area** to add it to the end of the **Form Area** - Click on the field in the **Fields Area** to add it to the end of the **Form Area**
### Change field label & help-text ### Change field label & help-text
To change the field label displayed on the form & add help-text, click on the field in the **Form Area** and update the values in the input boxes provided for **Label** <1> & **Help Text** <2>. To change the field label displayed on the form & add help-text, click on the field in the **Form Area** and update the values in the input boxes provided for **Label** {"<"}1{">"} & **Help Text** {"<"}2{">"}.
![Field Label & Help Text](/img/v2/views/form-view-field-label-help-text.png) ![Field Label & Help Text](/img/v2/views/form-view-field-label-help-text.png)

2
packages/noco-docs/docs/090.views/090.actions-on-view.md

@ -41,7 +41,7 @@ The view context menu provides a set of tools to interact with the view. The vie
## Delete view ## Delete view
:::danger :::info
**This action cannot be undone.** **This action cannot be undone.**
::: :::

6
packages/noco-docs/docs/150.engineering/060.builds-and-releases.md

@ -137,13 +137,13 @@ When a non-draft Pull Request is created, reopened or synchronized, a timely bui
- `packages/nc-plugin/**` - `packages/nc-plugin/**`
- `packages/nocodb/**` - `packages/nocodb/**`
The docker images will be built and pushed to Docker Hub (See [nocodb/nocodb-timely](https://hub.docker.com/r/nocodb/nocodb-timely/tags) for the full list). Once the image is ready, Github bot will add a comment with the command in the pull request. The tag would be `<NOCODB_CURRENT_VERSION>-pr-<PR_NUMBER>-<YYYYMMDD>-<HHMM>`. The docker images will be built and pushed to Docker Hub (See [nocodb/nocodb-timely](https://hub.docker.com/r/nocodb/nocodb-timely/tags) for the full list). Once the image is ready, GitHub bot will add a comment with the command in the pull request. The tag would be `<NOCODB_CURRENT_VERSION>-pr-<PR_NUMBER>-<YYYYMMDD>-<HHMM>`.
![image](https://user-images.githubusercontent.com/35857179/175012097-240dab05-da93-4c4e-87c1-1c36fb1350bd.png) ![image](https://user-images.githubusercontent.com/35857179/175012097-240dab05-da93-4c4e-87c1-1c36fb1350bd.png)
## Executables or Binaries ## Executables or Binaries
Similarly, we provide a timely build for executables for non-docker users. The source code will be built, packaged as binary files, and pushed to Github (See [nocodb/nocodb-timely](https://github.com/nocodb/nocodb-timely/releases) for the full list). Similarly, we provide a timely build for executables for non-docker users. The source code will be built, packaged as binary files, and pushed to GitHub (See [nocodb/nocodb-timely](https://github.com/nocodb/nocodb-timely/releases) for the full list).
Currently, we only support the following targets: Currently, we only support the following targets:
@ -154,7 +154,7 @@ Currently, we only support the following targets:
- `node16-macos-x64` - `node16-macos-x64`
- `node16-win-x64` - `node16-win-x64`
Once the executables are ready, Github bot will add a comment with the commands in the pull request. Once the executables are ready, GitHub bot will add a comment with the commands in the pull request.
![image](https://user-images.githubusercontent.com/35857179/175012070-f5f3e7b8-6dc5-4d1c-9f7e-654bc5039521.png) ![image](https://user-images.githubusercontent.com/35857179/175012070-f5f3e7b8-6dc5-4d1c-9f7e-654bc5039521.png)

2
packages/noco-docs/docs/150.engineering/070.translation.md

@ -9,7 +9,7 @@ tags: ['Engineering']
## How to add / edit translations ? ## How to add / edit translations ?
### Using Github ### Using GitHub
- For English, make changes directly to [en.json](https://github.com/nocodb/nocodb/blob/develop/packages/nc-gui/lang/en.json) & commit to `develop` - For English, make changes directly to [en.json](https://github.com/nocodb/nocodb/blob/develop/packages/nc-gui/lang/en.json) & commit to `develop`
- For any other language, use `crowdin` option. - For any other language, use `crowdin` option.

7586
packages/noco-docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

30
packages/noco-docs/package.json

@ -28,26 +28,26 @@
"typecheck": "tsc" "typecheck": "tsc"
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "2.4.1", "@docusaurus/core": "3.0.0",
"@docusaurus/plugin-client-redirects": "2.4.1", "@docusaurus/plugin-client-redirects": "3.0.0",
"@docusaurus/plugin-ideal-image": "2.4.1", "@docusaurus/plugin-ideal-image": "3.0.0",
"@docusaurus/plugin-sitemap": "2.4.1", "@docusaurus/plugin-sitemap": "3.0.0",
"@docusaurus/preset-classic": "2.4.1", "@docusaurus/preset-classic": "3.0.0",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "^3.0.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"docusaurus-plugin-sass": "^0.2.5", "docusaurus-plugin-sass": "^0.2.5",
"docusaurus-theme-search-typesense": "^0.12.0-0", "docusaurus-theme-search-typesense": "^0.14.0",
"nc-analytics": "^0.0.4", "nc-analytics": "^0.0.7",
"plugin-image-zoom": "github:flexanalytics/plugin-image-zoom", "plugin-image-zoom": "github:flexanalytics/plugin-image-zoom",
"prism-react-renderer": "^1.3.5", "prism-react-renderer": "^1.3.5",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"sass": "^1.66.1" "sass": "^1.69.5"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "2.4.1", "@docusaurus/module-type-aliases": "3.0.0",
"@tsconfig/docusaurus": "^1.0.5", "@tsconfig/docusaurus": "^1.0.7",
"typescript": "^4.7.4" "typescript": "^4.9.5"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@ -62,6 +62,6 @@
] ]
}, },
"engines": { "engines": {
"node": ">=16.14" "node": ">=16.14.2"
} }
} }

18
packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/090.formulas.md vendored

@ -62,7 +62,7 @@ Unlike other column types, formula cells cannot be modified by double-clicking s
:::tip :::tip
To change the order of arithmetic operation, you can use round bracket parantheses (). <br/> To change the order of arithmetic operation, you can use round bracket parantheses (). <br/>
Example: ({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ )) Example: `({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ ))`
::: :::
@ -89,14 +89,14 @@ Example: ({Column1} + ({Column2} * {Column3}) / (3 - $Column4$ ))
| Name | Syntax | Sample | Output | Remark | | Name | Syntax | Sample | Output | Remark |
|---|---|---|---|---| |---|---|---|---|---|
| **NOW** | `NOW()` | `NOW()` | 2022-05-19 17:20:43 | Returns the current time and day | | **NOW** | `NOW()` | `NOW()` | 2022-05-19 17:20:43 | Returns the current time and day |
| | `IF(NOW() < {DATE_COL}, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than {DATE_COL}, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | | | `IF(NOW() < {DATE_COL}, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than `{DATE_COL}`, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| **DATEADD** | `DATEADD(date \| datetime, value, ["day" \| "week" \| "month" \| "year"])` | `DATEADD(date, 1, 'day')` | Supposing {DATE_COL} is 2022-03-14. The result is 2022-03-15. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` | | **DATEADD** | `DATEADD(date \| datetime, value, ["day" \| "week" \| "month" \| "year"])` | `DATEADD(date, 1, 'day')` | Supposing `{DATE_COL}` is 2022-03-14. The result is 2022-03-15. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` |
| | | `DATEADD(date, 1, 'week')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2022-03-21 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'week')` | | | | `DATEADD(date, 1, 'week')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2022-03-21 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'week')` |
| | | `DATEADD(date, 1, 'month')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2022-04-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'month')` | | | | `DATEADD(date, 1, 'month')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2022-04-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'month')` |
| | | `DATEADD(date, 1, 'year')` | Supposing {DATE_COL} is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` | | | | `DATEADD(date, 1, 'year')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime columns and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | | | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than `{DATE_COL}` plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than {DATE_COL} plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. | | | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than `{DATE_COL}` plus 10 days, it returns true. Otherwise, it returns false. | DateTime columns and negative values are supported. |
| **DATETIME_DIFF** | `DATETIME_DIFF(date, date, ["milliseconds" \| "ms" \| "seconds" \| "s" \| "minutes" \| "m" \| "hours" \| "h" \| "days" \| "d" \| "weeks" \| "w" \| "months" \| "M" \| "quarters" \| "Q" \| "years" \| "y"])` | `DATETIME_DIFF("2022/10/14", "2022/10/15", "second")` | Supposing {DATE_COL_1} is 2017-08-25 and {DATE_COL_2} is 2011-08-25. The result is 86400. | Compares two dates and returns the difference in the unit specified. Positive integers indicate the second date being in the past compared to the first and vice versa for negative ones. | | **DATETIME_DIFF** | `DATETIME_DIFF(date, date, ["milliseconds" \| "ms" \| "seconds" \| "s" \| "minutes" \| "m" \| "hours" \| "h" \| "days" \| "d" \| "weeks" \| "w" \| "months" \| "M" \| "quarters" \| "Q" \| "years" \| "y"])` | `DATETIME_DIFF("2022/10/14", "2022/10/15", "second")` | Supposing `{DATE_COL_1}` is 2017-08-25 and `{DATE_COL_2}` is 2011-08-25. The result is 86400. | Compares two dates and returns the difference in the unit specified. Positive integers indicate the second date being in the past compared to the first and vice versa for negative ones. |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday | | | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday |
| **WEEKDAY** | `WEEKDAY(date, [startDayOfWeek])` | `WEEKDAY(NOW())` | If today is Monday, it returns 0 | Returns the day of the week as an integer between 0 and 6 inclusive starting from Monday by default. You can optionally change the start day of the week by specifying in the second argument | | **WEEKDAY** | `WEEKDAY(date, [startDayOfWeek])` | `WEEKDAY(NOW())` | If today is Monday, it returns 0 | Returns the day of the week as an integer between 0 and 6 inclusive starting from Monday by default. You can optionally change the start day of the week by specifying in the second argument |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday | | | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday |

14
packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/160.views.md vendored

@ -12,10 +12,10 @@ To navigate different views, we can select the target one in the view sidebar. B
## View Menu Bar ## View Menu Bar
To work with `Views`, use View menu-bar on the right hand side - To work with `Views`, use View menu-bar on the right hand side -
- <1> Toggle View menu-bar. - {"<"}1{">"} Toggle View menu-bar.
- <2> Displays created view-list for the selected table - {"<"}2{">"} Displays created view-list for the selected table
- Currently active view is high-lighted - Currently active view is high-lighted
- <3> Add new view to the list - {"<"}3{">"} Add new view to the list
![image](https://user-images.githubusercontent.com/35857179/194814369-53fa8875-7610-4849-9a91-f94096b15b3f.png) ![image](https://user-images.githubusercontent.com/35857179/194814369-53fa8875-7610-4849-9a91-f94096b15b3f.png)
@ -77,7 +77,7 @@ We can apply permission to each View. By default, Collaborative Views will be us
### Create a View ### Create a View
Click '+' in View-menu sidebar, as shown in <3>. Click '+' in View-menu sidebar, as shown in {"<"}3{">"}.
### Rename a View ### Rename a View
@ -87,7 +87,7 @@ Double click on `view-name`, edit, <enter />.
### Delete a View ### Delete a View
Hover the target View and click the delete icon, as shown in <2>. Hover the target View and click the delete icon, as shown in {"<"}2{">"}.
:::note :::note
@ -99,12 +99,12 @@ You cannot delete the very first Grid View (termed as `Default view`).
### Duplicate a View ### Duplicate a View
Hover the target View and click the copy icon, as shown in <2>. Hover the target View and click the copy icon, as shown in {"<"}2{">"}.
<!-- ![image](https://user-images.githubusercontent.com/35857179/163353865-7275499e-c685-44f4-906c-ba08f0ee419e.png) --> <!-- ![image](https://user-images.githubusercontent.com/35857179/163353865-7275499e-c685-44f4-906c-ba08f0ee419e.png) -->
### Reorder a View ### Reorder a View
Hover the target View and re-order it as needed by drag-drop the drag icon, as shown in <1>. Hover the target View and re-order it as needed by drag-drop the drag icon, as shown in {"<"}1{">"}.
<!-- ![image](https://user-images.githubusercontent.com/35857179/163359674-c4aeff74-1cb4-498d-b79c-c6ddf84ad352.png) --> <!-- ![image](https://user-images.githubusercontent.com/35857179/163359674-c4aeff74-1cb4-498d-b79c-c6ddf84ad352.png) -->

6
packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/200.import-airtable-to-sql-database-within-a-minute-for-free.md vendored

@ -58,9 +58,9 @@ Below are 3 simple steps
<!-- ![image](https://user-images.githubusercontent.com/35857179/168773192-f3ef9d36-3329-4324-ae25-989b611f66bf.png) --> <!-- ![image](https://user-images.githubusercontent.com/35857179/168773192-f3ef9d36-3329-4324-ae25-989b611f66bf.png) -->
2. Input API key & Shared Base ID / URL (retrieved from `Get Airtable Credentials` above). 2. Input API key & Shared Base ID / URL (retrieved from `Get Airtable Credentials` above).
- <1> API Key - {"<"}1{">"} API Key
- <2> Share Base ID - {"<"}2{">"} Share Base ID
- <3> Configuration option - {"<"}3{">"} Configuration option
- Import Data: disable this option to import only table & view schema's - Import Data: disable this option to import only table & view schema's
- Import Secondary Views: disable this option to import only primary grid view per table - Import Secondary Views: disable this option to import only primary grid view per table
- Import Rollup Columns: disable this option to skip Rollup column import - Import Rollup Columns: disable this option to skip Rollup column import

4
packages/noco-docs/versioned_docs/version-0.109.7/030.setup-and-usages/230.team-and-auth.md vendored

@ -36,8 +36,8 @@ If you do not have an SMTP sender configured, make sure to copy the invite link
### How to Update user permissions ### How to Update user permissions
1. Use `Edit` <1> menu to assign a different role to existing user 1. Use `Edit` {"<"}1{">"} menu to assign a different role to existing user
2. Use `Delete` <2> menu to remove a user from accessing current project 2. Use `Delete` {"<"}2{">"} menu to remove a user from accessing current project
![image](https://user-images.githubusercontent.com/35857179/219830858-be7a4656-9f3b-440c-9a79-165f919223d7.png) ![image](https://user-images.githubusercontent.com/35857179/219830858-be7a4656-9f3b-440c-9a79-165f919223d7.png)

246
packages/noco-docs/versioned_docs/version-0.109.7/040.developer-resources/020.rest-apis.md vendored

@ -13,7 +13,7 @@ You may also interact with the API's resources via <a href="/0.109.7/developer-r
:::note :::note
Currently, the default value for {orgs} is <b>noco</b>. Users will be able to change it in the future release. Currently, the default value for `{orgs}` is <b>noco</b>. Users will be able to change it in the future release.
::: :::
@ -26,145 +26,145 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Auth | Get | auth | me | /api/v1/auth/user/me | | Auth | Get | auth | me | /api/v1/auth/user/me |
| Auth | Post | auth | passwordForgot | /api/v1/auth/password/forgot | | Auth | Post | auth | passwordForgot | /api/v1/auth/password/forgot |
| Auth | Post | auth | passwordChange | /api/v1/auth/password/change | | Auth | Post | auth | passwordChange | /api/v1/auth/password/change |
| Auth | Post | auth | passwordReset | /api/v1/auth/password/reset/{token} | | Auth | Post | auth | passwordReset | /api/v1/auth/password/reset/`{token}` |
| Auth | Post | auth | tokenRefresh | /api/v1/auth/token/refresh | | Auth | Post | auth | tokenRefresh | /api/v1/auth/token/refresh |
| Auth | Post | auth | passwordResetTokenValidate | /api/v1/auth/token/validate/{token} | | Auth | Post | auth | passwordResetTokenValidate | /api/v1/auth/token/validate/`{token}` |
| Auth | Post | auth | emailValidate | /api/v1/auth/email/validate/{email} | | Auth | Post | auth | emailValidate | /api/v1/auth/email/validate/`{email}` |
### Public APIs ### Public APIs
| Category | Method | Tag | Function Name | Path | | Category | Method | Tag | Function Name | Path |
|---|---|---|---|---| |---|---|---|---|---|
| Public | Get | public | sharedBaseGet | /api/v1/db/public/shared-base/{sharedBaseUuid}/meta | | Public | Get | public | sharedBaseGet | /api/v1/db/public/shared-base/`{sharedBaseUuid}`/meta |
| Public | Post | public | dataList | /api/v1/db/public/shared-view/{sharedViewUuid}/rows | | Public | Post | public | dataList | /api/v1/db/public/shared-view/`{sharedViewUuid}`/rows |
| Public | Get | public | dataNestedList | /api/v1/db/public/shared-view/{sharedViewUuid}/rows/{rowId}/{relationType}/{columnName} | | Public | Get | public | dataNestedList | /api/v1/db/public/shared-view/`{sharedViewUuid}`/rows/`{rowId}`/`{relationType}`/`{columnName}` |
| Public | Post | public | dataCreate | /api/v1/db/public/shared-view/{sharedViewUuid}/rows | | Public | Post | public | dataCreate | /api/v1/db/public/shared-view/`{sharedViewUuid}`/rows |
| Public | Get | public | csvExport | /api/v1/db/public/shared-view/{sharedViewUuid}/rows/export/{type} | | Public | Get | public | csvExport | /api/v1/db/public/shared-view/`{sharedViewUuid}`/rows/export/`{type}` |
| Public | Get | public | dataRelationList | /api/v1/db/public/shared-view/{sharedViewUuid}/nested/{columnName} | | Public | Get | public | dataRelationList | /api/v1/db/public/shared-view/`{sharedViewUuid}`/nested/`{columnName}` |
| Public | Get | public | sharedViewMetaGet | /api/v1/db/public/shared-view/{sharedViewUuid}/meta | | Public | Get | public | sharedViewMetaGet | /api/v1/db/public/shared-view/`{sharedViewUuid}`/meta |
| Public | Get | public | groupedDataList | /api/v1/db/public/shared-view/{sharedViewUuid}/group/{columnId} | | Public | Get | public | groupedDataList | /api/v1/db/public/shared-view/`{sharedViewUuid}`/group/`{columnId}` |
### Data APIs ### Data APIs
| Category | Method | Tag | Function Name | Path | | Category | Method | Tag | Function Name | Path |
|---|---|---|---|---| |---|---|---|---|---|
| Data | Delete| dbTableRow | bulkDelete | /api/v1/db/data/bulk/{orgs}/{projectName}/{tableName}/ | | Data | Delete| dbTableRow | bulkDelete | /api/v1/db/data/bulk/`{orgs}`/`{projectName}`/`{tableName}`/ |
| Data | Post | dbTableRow | bulkCreate | /api/v1/db/data/bulk/{orgs}/{projectName}/{tableName}/ | | Data | Post | dbTableRow | bulkCreate | /api/v1/db/data/bulk/`{orgs}`/`{projectName}`/`{tableName}`/ |
| Data | Patch | dbTableRow | bulkUpdate | /api/v1/db/data/bulk/{orgs}/{projectName}/{tableName}/ | | Data | Patch | dbTableRow | bulkUpdate | /api/v1/db/data/bulk/`{orgs}`/`{projectName}`/`{tableName}`/ |
| Data | Patch | dbTableRow | bulkUpdateAll | /api/v1/db/data/bulk/{orgs}/{projectName}/{tableName}/all | | Data | Patch | dbTableRow | bulkUpdateAll | /api/v1/db/data/bulk/`{orgs}`/`{projectName}`/`{tableName}`/all |
| Data | Delete| dbTableRow | bulkDeleteAll | /api/v1/db/data/bulk/{orgs}/{projectName}/{tableName}/all | | Data | Delete| dbTableRow | bulkDeleteAll | /api/v1/db/data/bulk/`{orgs}`/`{projectName}`/`{tableName}`/all |
| Data | Get | dbTableRow | list | /api/v1/db/data/{orgs}/{projectName}/{tableName} | | Data | Get | dbTableRow | list | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}` |
| Data | Get | dbTableRow | findOne | /api/v1/db/data/{orgs}/{projectName}/{tableName}/find-one | | Data | Get | dbTableRow | findOne | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/find-one |
| Data | Get | dbTableRow | groupBy | /api/v1/db/data/{orgs}/{projectName}/{tableName}/groupby | | Data | Get | dbTableRow | groupBy | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/groupby |
| Data | Get | dbTableRow | exist | /api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId}/exist | | Data | Get | dbTableRow | exist | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/`{rowId}`/exist |
| Data | Post | dbTableRow | create | /api/v1/db/data/{orgs}/{projectName}/{tableName} | | Data | Post | dbTableRow | create | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}` |
| Data | Get | dbTableRow | read | /api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId} | | Data | Get | dbTableRow | read | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/`{rowId}` |
| Data | Patch | dbTableRow | update | /api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId} | | Data | Patch | dbTableRow | update | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/`{rowId}` |
| Data | Delete| dbTableRow | delete | /api/v1/db/data/{orgs}/{projectName}/{tableName}/{rowId} | | Data | Delete| dbTableRow | delete | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/`{rowId}` |
| Data | Get | dbTableRow | count | /api/v1/db/data/{orgs}/{projectName}/{tableName}/count | | Data | Get | dbTableRow | count | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/count |
| Data | Get | dbTableRow | groupedDataList | /api/v1/db/data/{orgs}/{projectName}/{tableName}/group/{columnId} | | Data | Get | dbTableRow | groupedDataList | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/group/`{columnId}` |
| Data | Get | dbViewRow | list | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName} | | Data | Get | dbViewRow | list | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}` |
| Data | Get | dbViewRow | findOne | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/find-one | | Data | Get | dbViewRow | findOne | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/find-one |
| Data | Get | dbViewRow | groupBy | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/groupby | | Data | Get | dbViewRow | groupBy | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/groupby |
| Data | Get | dbViewRow | exist | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId}/exist | | Data | Get | dbViewRow | exist | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/`{rowId}`/exist |
| Data | Post | dbViewRow | create | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName} | | Data | Post | dbViewRow | create | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}` |
| Data | Get | dbViewRow | read | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId} | | Data | Get | dbViewRow | read | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/`{rowId}` |
| Data | Patch | dbViewRow | update | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId} | | Data | Patch | dbViewRow | update | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/`{rowId}` |
| Data | Delete| dbViewRow | delete | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/{rowId} | | Data | Delete| dbViewRow | delete | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/`{rowId}` |
| Data | Get | dbViewRow | count | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/count | | Data | Get | dbViewRow | count | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/count |
| Data | Get | dbViewRow | groupedDataList | /api/v1/db/data/{orgs}/{projectName}/{tableName}/views/{viewName}/group/{columnId} | | Data | Get | dbViewRow | groupedDataList | /api/v1/db/data/`{orgs}`/`{projectName}`/`{tableName}`/views/`{viewName}`/group/`{columnId}` |
### Meta APIs ### Meta APIs
| Category | Method | Tag | Function Name | Path | | Category | Method | Tag | Function Name | Path |
|---|---|---|---|---| |---|---|---|---|---|
| Meta | Get | apiToken | list | /api/v1/db/meta/projects/{projectId}/api-tokens | | Meta | Get | apiToken | list | /api/v1/db/meta/projects/`{projectId}`/api-tokens |
| Meta | Post | apiToken | create | /api/v1/db/meta/projects/{projectId}/api-tokens | | Meta | Post | apiToken | create | /api/v1/db/meta/projects/`{projectId}`/api-tokens |
| Meta | Delete| apiToken | delete | /api/v1/db/meta/projects/{projectId}/api-tokens/{token} | | Meta | Delete| apiToken | delete | /api/v1/db/meta/projects/`{projectId}`/api-tokens/`{token}` |
| Meta | Get | auth | projectUserList | /api/v1/db/meta/projects/{projectId}/users | | Meta | Get | auth | projectUserList | /api/v1/db/meta/projects/`{projectId}`/users |
| Meta | Post | auth | projectUserAdd | /api/v1/db/meta/projects/{projectId}/users | | Meta | Post | auth | projectUserAdd | /api/v1/db/meta/projects/`{projectId}`/users |
| Meta | Patch | auth | projectUserUpdate | /api/v1/db/meta/projects/{projectId}/users/{userId} | | Meta | Patch | auth | projectUserUpdate | /api/v1/db/meta/projects/`{projectId}`/users/`{userId}` |
| Meta | Delete| auth | projectUserRemove | /api/v1/db/meta/projects/{projectId}/users/{userId} | | Meta | Delete| auth | projectUserRemove | /api/v1/db/meta/projects/`{projectId}`/users/`{userId}` |
| Meta | Post | auth | projectUserResendInvite | /api/v1/db/meta/projects/{projectId}/users/{userId}/resend-invite | | Meta | Post | auth | projectUserResendInvite | /api/v1/db/meta/projects/`{projectId}`/users/`{userId}`/resend-invite |
| Meta | Post | dbTable | create | /api/v1/db/meta/projects/{projectId}/tables | | Meta | Post | dbTable | create | /api/v1/db/meta/projects/`{projectId}`/tables |
| Meta | Get | dbTable | list | /api/v1/db/meta/projects/{projectId}/tables | | Meta | Get | dbTable | list | /api/v1/db/meta/projects/`{projectId}`/tables |
| Meta | Post | dbTableColumn | create | /api/v1/db/meta/tables/{tableId}/columns | | Meta | Post | dbTableColumn | create | /api/v1/db/meta/tables/`{tableId}`/columns |
| Meta | Patch | dbTableColumn | update | /api/v1/db/meta/tables/{tableId}/columns/{columnId} | | Meta | Patch | dbTableColumn | update | /api/v1/db/meta/tables/`{tableId}`/columns/`{columnId}` |
| Meta | Delete| dbTableColumn | delete | /api/v1/db/meta/tables/{tableId}/columns/{columnId} | | Meta | Delete| dbTableColumn | delete | /api/v1/db/meta/tables/`{tableId}`/columns/`{columnId}` |
| Meta | Post | dbTableColumn | primaryColumnSet | /api/v1/db/meta/tables/{tableId}/columns/{columnId}/primary | | Meta | Post | dbTableColumn | primaryColumnSet | /api/v1/db/meta/tables/`{tableId}`/columns/`{columnId}`/primary |
| Meta | Get | dbTableFilter | get | /api/v1/db/meta/filters/{filterId} | | Meta | Get | dbTableFilter | get | /api/v1/db/meta/filters/`{filterId}` |
| Meta | Patch | dbTableFilter | update | /api/v1/db/meta/filters/{filterId} | | Meta | Patch | dbTableFilter | update | /api/v1/db/meta/filters/`{filterId}` |
| Meta | Delete| dbTableFilter | delete | /api/v1/db/meta/filters/{filterId} | | Meta | Delete| dbTableFilter | delete | /api/v1/db/meta/filters/`{filterId}` |
| Meta | Get | dbTableFilter | read | /api/v1/db/meta/views/{viewId}/filters | | Meta | Get | dbTableFilter | read | /api/v1/db/meta/views/`{viewId}`/filters |
| Meta | Post | dbTableFilter | create | /api/v1/db/meta/views/{viewId}/filters | | Meta | Post | dbTableFilter | create | /api/v1/db/meta/views/`{viewId}`/filters |
| Meta | Get | dbTableFilter | get | /api/v1/db/meta/filters/{filterId} | | Meta | Get | dbTableFilter | get | /api/v1/db/meta/filters/`{filterId}` |
| Meta | Patch | dbTableFilter | update | /api/v1/db/meta/filters/{filterId} | | Meta | Patch | dbTableFilter | update | /api/v1/db/meta/filters/`{filterId}` |
| Meta | Delete| dbTableFilter | delete | /api/v1/db/meta/filters/{filterId} | | Meta | Delete| dbTableFilter | delete | /api/v1/db/meta/filters/`{filterId}` |
| Meta | Get | dbTableFilter | childrenRead | /api/v1/db/meta/filters/{filterGroupId}/children | | Meta | Get | dbTableFilter | childrenRead | /api/v1/db/meta/filters/`{filterGroupId}`/children |
| Meta | Get | dbTableSort | list | /api/v1/db/meta/views/{viewId}/sorts | | Meta | Get | dbTableSort | list | /api/v1/db/meta/views/`{viewId}`/sorts |
| Meta | Post | dbTableSort | create | /api/v1/db/meta/views/{viewId}/sorts | | Meta | Post | dbTableSort | create | /api/v1/db/meta/views/`{viewId}`/sorts |
| Meta | Get | dbTableSort | read | /api/v1/db/meta/sorts/{sortId} | | Meta | Get | dbTableSort | read | /api/v1/db/meta/sorts/`{sortId}` |
| Meta | Patch | dbTableSort | update | /api/v1/db/meta/sorts/{sortId} | | Meta | Patch | dbTableSort | update | /api/v1/db/meta/sorts/`{sortId}` |
| Meta | Delete| dbTableSort | delete | /api/v1/db/meta/sorts/{sortId}/api/v1/db | | Meta | Delete| dbTableSort | delete | /api/v1/db/meta/sorts/`{sortId}`/api/v1/db |
| Meta | Patch | dbTableWebhook | update | /api/v1/db/meta/hooks/{hookId} | | Meta | Patch | dbTableWebhook | update | /api/v1/db/meta/hooks/`{hookId}` |
| Meta | Delete| dbTableWebhook | delete | /api/v1/db/meta/hooks/{hookId} | | Meta | Delete| dbTableWebhook | delete | /api/v1/db/meta/hooks/`{hookId}` |
| Meta | Get | dbTableWebhook | list | /api/v1/db/meta/tables/{tableId}/hooks | | Meta | Get | dbTableWebhook | list | /api/v1/db/meta/tables/`{tableId}`/hooks |
| Meta | Post | dbTableWebhook | create | /api/v1/db/meta/tables/{tableId}/hooks | | Meta | Post | dbTableWebhook | create | /api/v1/db/meta/tables/`{tableId}`/hooks |
| Meta | Post | dbTableWebhook | test | /api/v1/db/meta/tables/{tableId}/hooks/test | | Meta | Post | dbTableWebhook | test | /api/v1/db/meta/tables/`{tableId}`/hooks/test |
| Meta | Get | dbTableWebhook | samplePayloadGet | /api/v1/db/meta/tables/{tableId}/hooks/samplePayload/{operation} | | Meta | Get | dbTableWebhook | samplePayloadGet | /api/v1/db/meta/tables/`{tableId}`/hooks/samplePayload/`{operation}` |
| Meta | Get | dbTableWebhookFilter | read | /api/v1/db/meta/hooks/{hookId}/filters | | Meta | Get | dbTableWebhookFilter | read | /api/v1/db/meta/hooks/`{hookId}`/filters |
| Meta | Post | dbTableWebhookFilter | create | /api/v1/db/meta/hooks/{hookId}/filters | | Meta | Post | dbTableWebhookFilter | create | /api/v1/db/meta/hooks/`{hookId}`/filters |
| Meta | Get | dbView | list | /api/v1/db/meta/tables/{tableId}/views | | Meta | Get | dbView | list | /api/v1/db/meta/tables/`{tableId}`/views |
| Meta | Get | dbView | read | /api/v1/db/meta/tables/{tableId} | | Meta | Get | dbView | read | /api/v1/db/meta/tables/`{tableId}` |
| Meta | Patch | dbView | update | /api/v1/db/meta/tables/{tableId} | | Meta | Patch | dbView | update | /api/v1/db/meta/tables/`{tableId}` |
| Meta | Delete| dbView | delete | /api/v1/db/meta/tables/{tableId} | | Meta | Delete| dbView | delete | /api/v1/db/meta/tables/`{tableId}` |
| Meta | Post | dbView | reorder | /api/v1/db/meta/tables/{tableId}/reorder | | Meta | Post | dbView | reorder | /api/v1/db/meta/tables/`{tableId}`/reorder |
| Meta | Post | dbView | formCreate | /api/v1/db/meta/tables/{tableId}/forms | | Meta | Post | dbView | formCreate | /api/v1/db/meta/tables/`{tableId}`/forms |
| Meta | Patch | dbView | formUpdate | /api/v1/db/meta/forms/{formViewId} | | Meta | Patch | dbView | formUpdate | /api/v1/db/meta/forms/`{formViewId}` |
| Meta | Get | dbView | formRead | /api/v1/db/meta/forms/{formViewId} | | Meta | Get | dbView | formRead | /api/v1/db/meta/forms/`{formViewId}` |
| Meta | Patch | dbView | formColumnUpdate | /api/v1/db/meta/form-columns/{formViewColumnId} | | Meta | Patch | dbView | formColumnUpdate | /api/v1/db/meta/form-columns/`{formViewColumnId}` |
| Meta | Post | dbView | galleryCreate | /api/v1/db/meta/tables/{tableId}/galleries | | Meta | Post | dbView | galleryCreate | /api/v1/db/meta/tables/`{tableId}`/galleries |
| Meta | Patch | dbView | galleryUpdate | /api/v1/db/meta/galleries/{galleryViewId} | | Meta | Patch | dbView | galleryUpdate | /api/v1/db/meta/galleries/`{galleryViewId}` |
| Meta | Get | dbView | galleryRead | /api/v1/db/meta/galleries/{galleryViewId} | | Meta | Get | dbView | galleryRead | /api/v1/db/meta/galleries/`{galleryViewId}` |
| Meta | Post | dbView | kanbanCreate | /api/v1/db/meta/tables/{tableId}/kanbans | | Meta | Post | dbView | kanbanCreate | /api/v1/db/meta/tables/`{tableId}`/kanbans |
| Meta | Patch | dbView | kanbanUpdate | /api/v1/db/meta/kanban/{kanbanViewId} | | Meta | Patch | dbView | kanbanUpdate | /api/v1/db/meta/kanban/`{kanbanViewId}` |
| Meta | Get | dbView | kanbanRead | /api/v1/db/meta/kanbans/{kanbanViewId} | | Meta | Get | dbView | kanbanRead | /api/v1/db/meta/kanbans/`{kanbanViewId}` |
| Meta | Post | dbView | mapCreate | /api/v1/db/meta/tables/{tableId}/maps | | Meta | Post | dbView | mapCreate | /api/v1/db/meta/tables/`{tableId}`/maps |
| Meta | Patch | dbView | mapUpdate | /api/v1/db/meta/maps/{mapViewId} | | Meta | Patch | dbView | mapUpdate | /api/v1/db/meta/maps/`{mapViewId}` |
| Meta | Get | dbView | mapRead | /api/v1/db/meta/maps/{mapViewId} | | Meta | Get | dbView | mapRead | /api/v1/db/meta/maps/`{mapViewId}` |
| Meta | Post | dbView | gridCreate | /api/v1/db/meta/tables/{tableId}/grids | | Meta | Post | dbView | gridCreate | /api/v1/db/meta/tables/`{tableId}`/grids |
| Meta | Get | dbView | gridColumnsList | /api/v1/db/meta/grids/{gridId}/grid-columns | | Meta | Get | dbView | gridColumnsList | /api/v1/db/meta/grids/`{gridId}`/grid-columns |
| Meta | Patch | dbView | gridColumnUpdate | /api/v1/db/meta/grid-columns/{columnId} | | Meta | Patch | dbView | gridColumnUpdate | /api/v1/db/meta/grid-columns/`{columnId}` |
| Meta | Patch | dbView | update | /api/v1/db/meta/views/{viewId} | | Meta | Patch | dbView | update | /api/v1/db/meta/views/`{viewId}` |
| Meta | Delete| dbView | delete | /api/v1/db/meta/views/{viewId} | | Meta | Delete| dbView | delete | /api/v1/db/meta/views/`{viewId}` |
| Meta | Post | dbView | showAllColumn | /api/v1/db/meta/views/{viewId}/show-all | | Meta | Post | dbView | showAllColumn | /api/v1/db/meta/views/`{viewId}`/show-all |
| Meta | Post | dbView | hideAllColumn | /api/v1/db/meta/views/{viewId}/hide-all | | Meta | Post | dbView | hideAllColumn | /api/v1/db/meta/views/`{viewId}`/hide-all |
| Meta | Get | dbViewColumn | list | /api/v1/db/meta/views/{viewId}/columns | | Meta | Get | dbViewColumn | list | /api/v1/db/meta/views/`{viewId}`/columns |
| Meta | Post | dbViewColumn | create | /api/v1/db/meta/views/{viewId}/columns | | Meta | Post | dbViewColumn | create | /api/v1/db/meta/views/`{viewId}`/columns |
| Meta | Patch | dbViewColumn | update | /api/v1/db/meta/views/{viewId}/columns/{columnId} | | Meta | Patch | dbViewColumn | update | /api/v1/db/meta/views/`{viewId}`/columns/`{columnId}` |
| Meta | Get | dbViewShare | list | /api/v1/db/meta/views/{viewId}/share | | Meta | Get | dbViewShare | list | /api/v1/db/meta/views/`{viewId}`/share |
| Meta | Post | dbViewShare | create | /api/v1/db/meta/views/{viewId}/share | | Meta | Post | dbViewShare | create | /api/v1/db/meta/views/`{viewId}`/share |
| Meta | Patch | dbViewShare | update | /api/v1/db/meta/views/{viewId}/share | | Meta | Patch | dbViewShare | update | /api/v1/db/meta/views/`{viewId}`/share |
| Meta | Delete| dbViewShare | delete | /api/v1/db/meta/views/{viewId}/share | | Meta | Delete| dbViewShare | delete | /api/v1/db/meta/views/`{viewId}`/share |
| Meta | Get | plugin | list | /api/v1/db/meta/plugins | | Meta | Get | plugin | list | /api/v1/db/meta/plugins |
| Meta | Get | plugin | status | /api/v1/db/meta/plugins/{pluginTitle}/status | | Meta | Get | plugin | status | /api/v1/db/meta/plugins/`{pluginTitle}`/status |
| Meta | Post | plugin | test | /api/v1/db/meta/plugins/test | | Meta | Post | plugin | test | /api/v1/db/meta/plugins/test |
| Meta | PATCH | plugin | update | /api/v1/db/meta/plugins/{pluginId} | | Meta | PATCH | plugin | update | /api/v1/db/meta/plugins/`{pluginId}` |
| Meta | Get | plugin | read | /api/v1/db/meta/plugins/{pluginId} | | Meta | Get | plugin | read | /api/v1/db/meta/plugins/`{pluginId}` |
| Meta | Get | project | metaGet | /api/v1/db/meta/projects/{projectId}/info | | Meta | Get | project | metaGet | /api/v1/db/meta/projects/`{projectId}`/info |
| Meta | Get | project | modelVisibilityList | /api/v1/db/meta/projects/{projectId}/visibility-rules | | Meta | Get | project | modelVisibilityList | /api/v1/db/meta/projects/`{projectId}`/visibility-rules |
| Meta | Post | project | modelVisibilitySet | /api/v1/db/meta/projects/{projectId}/visibility-rules | | Meta | Post | project | modelVisibilitySet | /api/v1/db/meta/projects/`{projectId}`/visibility-rules |
| Meta | Get | project | list | /api/v1/db/meta/projects | | Meta | Get | project | list | /api/v1/db/meta/projects |
| Meta | Post | project | create | /api/v1/db/meta/projects | | Meta | Post | project | create | /api/v1/db/meta/projects |
| Meta | Get | project | read | /api/v1/db/meta/projects/{projectId} | | Meta | Get | project | read | /api/v1/db/meta/projects/`{projectId}` |
| Meta | Delete| project | delete | /api/v1/db/meta/projects/{projectId} | | Meta | Delete| project | delete | /api/v1/db/meta/projects/`{projectId}` |
| Meta | Get | project | auditList | /api/v1/db/meta/projects/{projectId}/audits | | Meta | Get | project | auditList | /api/v1/db/meta/projects/`{projectId}`/audits |
| Meta | Get | project | metaDiffGet | /api/v1/db/meta/projects/{projectId}/meta-diff | | Meta | Get | project | metaDiffGet | /api/v1/db/meta/projects/`{projectId}`/meta-diff |
| Meta | Post | project | metaDiffSync | /api/v1/db/meta/projects/{projectId}/meta-diff | | Meta | Post | project | metaDiffSync | /api/v1/db/meta/projects/`{projectId}`/meta-diff |
| Meta | Get | project | sharedBaseGet | /api/v1/db/meta/projects/{projectId}/shared | | Meta | Get | project | sharedBaseGet | /api/v1/db/meta/projects/`{projectId}`/shared |
| Meta | Delete| project | sharedBaseDisable | /api/v1/db/meta/projects/{projectId}/shared | | Meta | Delete| project | sharedBaseDisable | /api/v1/db/meta/projects/`{projectId}`/shared |
| Meta | Post | project | sharedBaseCreate | /api/v1/db/meta/projects/{projectId}/shared | | Meta | Post | project | sharedBaseCreate | /api/v1/db/meta/projects/`{projectId}`/shared |
| Meta | Patch | project | sharedBaseUpdate | /api/v1/db/meta/projects/{projectId}/shared | | Meta | Patch | project | sharedBaseUpdate | /api/v1/db/meta/projects/`{projectId}`/shared |
| Meta | Post | storage | upload | /api/v1/db/storage/upload | | Meta | Post | storage | upload | /api/v1/db/storage/upload |
| Meta | Post | storage | uploadByUrl | /api/v1/db/storage/upload-by-url | | Meta | Post | storage | uploadByUrl | /api/v1/db/storage/upload-by-url |
| Meta | Get | utils | commentList | /api/v1/db/meta/audits/comments | | Meta | Get | utils | commentList | /api/v1/db/meta/audits/comments |
@ -180,11 +180,11 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| Meta | Get | utils | aggregatedMetaInfo | /api/v1/aggregated-meta-info | | Meta | Get | utils | aggregatedMetaInfo | /api/v1/aggregated-meta-info |
| Meta | Get | orgUsers | list | /api/v1/users | | Meta | Get | orgUsers | list | /api/v1/users |
| Meta | Post | orgUsers | add | /api/v1/users | | Meta | Post | orgUsers | add | /api/v1/users |
| Meta | Patch | orgUsers | update | /api/v1/users/{userId} | | Meta | Patch | orgUsers | update | /api/v1/users/`{userId}` |
| Meta | Delete | orgUsers | delete | /api/v1/users/{userId} | | Meta | Delete | orgUsers | delete | /api/v1/users/`{userId}` |
| Meta | Get | orgTokens | list | /api/v1/tokens | | Meta | Get | orgTokens | list | /api/v1/tokens |
| Meta | Post | orgTokens | create | /api/v1/tokens | | Meta | Post | orgTokens | create | /api/v1/tokens |
| Meta | Delete | orgTokens | delete | /api/v1/tokens/{token} | | Meta | Delete | orgTokens | delete | /api/v1/tokens/`{token}` |
| Meta | Get | orgAppSettings | get | /api/v1/app-settings | | Meta | Get | orgAppSettings | get | /api/v1/app-settings |
| Meta | Post | orgAppSettings | set | /api/v1/app-settings | | Meta | Post | orgAppSettings | set | /api/v1/app-settings |

2
packages/noco-docs/versioned_docs/version-0.109.7/040.developer-resources/030.sdk.md vendored

@ -61,7 +61,7 @@ For Tag and FunctionName, please check out the API table <a href="/0.109.7/devel
::: :::
#### Example: Calling API - /api/v1/db/meta/projects/{projectId}/tables #### Example: Calling API - /api/v1/db/meta/projects/`{projectId}`/tables
```js ```js
await api.dbTable.create(params) await api.dbTable.create(params)

18
packages/noco-docs/versioned_docs/version-0.109.7/040.developer-resources/040.webhooks.md vendored

@ -84,17 +84,17 @@ The current row data and other details will be available in the hooks payload so
For a table with column names (id, title, created_at, updated_at). For a table with column names (id, title, created_at, updated_at).
For INSERT/ UPDATE based triggers, use following handlebars to access corresponding **data** fields. For INSERT/ UPDATE based triggers, use following handlebars to access corresponding **data** fields.
- {{ **data**.id }} - `{{ **data**.id }}`
- {{ **data**.title }} - `{{ **data**.title }}`
- {{ **data**.created_at }} - `{{ **data**.created_at }}`
- {{ **data**.updated_at }} - `{{ **data**.updated_at }}`
Note that, for Update trigger - all the fields in the ROW will be accessible, not just the field updated. Note that, for Update trigger - all the fields in the ROW will be accessible, not just the field updated.
For DELETE based triggers, **only** {{ data.id }} is accessible representing ID of the column deleted. For DELETE based triggers, **only** `{{ data.id }}` is accessible representing ID of the column deleted.
### JSON format ### JSON format
Use {{ json data }} to dump complete data & user information available in JSON format Use `{{ json data }}` to dump complete data & user information available in JSON format
### Additional references: ### Additional references:
@ -148,7 +148,7 @@ Detailed procedure for discord webhook described [here](https://support.discord.
- **Select Discord Channels**: Select from the drop down list, channel name configured in Step (2). Please click on 'Reload' if drop down list is empty. - **Select Discord Channels**: Select from the drop down list, channel name configured in Step (2). Please click on 'Reload' if drop down list is empty.
- **Body**: Message to be posted over Discord channel, via webhooks on trigger of configured event. - **Body**: Message to be posted over Discord channel, via webhooks on trigger of configured event.
- Body can contain plain text & - Body can contain plain text &
- Handlebars {{ }} - Handlebars `{{ }}`
## Slack ## Slack
@ -199,7 +199,7 @@ Detailed procedure for discord webhook described [here](https://support.discord.
### 3. Configure ### 3. Configure
- Open project and choose a table. - Open project and choose a table.
- Click 'More' > 'Webhooks'. - Click 'More' {">"} 'Webhooks'.
- Click 'Create webhook' - Click 'Create webhook'
- Configure webhook - Configure webhook
- **Title**: Name of your choice to identify this Webhook. - **Title**: Name of your choice to identify this Webhook.
@ -212,7 +212,7 @@ Detailed procedure for discord webhook described [here](https://support.discord.
- **Select Teams Channels**: Select from the drop down list, channel name configured in Step (2). Please click on 'Reload' if drop down list is empty. - **Select Teams Channels**: Select from the drop down list, channel name configured in Step (2). Please click on 'Reload' if drop down list is empty.
- **Body**: Message to be posted over Teams channel, via webhooks on trigger of configured event. - **Body**: Message to be posted over Teams channel, via webhooks on trigger of configured event.
- Body can contain plain text & - Body can contain plain text &
- Handlebars {{ }} - Handlebars `{{ }}`
## Webhook V2 ## Webhook V2

12
packages/nocodb-sdk/package.json

@ -1,6 +1,6 @@
{ {
"name": "nocodb-sdk", "name": "nocodb-sdk",
"version": "0.202.5", "version": "0.202.7",
"description": "NocoDB SDK", "description": "NocoDB SDK",
"main": "build/main/index.js", "main": "build/main/index.js",
"typings": "build/main/index.d.ts", "typings": "build/main/index.d.ts",
@ -39,12 +39,12 @@
}, },
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
"jsep": "^1.3.6" "jsep": "^1.3.8"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0", "@typescript-eslint/parser": "^6.1.0",
"cspell": "^4.1.0", "cspell": "^4.2.8",
"eslint": "^8.33.0", "eslint": "^8.33.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-eslint-comments": "^3.2.0",
@ -53,9 +53,9 @@
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"rimraf": "^5.0.1", "rimraf": "^5.0.5",
"tsc-alias": "^1.8.7", "tsc-alias": "^1.8.8",
"typescript": "^4.7.4" "typescript": "^5.2.2"
}, },
"files": [ "files": [
"build/main", "build/main",

123
packages/nocodb/package.json

@ -1,6 +1,6 @@
{ {
"name": "nocodb", "name": "nocodb",
"version": "0.202.5", "version": "0.202.7",
"description": "NocoDB Backend", "description": "NocoDB Backend",
"main": "dist/bundle.js", "main": "dist/bundle.js",
"author": { "author": {
@ -47,28 +47,29 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-kafka": "^3.410.0", "@aws-sdk/client-kafka": "^3.410.0",
"@aws-sdk/client-s3": "^3.423.0", "@aws-sdk/client-s3": "^3.423.0",
"@aws-sdk/lib-storage": "^3.451.0",
"@aws-sdk/s3-request-presigner": "^3.423.0", "@aws-sdk/s3-request-presigner": "^3.423.0",
"@google-cloud/storage": "^7.1.0", "@google-cloud/storage": "^7.1.0",
"@graphql-tools/merge": "^6.0.12", "@graphql-tools/merge": "^6.2.17",
"@jm18457/kafkajs-msk-iam-authentication-mechanism": "^3.1.2", "@jm18457/kafkajs-msk-iam-authentication-mechanism": "^3.1.2",
"@nestjs/bull": "^10.0.1", "@nestjs/bull": "^10.0.1",
"@nestjs/common": "^10.2.1", "@nestjs/common": "^10.2.9",
"@nestjs/config": "^3.0.0", "@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.1", "@nestjs/core": "^10.2.9",
"@nestjs/event-emitter": "^2.0.2", "@nestjs/event-emitter": "^2.0.3",
"@nestjs/jwt": "^10.1.0", "@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "^2.0.2", "@nestjs/mapped-types": "^2.0.4",
"@nestjs/passport": "^10.0.1", "@nestjs/passport": "^10.0.2",
"@nestjs/platform-express": "^10.2.1", "@nestjs/platform-express": "^10.2.9",
"@nestjs/platform-socket.io": "^10.2.1", "@nestjs/platform-socket.io": "^10.2.9",
"@nestjs/serve-static": "^4.0.0", "@nestjs/serve-static": "^4.0.0",
"@nestjs/throttler": "^4.2.1", "@nestjs/throttler": "^4.2.1",
"@nestjs/websockets": "^10.2.1", "@nestjs/websockets": "^10.2.9",
"@ntegral/nestjs-sentry": "^4.0.0", "@ntegral/nestjs-sentry": "^4.0.0",
"@sentry/node": "^6.3.5", "@sentry/node": "^6.19.7",
"@techpass/passport-openidconnect": "^0.3.3", "@techpass/passport-openidconnect": "^0.3.3",
"@types/chai": "^4.2.12", "@types/chai": "^4.3.10",
"airtable": "^0.12.1", "airtable": "^0.12.2",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"ajv-formats": "^2.1.1", "ajv-formats": "^2.1.1",
"archiver": "^5.0.2", "archiver": "^5.0.2",
@ -77,74 +78,74 @@
"aws-sdk": "^2.1455.0", "aws-sdk": "^2.1455.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"body-parser": "^1.20.1", "body-parser": "^1.20.2",
"boxen": "^5.1.0", "boxen": "^5.1.2",
"bull": "^4.11.3", "bull": "^4.11.5",
"bullmq": "^1.81.1", "bullmq": "^1.91.1",
"clear": "^0.1.0", "clear": "^0.1.0",
"clickhouse": "^2.6.0", "clickhouse": "^2.6.0",
"clickhouse-migrations": "^0.1.13", "clickhouse-migrations": "^0.1.13",
"colors": "^1.4.0", "colors": "^1.4.0",
"compare-versions": "^6.0.0-rc.1", "compare-versions": "^6.1.0",
"cookie-parser": "^1.4.5", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron": "^1.8.2", "cron": "^1.8.2",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"dataloader": "^2.0.0", "dataloader": "^2.0.0",
"dayjs": "^1.11.9", "dayjs": "^1.11.10",
"debug": "^4.3.4", "debug": "^4.3.4",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"ejs": "^3.1.3", "ejs": "^3.1.3",
"emittery": "^0.7.1", "emittery": "^0.7.2",
"express": "^4.18.1", "express": "^4.18.2",
"extract-zip": "^2.0.1", "extract-zip": "^2.0.1",
"fast-levenshtein": "^2.0.6", "fast-levenshtein": "^2.0.6",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"glob": "^7.1.6", "glob": "^7.2.3",
"graphql": "^15.3.0", "graphql": "^15.3.0",
"graphql-depth-limit": "^1.1.0", "graphql-depth-limit": "^1.1.0",
"graphql-type-json": "^0.3.2", "graphql-type-json": "^0.3.2",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"html-to-json-parser": "^1.1.0", "html-to-json-parser": "^1.1.0",
"import-fresh": "^3.2.1", "import-fresh": "^3.3.0",
"inflection": "^1.12.0", "inflection": "^1.12.0",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"ioredis-mock": "^8.8.3", "ioredis-mock": "^8.8.3",
"is-docker": "^2.2.1", "is-docker": "^2.2.1",
"isomorphic-dompurify": "^1.8.0", "isomorphic-dompurify": "^1.8.0",
"jsep": "^1.3.6", "jsep": "^1.3.8",
"json5": "^2.2.3", "json5": "^2.2.3",
"jsonfile": "^6.1.0", "jsonfile": "^6.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.2",
"kafkajs": "^2.2.4", "kafkajs": "^2.2.4",
"knex": "2.4.2", "knex": "2.4.2",
"list-github-dir-content": "^3.0.0", "list-github-dir-content": "^3.0.0",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"lru-cache": "^6.0.0", "lru-cache": "^6.0.0",
"mailersend": "^1.1.0", "mailersend": "^1.5.0",
"marked": "^4.3.0", "marked": "^4.3.0",
"minio": "^7.0.18", "minio": "^7.1.3",
"mkdirp": "^2.1.3", "mkdirp": "^2.1.6",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"mssql": "^10.0.0", "mssql": "^10.0.1",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.2.0", "mysql2": "^3.6.3",
"nanoid": "^3.1.20", "nanoid": "^3.1.20",
"nc-help": "0.3.1", "nc-help": "0.3.1",
"nc-lib-gui": "0.202.5", "nc-lib-gui": "0.202.7",
"nc-plugin": "^0.1.3", "nc-plugin": "^0.1.3",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"nestjs-kafka": "^1.0.6", "nestjs-kafka": "^1.0.6",
"nestjs-throttler-storage-redis": "^0.3.0", "nestjs-throttler-storage-redis": "^0.3.3",
"nocodb-sdk": "workspace:^", "nocodb-sdk": "workspace:^",
"nodemailer": "^6.4.10", "nodemailer": "^6.4.10",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"object-sizeof": "^2.6.1", "object-sizeof": "^2.6.3",
"os-locale": "^6.0.2", "os-locale": "^6.0.2",
"p-queue": "^6.6.2", "p-queue": "^6.6.2",
"papaparse": "^5.4.0", "papaparse": "^5.4.1",
"parse-database-url": "^0.3.0", "parse-database-url": "^0.3.0",
"passport": "^0.4.1", "passport": "^0.6.0",
"passport-auth-token": "^1.0.1", "passport-auth-token": "^1.0.1",
"passport-custom": "^1.1.1", "passport-custom": "^1.1.1",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
@ -160,35 +161,35 @@
"rmdir": "^1.2.0", "rmdir": "^1.2.0",
"rxjs": "^7.2.0", "rxjs": "^7.2.0",
"slash": "^3.0.0", "slash": "^3.0.0",
"slug": "^8.2.2", "slug": "^8.2.3",
"socket.io": "^4.4.1", "socket.io": "^4.4.1",
"sql-query-identifier": "^2.5.0", "sql-query-identifier": "^2.5.0",
"sqlite3": "^5.1.6", "sqlite3": "^5.1.6",
"tedious": "^16.4.0", "tedious": "^16.6.0",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
"twilio": "^3.55.1", "twilio": "^3.55.1",
"unique-names-generator": "^4.3.1", "unique-names-generator": "^4.7.1",
"uuid": "^9.0.0", "uuid": "^9.0.1",
"validator": "^13.1.1", "validator": "^13.1.17",
"xc-core-ts": "^0.1.0", "xc-core-ts": "^0.1.0",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^10.1.10", "@nestjs/cli": "^10.2.1",
"@nestjs/schematics": "^10.0.1", "@nestjs/schematics": "^10.0.3",
"@nestjs/testing": "^10.1.0", "@nestjs/testing": "^10.2.9",
"@nestjsplus/dyn-schematics": "^1.0.12", "@nestjsplus/dyn-schematics": "^1.0.12",
"@types/ejs": "^3.1.2", "@types/ejs": "^3.1.5",
"@types/express": "^4.17.17", "@types/express": "^4.17.21",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.8",
"@types/mocha": "^10.0.1", "@types/mocha": "^10.0.4",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.10",
"@types/node": "20.3.1", "@types/node": "20.3.3",
"@types/passport-google-oauth20": "^2.0.11", "@types/passport-google-oauth20": "^2.0.14",
"@types/passport-jwt": "^3.0.8", "@types/passport-jwt": "^3.0.13",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.16",
"@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.7.0", "@typescript-eslint/parser": "^6.11.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@ -201,14 +202,14 @@
"jest": "29.5.0", "jest": "29.5.0",
"mocha": "^10.1.0", "mocha": "^10.1.0",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"prettier": "^2.7.1", "prettier": "^2.8.8",
"source-map-support": "^0.5.20", "source-map-support": "^0.5.21",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"ts-jest": "29.0.5", "ts-jest": "29.0.5",
"ts-loader": "^9.2.3", "ts-loader": "^9.2.9",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3", "typescript": "^5.2.2",
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4"
}, },
"jest": { "jest": {

39
packages/nocodb/src/controllers/api-docs/api-docs.controller.ts

@ -18,10 +18,7 @@ import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
export class ApiDocsController { export class ApiDocsController {
constructor(private readonly apiDocsService: ApiDocsService) {} constructor(private readonly apiDocsService: ApiDocsService) {}
@Get([ @Get(['/api/v1/db/meta/projects/:baseId/swagger.json'])
'/api/v1/db/meta/projects/:baseId/swagger.json',
'/api/v2/meta/bases/:baseId/swagger.json',
])
@UseGuards(MetaApiLimiterGuard, GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
@Acl('swaggerJson') @Acl('swaggerJson')
async swaggerJson(@Param('baseId') baseId: string, @Request() req) { async swaggerJson(@Param('baseId') baseId: string, @Request() req) {
@ -33,21 +30,39 @@ export class ApiDocsController {
return swagger; return swagger;
} }
@Get([ @Get(['/api/v2/meta/bases/:baseId/swagger.json'])
'/api/v2/meta/bases/:baseId/swagger', @UseGuards(MetaApiLimiterGuard, GlobalGuard)
'/api/v1/db/meta/projects/:baseId/swagger', @Acl('swaggerJson')
]) async swaggerJsonV2(@Param('baseId') baseId: string, @Request() req) {
const swagger = await this.apiDocsService.swaggerJsonV2({
baseId: baseId,
siteUrl: req.ncSiteUrl,
});
return swagger;
}
@Get(['/api/v1/db/meta/projects/:baseId/swagger'])
@UseGuards(PublicApiLimiterGuard) @UseGuards(PublicApiLimiterGuard)
swaggerHtml(@Param('baseId') baseId: string, @Response() res) { swaggerHtml(@Param('baseId') baseId: string, @Response() res) {
res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' })); res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
} }
@UseGuards(PublicApiLimiterGuard) @UseGuards(PublicApiLimiterGuard)
@Get([ @Get(['/api/v1/db/meta/projects/:baseId/redoc'])
'/api/v1/db/meta/projects/:baseId/redoc',
'/api/v2/meta/bases/:baseId/redoc',
])
redocHtml(@Param('baseId') baseId: string, @Response() res) { redocHtml(@Param('baseId') baseId: string, @Response() res) {
res.send(getRedocHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' })); res.send(getRedocHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
} }
@Get(['/api/v2/meta/bases/:baseId/swagger'])
@UseGuards(PublicApiLimiterGuard)
swaggerHtmlV2(@Param('baseId') baseId: string, @Response() res) {
res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
@UseGuards(PublicApiLimiterGuard)
@Get(['/api/v2/meta/bases/:baseId/redoc'])
redocHtmlV2(@Param('baseId') baseId: string, @Response() res) {
res.send(getRedocHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
} }

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

@ -35,7 +35,6 @@ import type {
SelectOption, SelectOption,
} from '~/models'; } from '~/models';
import type { SortType } from 'nocodb-sdk'; import type { SortType } from 'nocodb-sdk';
import generateBTLookupSelectQuery from '~/db/generateBTLookupSelectQuery';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2'; import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2'; import genRollupSelectv2 from '~/db/genRollupSelectv2';
import conditionV2 from '~/db/conditionV2'; import conditionV2 from '~/db/conditionV2';
@ -60,10 +59,13 @@ import { HANDLE_WEBHOOK } from '~/services/hook-handler.service';
import { import {
COMPARISON_OPS, COMPARISON_OPS,
COMPARISON_SUB_OPS, COMPARISON_SUB_OPS,
GROUPBY_COMPARISON_OPS,
IS_WITHIN_COMPARISON_SUB_OPS, IS_WITHIN_COMPARISON_SUB_OPS,
} from '~/utils/globals'; } from '~/utils/globals';
import { extractProps } from '~/helpers/extractProps'; import { extractProps } from '~/helpers/extractProps';
import { defaultLimitConfig } from '~/helpers/extractLimitAndOffset'; import { defaultLimitConfig } from '~/helpers/extractLimitAndOffset';
import generateLookupSelectQuery from '~/db/generateLookupSelectQuery';
import { getAliasGenerator } from '~/utils';
dayjs.extend(utc); dayjs.extend(utc);
@ -386,6 +388,7 @@ class BaseModelSqlv2 {
validateFormula: true, validateFormula: true,
}); });
} }
return data?.map((d) => { return data?.map((d) => {
d.__proto__ = proto; d.__proto__ = proto;
return d; return d;
@ -549,18 +552,32 @@ class BaseModelSqlv2 {
const selectors = []; const selectors = [];
const groupBySelectors = []; const groupBySelectors = [];
const getAlias = getAliasGenerator('__nc_gb');
await Promise.all( await Promise.all(
args.column_name.split(',').map(async (col) => { args.column_name.split(',').map(async (col) => {
const column = cols.find( let column = cols.find((c) => c.column_name === col || c.title === col);
(c) => c.column_name === col || c.title === col,
);
groupByColumns[column.id] = column;
if (!column) { if (!column) {
throw NcError.notFound('Column not found'); throw NcError.notFound('Column not found');
} }
// if qrCode or Barcode replace it with value column nd keep the alias
if ([UITypes.QrCode, UITypes.Barcode].includes(column.uidt))
column = new Column({
...(await column
.getColOptions<BarcodeColumn | QrCodeColumn>()
.then((col) => col.getValueColumn())),
title: column.title,
});
groupByColumns[column.id] = column;
switch (column.uidt) { switch (column.uidt) {
case UITypes.Attachment:
NcError.badRequest(
'Group by using attachment column is not supported',
);
break;
case UITypes.Links: case UITypes.Links:
case UITypes.Rollup: case UITypes.Rollup:
selectors.push( selectors.push(
@ -599,12 +616,14 @@ class BaseModelSqlv2 {
} }
break; break;
case UITypes.Lookup: case UITypes.Lookup:
case UITypes.LinkToAnotherRecord:
{ {
const _selectQb = await generateBTLookupSelectQuery({ const _selectQb = await generateLookupSelectQuery({
baseModelSqlv2: this, baseModelSqlv2: this,
column, column,
alias: null, alias: null,
model: this.model, model: this.model,
getAlias,
}); });
const selectQb = this.dbDriver.raw(`?? as ??`, [ const selectQb = this.dbDriver.raw(`?? as ??`, [
@ -695,6 +714,7 @@ class BaseModelSqlv2 {
qb.groupBy(...groupBySelectors); qb.groupBy(...groupBySelectors);
applyPaginate(qb, rest); applyPaginate(qb, rest);
return await this.execAndParse(qb); return await this.execAndParse(qb);
} }
@ -711,18 +731,34 @@ class BaseModelSqlv2 {
const selectors = []; const selectors = [];
const groupBySelectors = []; const groupBySelectors = [];
const getAlias = getAliasGenerator('__nc_gb');
// todo: refactor and avoid duplicate code
await this.model.getColumns().then((cols) => await this.model.getColumns().then((cols) =>
Promise.all( Promise.all(
args.column_name.split(',').map(async (col) => { args.column_name.split(',').map(async (col) => {
const column = cols.find( let column = cols.find(
(c) => c.column_name === col || c.title === col, (c) => c.column_name === col || c.title === col,
); );
if (!column) { if (!column) {
throw NcError.notFound('Column not found'); throw NcError.notFound('Column not found');
} }
// if qrCode or Barcode replace it with value column nd keep the alias
if ([UITypes.QrCode, UITypes.Barcode].includes(column.uidt))
column = new Column({
...(await column
.getColOptions<BarcodeColumn | QrCodeColumn>()
.then((col) => col.getValueColumn())),
title: column.title,
});
switch (column.uidt) { switch (column.uidt) {
case UITypes.Attachment:
NcError.badRequest(
'Group by using attachment column is not supported',
);
break;
case UITypes.Rollup: case UITypes.Rollup:
case UITypes.Links: case UITypes.Links:
selectors.push( selectors.push(
@ -764,12 +800,14 @@ class BaseModelSqlv2 {
break; break;
} }
case UITypes.Lookup: case UITypes.Lookup:
case UITypes.LinkToAnotherRecord:
{ {
const _selectQb = await generateBTLookupSelectQuery({ const _selectQb = await generateLookupSelectQuery({
baseModelSqlv2: this, baseModelSqlv2: this,
column, column,
alias: null, alias: null,
model: this.model, model: this.model,
getAlias,
}); });
const selectQb = this.dbDriver.raw(`?? as ??`, [ const selectQb = this.dbDriver.raw(`?? as ??`, [
@ -2994,7 +3032,7 @@ class BaseModelSqlv2 {
// insert one by one as fallback to get ids for sqlite and mysql // insert one by one as fallback to get ids for sqlite and mysql
if (insertOneByOneAsFallback && (this.isSqlite || this.isMySQL)) { if (insertOneByOneAsFallback && (this.isSqlite || this.isMySQL)) {
// sqlite and mysql doesnt support returning, so insert one by one and return ids // sqlite and mysql doesn't support returning, so insert one by one and return ids
response = []; response = [];
const aiPkCol = this.model.primaryKeys.find((pk) => pk.ai); const aiPkCol = this.model.primaryKeys.find((pk) => pk.ai);
@ -5156,9 +5194,7 @@ export function extractSortsObject(
else sort.fk_column_id = aliasColObjMap[s.replace(/^\+/, '')]?.id; else sort.fk_column_id = aliasColObjMap[s.replace(/^\+/, '')]?.id;
if (throwErrorIfInvalid && !sort.fk_column_id) if (throwErrorIfInvalid && !sort.fk_column_id)
NcError.unprocessableEntity( NcError.unprocessableEntity(`Invalid field: ${s.replace(/^[+-]/, '')}`);
`Invalid column '${s.replace(/^[+-]/, '')}' in sort`,
);
return new Sort(sort); return new Sort(sort);
}); });
@ -5247,7 +5283,7 @@ export function extractFilterFromXwhere(
// mark `op` and `sub_op` any for being assignable to parameter of type // mark `op` and `sub_op` any for being assignable to parameter of type
function validateFilterComparison(uidt: UITypes, op: any, sub_op?: any) { function validateFilterComparison(uidt: UITypes, op: any, sub_op?: any) {
if (!COMPARISON_OPS.includes(op)) { if (!COMPARISON_OPS.includes(op) && !GROUPBY_COMPARISON_OPS.includes(op)) {
NcError.badRequest(`${op} is not supported.`); NcError.badRequest(`${op} is not supported.`);
} }
@ -5322,7 +5358,7 @@ export function extractCondition(
validateFilterComparison(aliasColObjMap[alias].uidt, op, sub_op); validateFilterComparison(aliasColObjMap[alias].uidt, op, sub_op);
} else if (throwErrorIfInvalid) { } else if (throwErrorIfInvalid) {
NcError.unprocessableEntity(`Column '${alias}' not found.`); NcError.unprocessableEntity(`Invalid field: ${alias}`);
} }
return new Filter({ return new Filter({
@ -5366,7 +5402,7 @@ export function _wherePk(primaryKeys: Column[], id: unknown | unknown[]) {
} }
} }
return id; return where;
} }
const ids = Array.isArray(id) ? id : (id + '').split('___'); const ids = Array.isArray(id) ? id : (id + '').split('___');

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

@ -8,11 +8,14 @@ import type Column from '~/models/Column';
import type LookupColumn from '~/models/LookupColumn'; import type LookupColumn from '~/models/LookupColumn';
import type RollupColumn from '~/models/RollupColumn'; import type RollupColumn from '~/models/RollupColumn';
import type FormulaColumn from '~/models/FormulaColumn'; import type FormulaColumn from '~/models/FormulaColumn';
import type { BarcodeColumn, QrCodeColumn } from '~/models';
import { NcError } from '~/helpers/catchError'; import { NcError } from '~/helpers/catchError';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2'; import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2'; import genRollupSelectv2 from '~/db/genRollupSelectv2';
import { sanitize } from '~/helpers/sqlSanitize'; import { sanitize } from '~/helpers/sqlSanitize';
import Filter from '~/models/Filter'; import Filter from '~/models/Filter';
import generateLookupSelectQuery from '~/db/generateLookupSelectQuery';
import { getAliasGenerator } from '~/utils';
// tod: tobe fixed // tod: tobe fixed
// extend(customParseFormat); // extend(customParseFormat);
@ -112,12 +115,47 @@ const parseConditionV2 = async (
}); });
}; };
} else { } else {
// handle group by filter separately,
// `gb_eq` is equivalent to `eq` but for lookup it compares on aggregated value returns in group by api
// aggregated value will be either json array or `___` separated string
// `gb_null` is equivalent to `blank` but for lookup it compares on aggregated value is null
if (
(filter.comparison_op as any) === 'gb_eq' ||
(filter.comparison_op as any) === 'gb_null'
) {
const column = await filter.getColumn();
if (
column.uidt === UITypes.Lookup ||
column.uidt === UITypes.LinkToAnotherRecord
) {
const model = await column.getModel();
const lkQb = await generateLookupSelectQuery({
baseModelSqlv2,
alias: alias,
model,
column,
getAlias: getAliasGenerator('__gb_filter_lk'),
});
return (qb) => {
if ((filter.comparison_op as any) === 'gb_eq')
qb.where(knex.raw('?', [filter.value]), lkQb.builder);
else qb.whereNull(knex.raw(lkQb.builder).wrap('(', ')'));
};
} else {
filter.comparison_op =
(filter.comparison_op as any) === 'gb_eq' ? 'eq' : 'blank';
// if qrCode or Barcode replace it with value column
if ([UITypes.QrCode, UITypes.Barcode].includes(column.uidt))
filter.fk_column_id = await column
.getColOptions<BarcodeColumn | QrCodeColumn>()
.then((col) => col.fk_column_id);
}
}
const column = await filter.getColumn(); const column = await filter.getColumn();
if (!column) { if (!column) {
if (throwErrorIfInvalid) { if (throwErrorIfInvalid) {
NcError.unprocessableEntity( NcError.unprocessableEntity(`Invalid field: ${filter.fk_column_id}`);
`Invalid column id '${filter.fk_column_id}' in filter`,
);
} }
return; return;
} }
@ -342,7 +380,7 @@ const parseConditionV2 = async (
return (qbP: Knex.QueryBuilder) => { return (qbP: Knex.QueryBuilder) => {
if (filter.comparison_op in negatedMapping) if (filter.comparison_op in negatedMapping)
qbP.where((qb) => qbP.where((qb) =>
qbP qb
.whereNotIn(childColumn.column_name, selectQb) .whereNotIn(childColumn.column_name, selectQb)
.orWhereNull(childColumn.column_name), .orWhereNull(childColumn.column_name),
); );

163
packages/nocodb/src/db/generateBTLookupSelectQuery.ts

@ -1,163 +0,0 @@
import { RelationTypes, UITypes } from 'nocodb-sdk';
import type LookupColumn from '../models/LookupColumn';
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
import type {
Column,
FormulaColumn,
LinkToAnotherRecordColumn,
Model,
RollupColumn,
} from '~/models';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
import { NcError } from '~/helpers/catchError';
export default async function generateBTLookupSelectQuery({
column,
baseModelSqlv2,
alias,
model,
}: {
column: Column;
baseModelSqlv2: BaseModelSqlv2;
alias: string;
model: Model;
}): Promise<any> {
const knex = baseModelSqlv2.dbDriver;
const rootAlias = alias;
{
let aliasCount = 0,
selectQb;
const alias = `__nc_lk_${aliasCount++}`;
const lookup = await column.getColOptions<LookupColumn>();
{
const relationCol = await lookup.getRelationColumn();
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if not belongs to then throw error as we don't support
if (relation.type !== RelationTypes.BELONGS_TO)
NcError.badRequest('HasMany/ManyToMany lookup is not supported');
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb = knex(
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`,
).where(
`${alias}.${parentColumn.column_name}`,
knex.raw(`??`, [
`${rootAlias || baseModelSqlv2.getTnPath(childModel.table_name)}.${
childColumn.column_name
}`,
]),
);
}
let lookupColumn = await lookup.getLookupColumn();
let prevAlias = alias;
while (lookupColumn.uidt === UITypes.Lookup) {
const nestedAlias = `__nc_lk_nested_${aliasCount++}`;
const nestedLookup = await lookupColumn.getColOptions<LookupColumn>();
const relationCol = await nestedLookup.getRelationColumn();
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if any of the relation in nested lookup is
// not belongs to then throw error as we don't support
if (relation.type !== RelationTypes.BELONGS_TO)
NcError.badRequest('HasMany/ManyToMany lookup is not supported');
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb.join(
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${nestedAlias}`,
`${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}`,
);
lookupColumn = await nestedLookup.getLookupColumn();
prevAlias = nestedAlias;
}
switch (lookupColumn.uidt) {
case UITypes.Links:
case UITypes.Rollup:
{
const builder = (
await genRollupSelectv2({
baseModelSqlv2,
knex,
columnOptions:
(await lookupColumn.getColOptions()) as RollupColumn,
alias: prevAlias,
})
).builder;
selectQb.select(builder);
}
break;
case UITypes.LinkToAnotherRecord:
{
const nestedAlias = `__nc_sort${aliasCount++}`;
const relation =
await lookupColumn.getColOptions<LinkToAnotherRecordColumn>();
if (relation.type !== 'bt') return;
const colOptions =
(await column.getColOptions()) as LinkToAnotherRecordColumn;
const childColumn = await colOptions.getChildColumn();
const parentColumn = await colOptions.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb
.join(
`${baseModelSqlv2.getTnPath(
parentModel.table_name,
)} as ${nestedAlias}`,
`${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}`,
)
.select(parentModel?.displayValue?.column_name);
}
break;
case UITypes.Formula:
{
const builder = (
await formulaQueryBuilderv2(
baseModelSqlv2,
(
await column.getColOptions<FormulaColumn>()
).formula,
null,
model,
column,
)
).builder;
selectQb.select(builder);
}
break;
default:
{
selectQb.select(`${prevAlias}.${lookupColumn.column_name}`);
}
break;
}
return { builder: selectQb };
}
}

399
packages/nocodb/src/db/generateLookupSelectQuery.ts

@ -0,0 +1,399 @@
import { RelationTypes, UITypes } from 'nocodb-sdk';
import type LookupColumn from '../models/LookupColumn';
import type { BaseModelSqlv2 } from '~/db/BaseModelSqlv2';
import type {
BarcodeColumn,
Column,
FormulaColumn,
LinksColumn,
LinkToAnotherRecordColumn,
QrCodeColumn,
RollupColumn,
} from '~/models';
import { Model } from '~/models';
import formulaQueryBuilderv2 from '~/db/formulav2/formulaQueryBuilderv2';
import genRollupSelectv2 from '~/db/genRollupSelectv2';
import { getAliasGenerator } from '~/utils';
import { NcError } from '~/helpers/catchError';
const LOOKUP_VAL_SEPARATOR = '___';
export async function getDisplayValueOfRefTable(
relationCol: Column<LinkToAnotherRecordColumn | LinksColumn>,
) {
return await relationCol
.getColOptions()
.then((colOpt) => colOpt.getRelatedTable())
.then((model) => model.getColumns())
.then((cols) => cols.find((col) => col.pv));
}
export default async function generateLookupSelectQuery({
column,
baseModelSqlv2,
alias,
model: _model,
getAlias = getAliasGenerator('__lk_slt_'),
}: {
column: Column;
baseModelSqlv2: BaseModelSqlv2;
alias: string;
model: Model;
getAlias?: ReturnType<typeof getAliasGenerator>;
}): Promise<any> {
const knex = baseModelSqlv2.dbDriver;
const rootAlias = alias;
{
let selectQb;
const alias = getAlias();
let lookupColOpt: LookupColumn;
let isBtLookup = true;
if (column.uidt === UITypes.Lookup) {
lookupColOpt = await column.getColOptions<LookupColumn>();
} else if (column.uidt !== UITypes.LinkToAnotherRecord) {
NcError.badRequest('Invalid field type');
}
await column.getColOptions<LookupColumn>();
{
const relationCol = lookupColOpt
? await lookupColOpt.getRelationColumn()
: column;
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if not belongs to then throw error as we don't support
if (relation.type === RelationTypes.BELONGS_TO) {
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb = knex(
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`,
).where(
`${alias}.${parentColumn.column_name}`,
knex.raw(`??`, [
`${rootAlias || baseModelSqlv2.getTnPath(childModel.table_name)}.${
childColumn.column_name
}`,
]),
);
}
// if not belongs to then throw error as we don't support
else if (relation.type === RelationTypes.HAS_MANY) {
isBtLookup = false;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb = knex(
`${baseModelSqlv2.getTnPath(childModel.table_name)} as ${alias}`,
).where(
`${alias}.${childColumn.column_name}`,
knex.raw(`??`, [
`${rootAlias || baseModelSqlv2.getTnPath(parentModel.table_name)}.${
parentColumn.column_name
}`,
]),
);
}
// if not belongs to then throw error as we don't support
else if (relation.type === RelationTypes.MANY_TO_MANY) {
isBtLookup = false;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb = knex(
`${baseModelSqlv2.getTnPath(parentModel.table_name)} as ${alias}`,
);
const mmTableAlias = getAlias();
const mmModel = await relation.getMMModel();
const mmChildCol = await relation.getMMChildColumn();
const mmParentCol = await relation.getMMParentColumn();
selectQb
.innerJoin(
baseModelSqlv2.getTnPath(mmModel.table_name, mmTableAlias),
knex.ref(`${mmTableAlias}.${mmParentCol.column_name}`),
'=',
knex.ref(`${alias}.${parentColumn.column_name}`),
)
.where(
knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`),
'=',
knex.ref(
`${
rootAlias || baseModelSqlv2.getTnPath(childModel.table_name)
}.${childColumn.column_name}`,
),
);
}
}
let lookupColumn = lookupColOpt
? await lookupColOpt.getLookupColumn()
: await getDisplayValueOfRefTable(column);
// if lookup column is qr code or barcode extract the referencing column
if ([UITypes.QrCode, UITypes.Barcode].includes(lookupColumn.uidt)) {
lookupColumn = await lookupColumn
.getColOptions<BarcodeColumn | QrCodeColumn>()
.then((barcode) => barcode.getValueColumn());
}
let prevAlias = alias;
while (
lookupColumn.uidt === UITypes.Lookup ||
lookupColumn.uidt === UITypes.LinkToAnotherRecord
) {
const nestedAlias = getAlias();
let relationCol: Column<LinkToAnotherRecordColumn | LinksColumn>;
let nestedLookupColOpt: LookupColumn;
if (lookupColumn.uidt === UITypes.Lookup) {
nestedLookupColOpt = await lookupColumn.getColOptions<LookupColumn>();
relationCol = await nestedLookupColOpt.getRelationColumn();
} else {
relationCol = lookupColumn;
}
const relation =
await relationCol.getColOptions<LinkToAnotherRecordColumn>();
// if any of the relation in nested lookupColOpt is
// not belongs to then throw error as we don't support
if (relation.type === RelationTypes.BELONGS_TO) {
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb.join(
`${baseModelSqlv2.getTnPath(
parentModel.table_name,
)} as ${nestedAlias}`,
`${nestedAlias}.${parentColumn.column_name}`,
`${prevAlias}.${childColumn.column_name}`,
);
} else if (relation.type === RelationTypes.HAS_MANY) {
isBtLookup = false;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
selectQb.join(
`${baseModelSqlv2.getTnPath(
childModel.table_name,
)} as ${nestedAlias}`,
`${nestedAlias}.${childColumn.column_name}`,
`${prevAlias}.${parentColumn.column_name}`,
);
} else if (relation.type === RelationTypes.MANY_TO_MANY) {
isBtLookup = false;
const childColumn = await relation.getChildColumn();
const parentColumn = await relation.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
const mmTableAlias = getAlias();
const mmModel = await relation.getMMModel();
const mmChildCol = await relation.getMMChildColumn();
const mmParentCol = await relation.getMMParentColumn();
selectQb
.innerJoin(
baseModelSqlv2.getTnPath(mmModel.table_name, mmTableAlias),
knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`),
'=',
knex.ref(`${prevAlias}.${childColumn.column_name}`),
)
.innerJoin(
knex.raw('?? as ??', [
baseModelSqlv2.getTnPath(parentModel.table_name),
nestedAlias,
]),
knex.ref(`${mmTableAlias}.${mmParentCol.column_name}`),
'=',
knex.ref(`${nestedAlias}.${parentColumn.column_name}`),
)
.where(
knex.ref(`${mmTableAlias}.${mmChildCol.column_name}`),
'=',
knex.ref(
`${alias || baseModelSqlv2.getTnPath(childModel.table_name)}.${
childColumn.column_name
}`,
),
);
}
if (lookupColumn.uidt === UITypes.Lookup)
lookupColumn = await nestedLookupColOpt.getLookupColumn();
else lookupColumn = await getDisplayValueOfRefTable(relationCol);
prevAlias = nestedAlias;
}
{
// get basemodel and model of lookup column
const model = await lookupColumn.getModel();
const baseModelSqlv2 = await Model.getBaseModelSQL({
model,
dbDriver: knex,
});
switch (lookupColumn.uidt) {
case UITypes.Attachment:
NcError.badRequest(
'Group by using attachment column is not supported',
);
break;
case UITypes.Links:
case UITypes.Rollup:
{
const builder = (
await genRollupSelectv2({
baseModelSqlv2,
knex,
columnOptions:
(await lookupColumn.getColOptions()) as RollupColumn,
alias: prevAlias,
})
).builder;
selectQb.select(builder);
}
break;
case UITypes.Formula:
{
const builder = (
await formulaQueryBuilderv2(
baseModelSqlv2,
(
await lookupColumn.getColOptions<FormulaColumn>()
).formula,
lookupColumn.title,
model,
lookupColumn,
await model.getAliasColMapping(),
prevAlias,
)
).builder;
selectQb.select(builder);
}
break;
case UITypes.DateTime:
{
await baseModelSqlv2.selectObject({
qb: selectQb,
columns: [lookupColumn],
alias: prevAlias,
});
}
break;
default:
{
selectQb.select(
`${prevAlias}.${lookupColumn.column_name} as ${lookupColumn.title}`,
);
}
break;
}
}
// if all relation are belongs to then we don't need to do the aggregation
if (isBtLookup) {
return {
builder: selectQb,
};
}
const subQueryAlias = getAlias();
if (baseModelSqlv2.isPg) {
// alternate approach with array_agg
return {
builder: knex
.select(knex.raw('json_agg(??)::text', [lookupColumn.title]))
.from(selectQb.as(subQueryAlias)),
};
/*
// alternate approach with array_agg
return {
builder: knex
.select(knex.raw('array_agg(??)', [lookupColumn.title]))
.from(selectQb),
};*/
// alternate approach with string aggregation
// return {
// builder: knex
// .select(
// knex.raw('STRING_AGG(??::text, ?)', [
// lookupColumn.title,
// LOOKUP_VAL_SEPARATOR,
// ]),
// )
// .from(selectQb.as(subQueryAlias)),
// };
} else if (baseModelSqlv2.isMySQL) {
return {
builder: knex
.select(
knex.raw('cast(JSON_ARRAYAGG(??) as NCHAR)', [lookupColumn.title]),
)
.from(selectQb.as(subQueryAlias)),
};
// return {
// builder: knex
// .select(
// knex.raw('GROUP_CONCAT(?? ORDER BY ?? ASC SEPARATOR ?)', [
// lookupColumn.title,
// lookupColumn.title,
// LOOKUP_VAL_SEPARATOR,
// ]),
// )
// .from(selectQb.as(subQueryAlias)),
// };
} else if (baseModelSqlv2.isSqlite) {
// ref: https://stackoverflow.com/questions/13382856/sqlite3-join-group-concat-using-distinct-with-custom-separator
// selectQb.orderBy(`${lookupColumn.title}`, 'asc');
return {
builder: knex
.select(
knex.raw(`group_concat(??, ?)`, [
lookupColumn.title,
LOOKUP_VAL_SEPARATOR,
]),
)
.from(selectQb.as(subQueryAlias)),
};
}
NcError.notImplemented('Database not supported Group by on Lookup');
}
}

4
packages/nocodb/src/db/sortV2.ts

@ -36,9 +36,7 @@ export default async function sortV2(
const column = await sort.getColumn(); const column = await sort.getColumn();
if (!column) { if (!column) {
if (throwErrorIfInvalid) { if (throwErrorIfInvalid) {
NcError.unprocessableEntity( NcError.unprocessableEntity(`Invalid field: ${sort.fk_column_id}`);
`Invalid column id '${sort.fk_column_id}' in sort`,
);
} }
continue; continue;
} }

2
packages/nocodb/src/helpers/catchError.ts

@ -324,7 +324,7 @@ export function extractDBError(error): {
/ Invalid object name '(\w+)'./i, / Invalid object name '(\w+)'./i,
); );
const extractMissingColMatch = error.message.match( const extractMissingColMatch = error.message.match(
/ Invalid column name '(\w+)'./i, / Invalid field: (\w+)./i,
); );
if (extractTableNameMatch && extractTableNameMatch[1]) { if (extractTableNameMatch && extractTableNameMatch[1]) {

9
packages/nocodb/src/helpers/getAst.ts

@ -68,11 +68,12 @@ const getAst = async ({
fields = Array.isArray(fields) ? fields : fields.split(','); fields = Array.isArray(fields) ? fields : fields.split(',');
if (throwErrorIfInvalidParams) { if (throwErrorIfInvalidParams) {
const colAliasMap = await model.getColAliasMapping(); const colAliasMap = await model.getColAliasMapping();
const invalidFields = fields.filter((f) => !colAliasMap[f]); const aliasColMap = await model.getAliasColMapping();
if (invalidFields.length) { const invalidFields = fields.filter(
NcError.unprocessableEntity( (f) => !colAliasMap[f] && !aliasColMap[f],
`Following fields are invalid: ${invalidFields.join(', ')}`,
); );
if (invalidFields.length) {
NcError.unprocessableEntity(`Invalid field: ${invalidFields[0]}`);
} }
} }
} else { } else {

7
packages/nocodb/src/modules/jobs/jobs/at-import/at-import.processor.ts

@ -2529,6 +2529,13 @@ export class AtImportProcessor {
await generateMigrationStats(aTblSchema); await generateMigrationStats(aTblSchema);
} }
} catch (e) { } catch (e) {
// delete tables that were created
for (const table of ncSchema.tables) {
await this.tablesService.tableDelete({
tableId: table.id,
user: syncDB.user,
});
}
if (e.message) { if (e.message) {
this.telemetryService.sendEvent({ this.telemetryService.sendEvent({
evt_type: 'a:airtable-import:error', evt_type: 'a:airtable-import:error',

4
packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts

@ -3,7 +3,7 @@ import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull'; import { Job } from 'bull';
import papaparse from 'papaparse'; import papaparse from 'papaparse';
import debug from 'debug'; import debug from 'debug';
import { isLinksOrLTAR } from 'nocodb-sdk'; import { isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk';
import { Base, Column, Model, Source } from '~/models'; import { Base, Column, Model, Source } from '~/models';
import { BasesService } from '~/services/bases.service'; import { BasesService } from '~/services/bases.service';
import { import {
@ -373,6 +373,7 @@ export class DuplicateProcessor {
}); });
// update cdf // update cdf
if (!isVirtualCol(destColumn)) {
await this.columnsService.columnUpdate({ await this.columnsService.columnUpdate({
columnId: findWithIdentifier(idMap, sourceColumn.id), columnId: findWithIdentifier(idMap, sourceColumn.id),
column: { column: {
@ -381,6 +382,7 @@ export class DuplicateProcessor {
}, },
user: req.user, user: req.user,
}); });
}
this.debugLog(`job completed for ${job.id} (${JobTypes.DuplicateModel})`); this.debugLog(`job completed for ${job.id} (${JobTypes.DuplicateModel})`);

11
packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts

@ -129,6 +129,17 @@ export class ExportService {
} }
break; break;
case 'formula': case 'formula':
// rewrite formula_raw with aliases
column.colOptions['formula_raw'] = column.colOptions[k].replace(
/\{\{.*?\}\}/gm,
(match) => {
const col = model.columns.find(
(c) => c.id === match.slice(2, -2),
);
return `{${col?.title}}`;
},
);
column.colOptions[k] = column.colOptions[k].replace( column.colOptions[k] = column.colOptions[k].replace(
/(?<=\{\{).*?(?=\}\})/gm, /(?<=\{\{).*?(?=\}\})/gm,
(match) => idMap.get(match), (match) => idMap.get(match),

1
packages/nocodb/src/plugins/backblaze/Backblaze.ts

@ -54,6 +54,7 @@ export default class Backblaze implements IStorageAdapterV2 {
.get(url, { .get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer', responseType: 'arraybuffer',
}) })
.then((response) => { .then((response) => {

1
packages/nocodb/src/plugins/gcs/Gcs.ts

@ -110,6 +110,7 @@ export default class Gcs implements IStorageAdapterV2 {
.get(url, { .get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer', responseType: 'arraybuffer',
}) })
.then((response) => { .then((response) => {

1
packages/nocodb/src/plugins/linode/LinodeObjectStorage.ts

@ -53,6 +53,7 @@ export default class LinodeObjectStorage implements IStorageAdapterV2 {
.get(url, { .get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer', responseType: 'arraybuffer',
}) })
.then((response) => { .then((response) => {

1
packages/nocodb/src/plugins/mino/Minio.ts

@ -100,6 +100,7 @@ export default class Minio implements IStorageAdapterV2 {
.get(url, { .get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer', responseType: 'arraybuffer',
}) })
.then((response) => { .then((response) => {

1
packages/nocodb/src/plugins/ovhCloud/OvhCloud.ts

@ -53,6 +53,7 @@ export default class OvhCloud implements IStorageAdapterV2 {
.get(url, { .get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer', responseType: 'arraybuffer',
}) })
.then((response) => { .then((response) => {

12
packages/nocodb/src/plugins/s3/S3.ts

@ -2,6 +2,7 @@ import fs from 'fs';
import { promisify } from 'util'; import { promisify } from 'util';
import { GetObjectCommand, S3 as S3Client } from '@aws-sdk/client-s3'; import { GetObjectCommand, S3 as S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Upload } from '@aws-sdk/lib-storage';
import axios from 'axios'; import axios from 'axios';
import { useAgent } from 'request-filtering-agent'; import { useAgent } from 'request-filtering-agent';
import type { IStorageAdapterV2, XcFile } from 'nc-plugin'; import type { IStorageAdapterV2, XcFile } from 'nc-plugin';
@ -60,7 +61,7 @@ export default class S3 implements IStorageAdapterV2 {
.get(url, { .get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
responseType: 'arraybuffer', responseType: 'stream',
}) })
.then((response) => { .then((response) => {
uploadParams.Body = response.data; uploadParams.Body = response.data;
@ -162,8 +163,13 @@ export default class S3 implements IStorageAdapterV2 {
private async upload(uploadParams): Promise<any> { private async upload(uploadParams): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// call S3 to retrieve upload file to specified bucket // call S3 to retrieve upload file to specified bucket
this.s3Client const upload = new Upload({
.putObject({ ...this.defaultParams, ...uploadParams }) client: this.s3Client,
params: { ...this.defaultParams, ...uploadParams },
});
upload
.done()
.then((data) => { .then((data) => {
if (data) { if (data) {
resolve( resolve(

1
packages/nocodb/src/plugins/scaleway/ScalewayObjectStorage.ts

@ -103,6 +103,7 @@ export default class ScalewayObjectStorage implements IStorageAdapterV2 {
.get(url, { .get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer', responseType: 'arraybuffer',
}) })
.then((response) => { .then((response) => {

1
packages/nocodb/src/plugins/spaces/Spaces.ts

@ -53,6 +53,7 @@ export default class Spaces implements IStorageAdapterV2 {
.get(url, { .get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer', responseType: 'arraybuffer',
}) })
.then((response) => { .then((response) => {

1
packages/nocodb/src/plugins/upcloud/UpoCloud.ts

@ -53,6 +53,7 @@ export default class UpoCloud implements IStorageAdapterV2 {
.get(url, { .get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer', responseType: 'arraybuffer',
}) })
.then((response) => { .then((response) => {

1
packages/nocodb/src/plugins/vultr/Vultr.ts

@ -53,6 +53,7 @@ export default class Vultr implements IStorageAdapterV2 {
.get(url, { .get(url, {
httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }), httpsAgent: useAgent(url, { stopPortScanningByUrlRedirection: true }),
// TODO - use stream instead of buffer
responseType: 'arraybuffer', responseType: 'arraybuffer',
}) })
.then((response) => { .then((response) => {

30
packages/nocodb/src/services/api-docs/api-docs.service.ts

@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import getSwaggerJSON from './swagger/getSwaggerJSON'; import getSwaggerJSON from './swagger/getSwaggerJSON';
import getSwaggerJSONV2 from './swaggerV2/getSwaggerJSONV2';
import { NcError } from '~/helpers/catchError'; import { NcError } from '~/helpers/catchError';
import { Base, Model } from '~/models'; import { Base, Model } from '~/models';
@ -32,6 +33,35 @@ export class ApiDocsService {
}, },
] as any; ] as any;
return swagger;
}
async swaggerJsonV2(param: { baseId: string; siteUrl: string }) {
const base = await Base.get(param.baseId);
if (!base) NcError.notFound();
const models = await Model.list({
base_id: param.baseId,
source_id: null,
});
const swagger = await getSwaggerJSONV2(base, models);
swagger.servers = [
{
url: param.siteUrl,
},
{
url: '{customUrl}',
variables: {
customUrl: {
default: param.siteUrl,
description: 'Provide custom nocodb app base url',
},
},
},
] as any;
return swagger; return swagger;
} }
} }

2
packages/nocodb/src/services/api-docs/swagger/templates/paths.ts

@ -667,6 +667,6 @@ function getPaginatedResponseType(type: string) {
}, },
}; };
} }
function isRelationExist(columns: SwaggerColumn[]) { export function isRelationExist(columns: SwaggerColumn[]) {
return columns.some((c) => isLinksOrLTAR(c.column) && !c.column.system); return columns.some((c) => isLinksOrLTAR(c.column) && !c.column.system);
} }

28
packages/nocodb/src/services/api-docs/swaggerV2/getPaths.ts

@ -0,0 +1,28 @@
import { getModelPaths } from './templates/paths';
import type { Model } from '~/models';
import type { SwaggerColumn } from './getSwaggerColumnMetas';
import type { SwaggerView } from './getSwaggerJSONV2';
import Noco from '~/Noco';
export default async function getPaths(
{
model,
columns,
views,
}: {
model: Model;
columns: SwaggerColumn[];
views: SwaggerView[];
},
_ncMeta = Noco.ncMeta,
) {
const swaggerPaths = await getModelPaths({
tableName: model.title,
tableId: model.id,
views,
type: model.type,
columns,
});
return swaggerPaths;
}

29
packages/nocodb/src/services/api-docs/swaggerV2/getSchemas.ts

@ -0,0 +1,29 @@
import { getModelSchemas } from './templates/schemas';
import type { Base, Model } from '~/models';
import type { SwaggerColumn } from './getSwaggerColumnMetas';
import type { SwaggerView } from './getSwaggerJSONV2';
import Noco from '~/Noco';
export default async function getSchemas(
{
base,
model,
columns,
}: {
base: Base;
model: Model;
columns: SwaggerColumn[];
views: SwaggerView[];
},
_ncMeta = Noco.ncMeta,
) {
const swaggerSchemas = getModelSchemas({
tableName: model.title,
orgs: 'v1',
baseName: base.title,
columns,
});
return swaggerSchemas;
}

67
packages/nocodb/src/services/api-docs/swaggerV2/getSwaggerColumnMetas.ts

@ -0,0 +1,67 @@
import { UITypes } from 'nocodb-sdk';
import type { Base, Column, LinkToAnotherRecordColumn } from '~/models';
import SwaggerTypes from '~/db/sql-mgr/code/routers/xc-ts/SwaggerTypes';
import Noco from '~/Noco';
export default async (
columns: Column[],
base: Base,
ncMeta = Noco.ncMeta,
): Promise<SwaggerColumn[]> => {
const dbType = await base.getBases().then((b) => b?.[0]?.type);
return Promise.all(
columns.map(async (c) => {
const field: SwaggerColumn = {
title: c.title,
type: 'object',
virtual: true,
column: c,
};
switch (c.uidt) {
case UITypes.LinkToAnotherRecord:
{
const colOpt = await c.getColOptions<LinkToAnotherRecordColumn>(
ncMeta,
);
if (colOpt) {
const relTable = await colOpt.getRelatedTable(ncMeta);
field.type = undefined;
field.$ref = `#/components/schemas/${relTable.title}Request`;
}
}
break;
case UITypes.Formula:
case UITypes.Lookup:
field.type = 'object';
break;
case UITypes.Rollup:
case UITypes.Links:
field.type = 'number';
break;
case UITypes.Attachment:
field.type = 'array';
field.items = {
$ref: `#/components/schemas/Attachment`,
};
break;
default:
field.virtual = false;
SwaggerTypes.setSwaggerType(c, field, dbType);
break;
}
return field;
}),
);
};
export interface SwaggerColumn {
type: any;
title: string;
description?: string;
virtual?: boolean;
$ref?: any;
column: Column;
items?: any;
}

66
packages/nocodb/src/services/api-docs/swaggerV2/getSwaggerJSONV2.ts

@ -0,0 +1,66 @@
import { ViewTypes } from 'nocodb-sdk';
import swaggerBase from './swagger-base.json';
import getPaths from './getPaths';
import getSchemas from './getSchemas';
import getSwaggerColumnMetas from './getSwaggerColumnMetas';
import type {
Base,
FormViewColumn,
GalleryViewColumn,
GridViewColumn,
Model,
View,
} from '~/models';
import Noco from '~/Noco';
export default async function getSwaggerJSONV2(
base: Base,
models: Model[],
ncMeta = Noco.ncMeta,
) {
// base swagger object
const swaggerObj = {
...swaggerBase,
paths: {},
components: {
...swaggerBase.components,
schemas: { ...swaggerBase.components.schemas },
},
};
// iterate and populate swagger schema and path for models and views
for (const model of models) {
let paths = {};
const columns = await getSwaggerColumnMetas(
await model.getColumns(ncMeta),
base,
ncMeta,
);
const views: SwaggerView[] = [];
for (const view of (await model.getViews(false, ncMeta)) || []) {
if (view.type !== ViewTypes.GRID) continue;
views.push({
view,
columns: await view.getColumns(ncMeta),
});
}
// skip mm tables
if (!model.mm) paths = await getPaths({ model, columns, views }, ncMeta);
const schemas = await getSchemas({ base, model, columns, views }, ncMeta);
Object.assign(swaggerObj.paths, paths);
Object.assign(swaggerObj.components.schemas, schemas);
}
return swaggerObj;
}
export interface SwaggerView {
view: View;
columns: Array<GridViewColumn | GalleryViewColumn | FormViewColumn>;
}

128
packages/nocodb/src/services/api-docs/swaggerV2/swagger-base.json

@ -0,0 +1,128 @@
{
"openapi": "3.0.0",
"info": {
"title": "nocodb",
"version": "2.0"
},
"servers": [
{
"url": "http://localhost:8080"
}
],
"paths": {
},
"components": {
"schemas": {
"Paginated": {
"title": "Paginated",
"type": "object",
"properties": {
"pageSize": {
"type": "integer"
},
"totalRows": {
"type": "integer"
},
"isFirstPage": {
"type": "boolean"
},
"isLastPage": {
"type": "boolean"
},
"page": {
"type": "number"
}
}
},
"Attachment": {
"title": "Attachment",
"type": "object",
"properties": {
"mimetype": {
"type": "string"
},
"size": {
"type": "integer"
},
"title": {
"type": "string"
},
"url": {
"type": "string"
},
"icon": {
"type": "string"
}
}
},
"Groupby": {
"title": "Groupby",
"type": "object",
"properties": {
"count": {
"type": "number",
"description": "count"
},
"column_name": {
"type": "string",
"description": "the value of the given column"
}
}
}
},
"securitySchemes": {
"xcAuth": {
"type": "apiKey",
"in": "header",
"name": "xc-auth",
"description": "JWT access token"
},
"xcToken": {
"type": "apiKey",
"in": "header",
"name": "xc-token",
"description": "API token"
}
},
"responses": {
"BadRequest": {
"description": "BadReqeust",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"msg": {
"type": "string",
"x-stoplight": {
"id": "p9mk4oi0hbihm"
},
"example": "BadRequest [Error]: <ERROR MESSAGE>"
}
},
"required": [
"msg"
]
},
"examples": {
"Example 1": {
"value": {
"msg": "BadRequest [Error]: <ERROR MESSAGE>"
}
}
}
}
},
"headers": {}
}
}
},
"security": [
{
"xcAuth": []
},
{
"xcToken": []
}
]
}

10
packages/nocodb/src/services/api-docs/swaggerV2/templates/headers.ts

@ -0,0 +1,10 @@
export const csvExportResponseHeader = {
'nc-export-offset': {
schema: {
type: 'integer',
},
description:
'Offset of next set of data which will be helpful if there is large amount of data. It will returns `-1` if all set of data exported.',
example: '1000',
},
};

237
packages/nocodb/src/services/api-docs/swaggerV2/templates/params.ts

@ -0,0 +1,237 @@
import { isLinksOrLTAR, RelationTypes, UITypes } from 'nocodb-sdk';
import type { LinkToAnotherRecordColumn } from '~/models';
import type { SwaggerColumn } from '../getSwaggerColumnMetas';
import type { SwaggerView } from '~/services/api-docs/swaggerV2/getSwaggerJSONV2';
export const recordIdParam = {
schema: {
type: 'string',
},
name: 'recordId',
in: 'path',
required: true,
example: 1,
description:
'Primary key of the record you want to read. If the table have composite primary key then combine them by using `___` and pass it as primary key.',
};
export const fieldsParam = {
schema: {
type: 'string',
},
in: 'query',
name: 'fields',
description:
'Array of field names or comma separated filed names to include in the response objects. In array syntax pass it like `fields[]=field1&fields[]=field2` or alternately `fields=field1,field2`.',
};
export const sortParam = {
schema: {
type: 'string',
},
in: 'query',
name: 'sort',
description:
'Comma separated field names to sort rows, rows will sort in ascending order based on provided columns. To sort in descending order provide `-` prefix along with column name, like `-field`. Example : `sort=field1,-field2`',
};
export const whereParam = {
schema: {
type: 'string',
},
in: 'query',
name: 'where',
description:
'This can be used for filtering rows, which accepts complicated where conditions. For more info visit [here](https://docs.nocodb.com/developer-resources/rest-apis#comparison-operators). Example : `where=(field1,eq,value)`',
};
export const limitParam = {
schema: {
type: 'number',
minimum: 1,
},
in: 'query',
name: 'limit',
description:
'The `limit` parameter used for pagination, the response collection size depends on limit value with default value `25` and maximum value `1000`, which can be overridden by environment variables `DB_QUERY_LIMIT_DEFAULT` and `DB_QUERY_LIMIT_MAX` respectively.',
example: 25,
};
export const offsetParam = {
schema: {
type: 'number',
minimum: 0,
},
in: 'query',
name: 'offset',
description:
'The `offset` parameter used for pagination, the value helps to select collection from a certain index.',
example: 0,
};
export const shuffleParam = {
schema: {
type: 'number',
minimum: 0,
maximum: 1,
},
in: 'query',
name: 'shuffle',
description:
'The `shuffle` parameter used for pagination, the response will be shuffled if it is set to 1.',
example: 0,
};
export const columnNameQueryParam = {
schema: {
type: 'string',
},
in: 'query',
name: 'column_name',
description:
'Column name of the column you want to group by, eg. `column_name=column1`',
};
export const linkFieldNameParam = (columns: SwaggerColumn[]) => {
const linkColumnIds = [];
const description = [
'**Links Field Identifier** corresponding to the relation field `Links` established between tables.\n\nLink Columns:',
];
for (const { column } of columns) {
if (!isLinksOrLTAR(column) || column.system) continue;
linkColumnIds.push(column.id);
description.push(`* ${column.id} - ${column.title}`);
}
return {
schema: {
type: 'string',
enum: linkColumnIds,
},
name: 'linkFieldId',
in: 'path',
required: true,
description: description.join('\n'),
};
};
export const viewIdParams = (views: SwaggerView[]) => {
const viewIds = [];
const description = [
'Allows you to fetch records that are currently visible within a specific view.\n\nViews:',
];
for (const { view } of views) {
viewIds.push(view.id);
description.push(
`* ${view.id} - ${view.is_default ? 'Default view' : view.title}`,
);
}
return {
schema: {
type: 'string',
enum: viewIds,
},
description: description.join('\n'),
name: 'viewId',
in: 'query',
required: false,
};
};
export const referencedRowIdParam = {
schema: {
type: 'string',
},
name: 'refRowId',
in: 'path',
required: true,
};
export const exportTypeParam = {
schema: {
type: 'string',
enum: ['csv', 'excel'],
},
name: 'type',
in: 'path',
required: true,
};
export const csvExportOffsetParam = {
schema: {
type: 'number',
minimum: 0,
},
in: 'query',
name: 'offset',
description:
'Helps to start export from a certain index. You can get the next set of data offset from previous response header named `nc-export-offset`.',
example: 0,
};
export const nestedWhereParam = (colName) => ({
schema: {
type: 'string',
},
in: 'query',
name: `nested[${colName}][where]`,
description: `This can be used for filtering rows in nested column \`${colName}\`, which accepts complicated where conditions. For more info visit [here](https://docs.nocodb.com/developer-resources/rest-apis#comparison-operators). Example : \`nested[${colName}][where]=(field1,eq,value)\``,
});
export const nestedFieldParam = (colName) => ({
schema: {
type: 'string',
},
in: 'query',
name: `nested[${colName}][fields]`,
description: `Array of field names or comma separated filed names to include in the in nested column \`${colName}\` result. In array syntax pass it like \`fields[]=field1&fields[]=field2.\`. Example : \`nested[${colName}][fields]=field1,field2\``,
});
export const nestedSortParam = (colName) => ({
schema: {
type: 'string',
},
in: 'query',
name: `nested[${colName}][sort]`,
description: `Comma separated field names to sort rows in nested column \`${colName}\` rows, it will sort in ascending order based on provided columns. To sort in descending order provide \`-\` prefix along with column name, like \`-field\`. Example : \`nested[${colName}][sort]=field1,-field2\``,
});
export const nestedLimitParam = (colName) => ({
schema: {
type: 'number',
minimum: 1,
},
in: 'query',
name: `nested[${colName}][limit]`,
description: `The \`limit\` parameter used for pagination of nested \`${colName}\` rows, the response collection size depends on limit value and default value is \`25\`.`,
example: '25',
});
export const nestedOffsetParam = (colName) => ({
schema: {
type: 'number',
minimum: 0,
},
in: 'query',
name: `nested[${colName}][offset]`,
description: `The \`offset\` parameter used for pagination of nested \`${colName}\` rows, the value helps to select collection from a certain index.`,
example: 0,
});
export const getNestedParams = async (
columns: SwaggerColumn[],
): Promise<any[]> => {
return await columns.reduce(async (paramsArr, { column }) => {
if (column.uidt === UITypes.LinkToAnotherRecord && !column.system) {
const colOpt = await column.getColOptions<LinkToAnotherRecordColumn>();
if (colOpt.type !== RelationTypes.BELONGS_TO) {
return [
...(await paramsArr),
nestedWhereParam(column.title),
nestedOffsetParam(column.title),
nestedLimitParam(column.title),
nestedFieldParam(column.title),
nestedSortParam(column.title),
];
} else {
return [...(await paramsArr), nestedFieldParam(column.title)];
}
}
return paramsArr;
}, Promise.resolve([]));
};

407
packages/nocodb/src/services/api-docs/swaggerV2/templates/paths.ts

@ -0,0 +1,407 @@
import { ModelTypes } from 'nocodb-sdk';
import {
fieldsParam,
getNestedParams,
limitParam,
linkFieldNameParam,
offsetParam,
recordIdParam,
shuffleParam,
sortParam,
viewIdParams,
whereParam,
} from './params';
import type { SwaggerColumn } from '../getSwaggerColumnMetas';
import type { SwaggerView } from '~/services/api-docs/swaggerV2/getSwaggerJSONV2';
import { isRelationExist } from '~/services/api-docs/swagger/templates/paths';
export const getModelPaths = async (ctx: {
tableName: string;
type: ModelTypes;
columns: SwaggerColumn[];
tableId: string;
views: SwaggerView[];
}): Promise<{ [path: string]: any }> => ({
[`/api/v2/tables/${ctx.tableId}/records`]: {
get: {
summary: `${ctx.tableName} list`,
operationId: `${ctx.tableName.toLowerCase()}-db-table-row-list`,
description: `List of all rows from ${ctx.tableName} ${ctx.type} and response data fields can be filtered based on query params.`,
tags: [ctx.tableName],
parameters: [
viewIdParams(ctx.views),
fieldsParam,
sortParam,
whereParam,
limitParam,
shuffleParam,
offsetParam,
...(await getNestedParams(ctx.columns)),
],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: getPaginatedResponseType(`${ctx.tableName}Response`),
},
},
},
},
},
...(ctx.type === ModelTypes.TABLE
? {
post: {
summary: `${ctx.tableName} create`,
description:
'Insert a new row in table by providing a key value pair object where key refers to the column alias. All the required fields should be included with payload excluding `autoincrement` and column with default value.',
operationId: `${ctx.tableName.toLowerCase()}-create`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Response`,
},
},
},
},
'400': {
$ref: '#/components/responses/BadRequest',
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
$ref: `#/components/schemas/${ctx.tableName}Request`,
},
{
type: 'array',
items: {
$ref: `#/components/schemas/${ctx.tableName}Request`,
},
},
],
},
},
},
},
},
patch: {
summary: `${ctx.tableName} update`,
operationId: `${ctx.tableName.toLowerCase()}-update`,
description:
'Partial update row in table by providing a key value pair object where key refers to the column alias. You need to only include columns which you want to update.',
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
},
},
},
'400': {
$ref: '#/components/responses/BadRequest',
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
$ref: `#/components/schemas/${ctx.tableName}Request`,
},
{
type: 'array',
items: {
$ref: `#/components/schemas/${ctx.tableName}Request`,
},
},
],
},
},
},
},
},
delete: {
summary: `${ctx.tableName} delete`,
operationId: `${ctx.tableName.toLowerCase()}-delete`,
responses: {
'200': {
description: 'OK',
},
},
tags: [ctx.tableName],
description:
'Delete a row by using the **primary key** column value.',
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
$ref: `#/components/schemas/${ctx.tableName}IdRequest`,
},
{
type: 'array',
items: {
$ref: `#/components/schemas/${ctx.tableName}IdRequest`,
},
},
],
},
},
},
},
},
}
: {}),
},
[`/api/v2/tables/${ctx.tableId}/records/{recordId}`]: {
get: {
parameters: [recordIdParam, fieldsParam],
summary: `${ctx.tableName} read`,
description: 'Read a row data by using the **primary key** column value.',
operationId: `${ctx.tableName.toLowerCase()}-read`,
tags: [ctx.tableName],
responses: {
'201': {
description: 'Created',
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${ctx.tableName}Response`,
},
},
},
},
},
},
},
[`/api/v2/tables/${ctx.tableId}/records/count`]: {
parameters: [viewIdParams(ctx.views)],
get: {
summary: `${ctx.tableName} count`,
operationId: `${ctx.tableName.toLowerCase()}-count`,
description: 'Get rows count of a table by applying optional filters.',
tags: [ctx.tableName],
parameters: [whereParam],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
count: {
type: 'number',
},
},
required: ['list', 'pageInfo'],
},
examples: {
'Example 1': {
value: {
count: 3,
},
},
},
},
},
},
'400': {
$ref: '#/components/responses/BadRequest',
},
},
},
},
...(isRelationExist(ctx.columns)
? {
[`/api/v2/tables/${ctx.tableId}/links/{linkFieldId}/records/{recordId}`]:
{
parameters: [linkFieldNameParam(ctx.columns), recordIdParam],
get: {
summary: 'Link Records list',
operationId: `${ctx.tableName.toLowerCase()}-nested-list`,
description:
'This API endpoint allows you to retrieve list of linked records for a specific `Link field` and `Record ID`. The response is an array of objects containing Primary Key and its corresponding display value.',
tags: [ctx.tableName],
parameters: [
fieldsParam,
sortParam,
whereParam,
limitParam,
offsetParam,
],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
list: {
type: 'array',
description: 'List of data objects',
items: {
type: 'object',
},
},
pageInfo: {
$ref: '#/components/schemas/Paginated',
description: 'Paginated Info',
},
},
required: ['list', 'pageInfo'],
},
},
},
},
'400': {
$ref: '#/components/responses/BadRequest',
},
},
},
post: {
summary: 'Link Records',
operationId: `${ctx.tableName.toLowerCase()}-nested-link`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
examples: {
'Example 1': {
value: true,
},
},
},
},
},
'400': {
$ref: '#/components/responses/BadRequest',
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
type: 'object',
},
{
type: 'array',
items: {
type: 'object',
},
},
],
},
examples: {
'Example 1': {
value: [
{
Id: 4,
},
{
Id: 5,
},
],
},
},
},
},
},
description:
'This API endpoint allows you to link records to a specific `Link field` and `Record ID`. The request payload is an array of record-ids from the adjacent table for linking purposes. Note that any existing links, if present, will be unaffected during this operation.',
parameters: [recordIdParam],
},
delete: {
summary: 'Unlink Records',
operationId: `${ctx.tableName.toLowerCase()}-nested-unlink`,
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: {},
examples: {
'Example 1': {
value: true,
},
},
},
},
},
'400': {
$ref: '#/components/responses/BadRequest',
},
},
tags: [ctx.tableName],
requestBody: {
content: {
'application/json': {
schema: {
oneOf: [
{
type: 'array',
items: {
type: 'object',
},
},
],
},
examples: {
'Example 1': {
value: [
{
Id: 1,
},
{
Id: 2,
},
],
},
},
},
},
},
description:
'This API endpoint allows you to unlink records from a specific `Link field` and `Record ID`. The request payload is an array of record-ids from the adjacent table for unlinking purposes. Note that, \n- duplicated record-ids will be ignored.\n- non-existent record-ids will be ignored.',
parameters: [recordIdParam],
},
},
}
: {}),
});
function getPaginatedResponseType(type: string) {
return {
type: 'object',
properties: {
list: {
type: 'array',
items: {
$ref: `#/components/schemas/${type}`,
},
},
PageInfo: {
$ref: `#/components/schemas/Paginated`,
},
},
};
}

109
packages/nocodb/src/services/api-docs/swaggerV2/templates/schemas.ts

@ -0,0 +1,109 @@
import { isSystemColumn } from 'nocodb-sdk';
import type { SwaggerColumn } from '../getSwaggerColumnMetas';
export const getModelSchemas = (ctx: {
tableName: string;
orgs: string;
baseName: string;
columns: Array<SwaggerColumn>;
}) => ({
[`${ctx.tableName}Response`]: {
title: `${ctx.tableName} Response`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
...(column.system
? {}
: {
[title]: fieldProps,
}),
}),
{},
) || {}),
},
},
[`${ctx.tableName}Request`]: {
title: `${ctx.tableName} Request`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
...(virtual || isSystemColumn(column) || column.ai || column.meta?.ag
? {}
: {
[title]: fieldProps,
}),
}),
{},
) || {}),
},
},
[`${ctx.tableName}IdRequest`]: {
title: `${ctx.tableName} Id Request`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
...(column.pk
? {
[title]: fieldProps,
}
: {}),
}),
{},
) || {}),
},
},
});
export const getViewSchemas = (ctx: {
tableName: string;
viewName: string;
orgs: string;
baseName: string;
columns: Array<SwaggerColumn>;
}) => ({
[`${ctx.tableName}${ctx.viewName}GridResponse`]: {
title: `${ctx.tableName} : ${ctx.viewName} Response`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
[title]: fieldProps,
}),
{},
) || {}),
},
},
[`${ctx.tableName}${ctx.viewName}GridRequest`]: {
title: `${ctx.tableName} : ${ctx.viewName} Request`,
type: 'object',
description: '',
'x-internal': false,
properties: {
...(ctx.columns?.reduce(
(colsObj, { title, virtual, column, ...fieldProps }) => ({
...colsObj,
...(virtual
? {}
: {
[title]: fieldProps,
}),
}),
{},
) || {}),
},
},
});

3
packages/nocodb/src/services/datas.service.ts

@ -27,6 +27,7 @@ export class DatasService {
model, model,
view, view,
query: param.query, query: param.query,
throwErrorIfInvalidParams: true,
}); });
} }
@ -51,7 +52,7 @@ export class DatasService {
dbDriver: await NcConnectionMgrv2.get(source), dbDriver: await NcConnectionMgrv2.get(source),
}); });
const countArgs: any = { ...param.query }; const countArgs: any = { ...param.query, throwErrorIfInvalidParams: true };
try { try {
countArgs.filterArr = JSON.parse(countArgs.filterArrJson); countArgs.filterArr = JSON.parse(countArgs.filterArrJson);
} catch (e) {} } catch (e) {}

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

@ -461,9 +461,7 @@ export class TablesService {
} }
if (column.column_name.length > mxColumnLength) { if (column.column_name.length > mxColumnLength) {
NcError.badRequest( column.column_name = column.column_name.slice(0, mxColumnLength);
`Column name ${column.column_name} exceeds ${mxColumnLength} characters`,
);
} }
if (column.title && column.title.length > 255) { if (column.title && column.title.length > 255) {

5
packages/nocodb/src/utils/globals.ts

@ -171,6 +171,11 @@ export enum CacheDelDirection {
CHILD_TO_PARENT = 'CHILD_TO_PARENT', CHILD_TO_PARENT = 'CHILD_TO_PARENT',
} }
export const GROUPBY_COMPARISON_OPS = <const>[
// these are used for groupby
'gb_eq',
'gb_null',
];
export const COMPARISON_OPS = <const>[ export const COMPARISON_OPS = <const>[
'eq', 'eq',
'neq', 'neq',

10
packages/nocodb/tests/unit/rest/tests/groupby.test.ts

@ -299,7 +299,7 @@ function groupByTests() {
expect(response.body.list.length).to.equal(1); expect(response.body.list.length).to.equal(1);
}); });
it('Check One GroupBy Column with MM Lookup which is not supported', async function () { it('Check One GroupBy Column with MM Lookup which is supported', async function () {
await createLookupColumn(context, { await createLookupColumn(context, {
base: sakilaProject, base: sakilaProject,
title: 'ActorNames', title: 'ActorNames',
@ -308,15 +308,17 @@ function groupByTests() {
relatedTableColumnTitle: 'FirstName', relatedTableColumnTitle: 'FirstName',
}); });
const res = await request(context.app) const response = await request(context.app)
.get(`/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/groupby`) .get(`/api/v1/db/data/noco/${sakilaProject.id}/${filmTable.id}/groupby`)
.set('xc-auth', context.token) .set('xc-auth', context.token)
.query({ .query({
column_name: 'ActorNames', column_name: 'ActorNames',
}) })
.expect(400); .expect(200);
assert.match(res.body.msg, /not supported/); assert.match(response.body.list[1]['ActorNames'], /ADAM|ANNE/);
expect(+response.body.list[1]['count']).to.gt(0);
expect(response.body.list.length).to.equal(25);
}); });
it('Check One GroupBy Column with Formula and Formula referring another formula', async function () { it('Check One GroupBy Column with Formula and Formula referring another formula', async function () {

3496
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

3
renovate.json

@ -3,7 +3,6 @@
"extends": [ "extends": [
"config:base", "config:base",
":dependencyDashboard", ":dependencyDashboard",
":onlyNpm",
":prConcurrentLimit20", ":prConcurrentLimit20",
":autodetectPinVersions", ":autodetectPinVersions",
":label(renovate)", ":label(renovate)",
@ -62,5 +61,5 @@
"assignees": [ "assignees": [
"wingkwong" "wingkwong"
], ],
"enabled": false "enabled": true
} }

8
scripts/pkg-executable/package.json

@ -27,9 +27,9 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.17.1", "express": "^4.17.3",
"nocodb": "0.202.5", "nocodb": "0.202.7",
"@nestjs/common": "^10.2.1", "@nestjs/common": "^10.2.9",
"@nestjs/core": "^10.2.1" "@nestjs/core": "^10.2.9"
} }
} }

6
tests/playwright/pages/Dashboard/Import/ImportTemplate.ts

@ -23,7 +23,11 @@ export class ImportTemplatePage extends BasePage {
const rowCount = await tr.count(); const rowCount = await tr.count();
const tableList: string[] = []; const tableList: string[] = [];
for (let i = 0; i < rowCount; i++) { for (let i = 0; i < rowCount; i++) {
const tableName = await getTextExcludeIconText(tr.nth(i)); const tableName = await this.get()
.locator(`.ant-collapse-header`)
.nth(i)
.locator('input[type="text"]')
.inputValue();
tableList.push(tableName); tableList.push(tableName);
} }
return tableList; return tableList;

Loading…
Cancel
Save