Browse Source

Merge remote-tracking branch 'origin/develop' into fix/i18n

pull/6614/head
Muhammed Mustafa 11 months ago
parent
commit
a856d4bb18
  1. 14
      .github/workflows/publish-api-docs.yml
  2. 6
      .github/workflows/release-docker.yml
  3. 4
      .github/workflows/release-npm.yml
  4. 24
      README.md
  5. BIN
      docker-compose/sqlite/nocodb/noco.db
  6. 2
      packages/nc-gui/assets/style.scss
  7. 1
      packages/nc-gui/assets/style/fonts.css
  8. 1
      packages/nc-gui/components.d.ts
  9. 6
      packages/nc-gui/components/cell/Checkbox.vue
  10. 15
      packages/nc-gui/components/cell/Json.vue
  11. 7
      packages/nc-gui/components/cell/MultiSelect.vue
  12. 8
      packages/nc-gui/components/cell/SingleSelect.vue
  13. 2
      packages/nc-gui/components/cell/attachment/Carousel.vue
  14. 42
      packages/nc-gui/components/cell/attachment/index.vue
  15. 4
      packages/nc-gui/components/dashboard/Sidebar.vue
  16. 55
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  17. 2
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  18. 50
      packages/nc-gui/components/dashboard/TreeView/index.vue
  19. 11
      packages/nc-gui/components/dashboard/View.vue
  20. 28
      packages/nc-gui/components/dlg/AirtableImport.vue
  21. 77
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  22. 8
      packages/nc-gui/components/dlg/QuickImport.vue
  23. 138
      packages/nc-gui/components/dlg/SharedBaseDuplicate.vue
  24. 75
      packages/nc-gui/components/dlg/TableDuplicate.vue
  25. 2
      packages/nc-gui/components/general/HelpAndSupport.vue
  26. 2
      packages/nc-gui/components/general/JoinCloud.vue
  27. 13
      packages/nc-gui/components/general/ShareProject.vue
  28. 8
      packages/nc-gui/components/general/WorkspaceIcon.vue
  29. 7
      packages/nc-gui/components/nc/Button.vue
  30. 2
      packages/nc-gui/components/nc/Pagination.vue
  31. 10
      packages/nc-gui/components/nc/Select.vue
  32. 2
      packages/nc-gui/components/shared-view/Gallery.vue
  33. 6
      packages/nc-gui/components/shared-view/Grid.vue
  34. 2
      packages/nc-gui/components/shared-view/Kanban.vue
  35. 2
      packages/nc-gui/components/shared-view/Map.vue
  36. 2
      packages/nc-gui/components/smartsheet/ApiSnippet.vue
  37. 2
      packages/nc-gui/components/smartsheet/Details.vue
  38. 8
      packages/nc-gui/components/smartsheet/Form.vue
  39. 13
      packages/nc-gui/components/smartsheet/Pagination.vue
  40. 13
      packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue
  41. 12
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  42. 13
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  43. 11
      packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue
  44. 2
      packages/nc-gui/components/smartsheet/details/Api.vue
  45. 7
      packages/nc-gui/components/smartsheet/details/Fields.vue
  46. 78
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  47. 59
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  48. 17
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  49. 30
      packages/nc-gui/components/smartsheet/grid/Table.vue
  50. 2
      packages/nc-gui/components/smartsheet/header/CellIcon.ts
  51. 2
      packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue
  52. 4
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  53. 8
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  54. 3
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  55. 6
      packages/nc-gui/components/smartsheet/toolbar/MappedBy.vue
  56. 16
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  57. 14
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  58. 4
      packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue
  59. 12
      packages/nc-gui/components/smartsheet/topbar/SelectMode.vue
  60. 3
      packages/nc-gui/components/tabs/Smartsheet.vue
  61. 7
      packages/nc-gui/components/template/Editor.vue
  62. 43
      packages/nc-gui/components/workspace/ProjectList.vue
  63. 5
      packages/nc-gui/composables/useAttachment.ts
  64. 8
      packages/nc-gui/composables/useColumnCreateStore.ts
  65. 9
      packages/nc-gui/composables/useCopySharedBase.ts
  66. 4
      packages/nc-gui/composables/useExpandedFormStore.ts
  67. 97
      packages/nc-gui/composables/useGridViewColumn.ts
  68. 10
      packages/nc-gui/composables/useTableNew.ts
  69. 557
      packages/nc-gui/composables/useViewColumns.ts
  70. 2
      packages/nc-gui/composables/useViewFilters.ts
  71. 4
      packages/nc-gui/composables/useViewGroupBy.ts
  72. 196
      packages/nc-gui/ee/assets/img/fieldPlaceholder.svg
  73. 19
      packages/nc-gui/helpers/parsers/CSVTemplateAdapter.ts
  74. 11
      packages/nc-gui/helpers/parsers/ExcelTemplateAdapter.ts
  75. 11
      packages/nc-gui/helpers/parsers/parserHelpers.ts
  76. 5
      packages/nc-gui/package.json
  77. 21
      packages/nc-gui/pages/copy-shared-base.vue
  78. 31
      packages/nc-gui/pages/index.vue
  79. 2
      packages/nc-gui/pages/index/[typeOrId]/[baseId]/index/index/[viewId]/[[viewTitle]].vue
  80. 6
      packages/nc-gui/store/config.ts
  81. 16
      packages/nc-gui/store/sidebar.ts
  82. 3
      packages/nc-gui/store/views.ts
  83. 18
      packages/nc-gui/utils/validation.ts
  84. 17
      packages/nc-gui/utils/workerUtils.ts
  85. 9
      packages/nc-gui/windi.config.ts
  86. 2
      packages/nc-lib-gui/package.json
  87. 30
      packages/noco-docs/docs/010.index.md
  88. 67
      packages/noco-docs/docs/020.getting-started/020.environment-variables.md
  89. 83
      packages/noco-docs/docs/020.getting-started/020.quick-start.md
  90. 46
      packages/noco-docs/docs/020.getting-started/030.terminologies.md
  91. 72
      packages/noco-docs/docs/020.getting-started/030.upgrading.md
  92. 113
      packages/noco-docs/docs/020.getting-started/040.keyboard-shortcuts.md
  93. 48
      packages/noco-docs/docs/020.getting-started/050.self-hosted/010.installation.md
  94. 69
      packages/noco-docs/docs/020.getting-started/050.self-hosted/020.environment-variables.md
  95. 149
      packages/noco-docs/docs/020.getting-started/050.self-hosted/030.upgrading.md
  96. 5
      packages/noco-docs/docs/020.getting-started/050.self-hosted/_category_.json
  97. 2
      packages/noco-docs/docs/020.getting-started/_category_.json
  98. 101
      packages/noco-docs/docs/030.setup-and-usages/010.dashboard.md
  99. 204
      packages/noco-docs/docs/030.setup-and-usages/020.table-operations.md
  100. 133
      packages/noco-docs/docs/030.setup-and-usages/030.column-operations.md
  101. Some files were not shown because too many files have changed in this diff Show More

14
.github/workflows/publish-api-docs.yml

@ -35,7 +35,7 @@ jobs:
env:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
with:
source_file: 'packages/nocodb/src/schema/swagger.json'
source_file: 'packages/nocodb/src/schema/swagger-v2.json'
destination_repo: 'nocodb/noco-apis-doc'
destination_folder: 'data-apis-v2'
user_email: 'oof1lab@gmail.com'
@ -53,3 +53,15 @@ jobs:
user_email: 'oof1lab@gmail.com'
user_name: 'o1lab'
commit_message: 'Autorelease from github.com/nocodb/nocodb'
- name: Pushes swagger file to meta-apis-v2
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658
env:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
with:
source_file: 'packages/nocodb/src/schema/swagger-v2.json'
destination_repo: 'nocodb/noco-apis-doc'
destination_folder: 'meta-apis-v2'
user_email: 'oof1lab@gmail.com'
user_name: 'o1lab'
commit_message: 'Autorelease from github.com/nocodb/nocodb'

6
.github/workflows/release-docker.yml

@ -90,10 +90,7 @@ jobs:
if: ${{ github.event.inputs.targetEnv == 'DEV' || inputs.targetEnv == 'DEV' }}
run: |
export NODE_OPTIONS="--max_old_space_size=16384"
NOCODB_SDK_PKG_NAME=nocodb-sdk
if [[ "${{ github.event.inputs.targetEnv == 'DEV' || inputs.targetEnv == 'DEV' }}" ]]; then
NOCODB_SDK_PKG_NAME=nocodb-sdk-daily
fi
NOCODB_SDK_PKG_NAME=nocodb-sdk-daily
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} targetVersion=${{ github.event.inputs.tag || inputs.tag }} node scripts/bumpNocodbSdkVersion.js &&
pnpm --filter=${NOCODB_SDK_PKG_NAME} install --ignore-scripts --no-frozen-lockfile && pnpm --filter=${NOCODB_SDK_PKG_NAME} run build &&
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} node scripts/upgradeNocodbSdk.js &&
@ -104,6 +101,7 @@ jobs:
- name: Build nocodb and docker files
run: |
pnpm install --ignore-scripts --no-frozen-lockfile
pnpm run docker:build
working-directory: ${{ env.working-directory }}

4
.github/workflows/release-npm.yml

@ -55,9 +55,11 @@ jobs:
- run: |
export NODE_OPTIONS="--max_old_space_size=16384"
NOCODB_SDK_PKG_NAME=nocodb-sdk
if [[ "${{ github.event.inputs.targetEnv == 'DEV' || inputs.targetEnv == 'DEV' }}" ]]; then
# If targetEnv is DEV, then use nocodb-sdk-daily package
if [[ ${{ github.event.inputs.targetEnv || inputs.targetEnv }} == 'DEV' ]]; then
NOCODB_SDK_PKG_NAME=nocodb-sdk-daily
fi
echo $NOCODB_SDK_PKG_NAME
targetEnv=${{ github.event.inputs.targetEnv || inputs.targetEnv }} targetVersion=${{ github.event.inputs.tag || inputs.tag }} node scripts/bumpNocodbSdkVersion.js &&
pnpm --filter=${NOCODB_SDK_PKG_NAME} install --ignore-scripts --no-frozen-lockfile && pnpm --filter=${NOCODB_SDK_PKG_NAME} run build && pnpm --filter=${NOCODB_SDK_PKG_NAME} publish --no-git-checks &&
sleep 90 &&

24
README.md

@ -14,7 +14,7 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart spreadshe
</p>
<div align="center">
[![Node version](https://img.shields.io/badge/node-%3E%3D%2018.14.0-brightgreen)](http://nodejs.org/download/)
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-green.svg)](https://conventionalcommits.org)
@ -210,22 +210,22 @@ Access Dashboard using: [http://localhost:8080/dashboard](http://localhost:8080/
# Table of Contents
- [Quick try](#quick-try)
- [NPX](#npx)
- [Node Application](#node-application)
- [Docker](#docker)
- [Docker Compose](#docker-compose)
- [NPX](#npx)
- [Node Application](#node-application)
- [Docker](#docker)
- [Docker Compose](#docker-compose)
- [GUI](#gui)
- [Join Our Community](#join-our-community)
- [Screenshots](#screenshots)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Rich Spreadsheet Interface](#rich-spreadsheet-interface)
- [App Store for Workflow Automations](#app-store-for-workflow-automations)
- [Programmatic Access](#programmatic-access)
- [Sync Schema](#sync-schema)
- [Audit](#audit)
- [Rich Spreadsheet Interface](#rich-spreadsheet-interface)
- [App Store for Workflow Automations](#app-store-for-workflow-automations)
- [Programmatic Access](#programmatic-access)
- [Sync Schema](#sync-schema)
- [Audit](#audit)
- [Production Setup](#production-setup)
- [Environment variables](#environment-variables)
- [Environment variables](#environment-variables)
- [Development Setup](#development-setup)
- [Contributing](#contributing)
- [Why are we building this?](#why-are-we-building-this)
@ -305,4 +305,4 @@ Thank you for your contributions! We appreciate all the contributions from the c
<a href="https://github.com/nocodb/nocodb/graphs/contributors">
<img src="https://contrib.rocks/image?repo=nocodb/nocodb" />
</a>
</a>

BIN
docker-compose/sqlite/nocodb/noco.db

Binary file not shown.

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

@ -603,7 +603,7 @@ input[type='number'] {
}
.nc-sidebar-node {
@apply !xs:(min-h-12 max-h-12 hover:bg-gray-50 ml-1.5);
@apply !xs:(min-h-12 max-h-12 hover:bg-gray-50 ml-1.5 w-[calc(100%-8px)]);
.nc-emoji {
@apply xs:(text-lg);

1
packages/nc-gui/assets/style/fonts.css

@ -2,6 +2,7 @@
@font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Manrope';
font-weight: 450 900;
src: url('./manrope/Manrope-VariableFont_wght.ttf') format('truetype')
}

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

@ -150,7 +150,6 @@ declare module '@vue/runtime-core' {
MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default']
MdiShieldKeyOutline: typeof import('~icons/mdi/shield-key-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiStar: typeof import('~icons/mdi/star')['default']
MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']

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

@ -91,7 +91,11 @@ useSelectedCellKeyupListener(active, (e) => {
}"
@click="onClick(false, $event)"
>
<div class="items-center" :class="{ 'w-full justify-start': isEditColumnMenu || isGallery || isForm }" @click="onClick(true)">
<div
class="items-center"
:class="{ 'w-full justify-start': isEditColumnMenu || isGallery || isForm, 'py-2': isEditColumnMenu }"
@click="onClick(true)"
>
<Transition name="layout" mode="out-in" :duration="100">
<component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"

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

@ -6,6 +6,7 @@ import {
IsFormInj,
JsonExpandInj,
ReadonlyInj,
RowHeightInj,
computed,
inject,
ref,
@ -46,6 +47,8 @@ const _isExpanded = inject(JsonExpandInj, ref(false))
const isExpanded = ref(false)
const rowHeight = inject(RowHeightInj, ref(undefined))
const localValue = computed<string | Record<string, any> | undefined>({
get: () => localValueState.value,
set: (val: undefined | string | Record<string, any>) => {
@ -140,6 +143,13 @@ useSelectedCellKeyupListener(active, (e) => {
}
})
const inputWrapperRef = ref<HTMLElement | null>(null)
onClickOutside(inputWrapperRef, (e) => {
if ((e.target as HTMLElement)?.closest('.nc-json-action')) return
editEnabled.value = false
})
watch(isExpanded, () => {
_isExpanded.value = isExpanded.value
})
@ -148,7 +158,7 @@ watch(isExpanded, () => {
<template>
<component :is="isExpanded ? NcModal : 'div'" v-model:visible="isExpanded" :closable="false" centered :footer="null">
<div v-if="editEnabled && !readonly" class="flex flex-col w-full" @mousedown.stop @mouseup.stop @click.stop>
<div class="flex flex-row justify-between pt-1 pb-2" @mousedown.stop>
<div class="flex flex-row justify-between pt-1 pb-2 nc-json-action" @mousedown.stop>
<a-button type="text" size="small" @click="isExpanded = !isExpanded">
<CilFullscreenExit v-if="isExpanded" class="h-2.5" />
@ -167,6 +177,7 @@ watch(isExpanded, () => {
</div>
<LazyMonacoEditor
ref="inputWrapperRef"
:model-value="localValue || ''"
class="min-w-full w-80"
:class="{ 'expanded-editor': isExpanded, 'editor': !isExpanded }"
@ -182,7 +193,7 @@ watch(isExpanded, () => {
<span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<span v-else>{{ vModel }}</span>
<LazyCellClampedText v-else :value="vModel" :lines="rowHeight" />
</component>
</template>

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

@ -43,6 +43,8 @@ const { modelValue, disableOptionCreation } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { isMobileMode } = useGlobal()
const column = inject(ColumnInj)!
const readOnly = inject(ReadonlyInj)!
@ -383,7 +385,7 @@ const selectedOpts = computed(() => {
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:bordered="false"
clear-icon
show-search
:show-search="!isMobileMode"
:show-arrow="editAllowed && !(readOnly || isLockedMode)"
:open="isOpen && editAllowed"
:disabled="readOnly || !editAllowed || isLockedMode"
@ -392,6 +394,9 @@ const selectedOpts = computed(() => {
@search="search"
@keydown.stop
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700 nc-select-expand-btn" />
</template>
<a-select-option
v-for="op of options"
:key="op.id || op.title"

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

@ -37,6 +37,8 @@ const { modelValue, disableOptionCreation } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { isMobileMode } = useGlobal()
const column = inject(ColumnInj)!
const readOnly = inject(ReadonlyInj)!
@ -202,6 +204,8 @@ async function addIfMissingAndSave() {
}
const search = () => {
if (isMobileMode.value) return
searchVal.value = aselect.value?.$el?.querySelector('.ant-select-selection-search-input')?.value
}
@ -285,7 +289,7 @@ const selectedOpt = computed(() => {
v-else
ref="aselect"
v-model:value="vModel"
class="w-full overflow-hidden"
class="w-full overflow-hidden xs:min-h-12"
:class="{ 'caret-transparent': !hasEditRoles }"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:allow-clear="!column.rqd && editAllowed"
@ -294,7 +298,7 @@ const selectedOpt = computed(() => {
:disabled="readOnly || !editAllowed || isLockedMode"
:show-arrow="hasEditRoles && !(readOnly || isLockedMode) && active && vModel === null"
:dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen && active ? 'active' : ''}`"
:show-search="isOpen && active"
:show-search="!isMobileMode && isOpen && active"
@select="onSelect"
@keydown="onKeydown($event)"
@search="search"

2
packages/nc-gui/components/cell/attachment/Carousel.vue

@ -55,7 +55,7 @@ useEventListener(container, 'click', (e) => {
<template>
<GeneralOverlay v-model="selectedImage" :z-index="1001" class="bg-gray-500 bg-opacity-50">
<template v-if="selectedImage">
<div ref="container" class="overflow-hidden p-12 text-center relative">
<div ref="container" class="overflow-hidden p-12 text-center relative xs:h-screen">
<div class="text-white group absolute top-5 right-5">
<component
:is="iconMap.closeCircle"

42
packages/nc-gui/components/cell/attachment/index.vue

@ -152,14 +152,18 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
const rowHeight = inject(RowHeightInj, ref())
const open = () => {
if (isMobileMode.value) return (isExpandedForm.value = true)
const open = (e: Event) => {
e.stopPropagation()
_open()
}
const openAttachment = (item: any) => {
if (isMobileMode.value) return
if (isMobileMode.value && !isExpandedForm.value) {
isExpandedForm.value = true
return
}
_openAttachment(item)
}
@ -169,6 +173,14 @@ const onExpand = () => {
modalVisible.value = true
}
const onImageClick = (item: any) => {
if (isMobileMode.value && !isExpandedForm.value) return
if (!isMobileMode.value && (isGallery.value || (isKanban.value && !isExpandedForm.value))) return
selectedImage.value = item
}
</script>
<template>
@ -178,7 +190,7 @@ const onExpand = () => {
:style="{
height: isForm || isExpandedForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
}"
class="nc-attachment-cell relative flex color-transition flex items-center w-full"
class="nc-attachment-cell relative flex color-transition flex items-center w-full xs:(min-h-12 max-h-32)"
:class="{ 'justify-center': !active, 'justify-between': active }"
>
<LazyCellAttachmentCarousel />
@ -198,26 +210,29 @@ const onExpand = () => {
<div
v-if="!isReadonly"
:class="{ 'mx-auto px-4': !visibleItems.length }"
:class="{ 'sm:(mx-auto px-4) xs:(w-full min-w-8)': !visibleItems.length }"
class="group cursor-pointer py-1 flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
data-testid="attachment-cell-file-picker-button"
@click.stop="open"
@click="open"
>
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<NcTooltip placement="bottom">
<NcTooltip placement="bottom" class="xs:w-full">
<template #title
><span data-rec="true">{{ $t('activity.attachmentDrop') }} </span></template
>
<div v-if="active || !visibleItems.length || (isForm && visibleItems.length)" class="flex items-center gap-1">
<div
v-if="active || !visibleItems.length || (isForm && visibleItems.length)"
class="flex items-center gap-1 xs:(w-full min-w-12 h-8 justify-center)"
>
<MaterialSymbolsAttachFile
class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]"
/>
<div
v-if="!visibleItems.length"
data-rec="true"
class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs"
class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs xs:(justify-center rounded-lg text-sm)"
>
{{ $t('activity.addFiles') }}
</div>
@ -245,12 +260,7 @@ const onExpand = () => {
<div
class="nc-attachment flex items-center flex-col flex-wrap justify-center"
:class="{ 'ml-2': active }"
@click="
() => {
if (isGallery || isMobileMode || (isKanban && !isExpandedForm)) return
selectedImage = item
}
"
@click="() => onImageClick(item)"
>
<LazyCellAttachmentImage
:alt="item.title || `#${i}`"
@ -281,7 +291,7 @@ const onExpand = () => {
<div
v-if="active || (isForm && visibleItems.length)"
class="h-6 w-5 group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
class="xs:hidden h-6 w-5 group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
>
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />

4
packages/nc-gui/components/dashboard/Sidebar.vue

@ -47,14 +47,14 @@ onUnmounted(() => {
</div>
<div
ref="treeViewDom"
class="flex flex-col nc-scrollbar-dark-md flex-grow xs:(border-transparent pt-2)"
class="flex flex-col nc-scrollbar-dark-md flex-grow xs:(border-transparent pt-2 pr-2)"
:class="{
'border-t-1': !isSharedBase,
'border-transparent': !isTreeViewOnScrollTop,
'pt-0.25': isSharedBase,
}"
>
<LazyDashboardTreeView v-if="!isWorkspaceLoading" />
<DashboardTreeView v-if="!isWorkspaceLoading" />
</div>
<div v-if="!isSharedBase">
<DashboardSidebarUserInfo />

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

@ -57,7 +57,7 @@ const basesStore = useBases()
const { isMobileMode } = useGlobal()
const { loadProjects, createProject: _createProject, updateProject, getProjectMetaInfo } = basesStore
const { createProject: _createProject, updateProject, getProjectMetaInfo } = basesStore
const { bases } = storeToRefs(basesStore)
@ -65,7 +65,7 @@ const { loadProjectTables } = useTablesStore()
const { activeTable } = storeToRefs(useTablesStore())
const { appInfo, navigateToProject } = useGlobal()
const { appInfo } = useGlobal()
const { orgRoles, isUIAllowed } = useRoles()
@ -351,46 +351,6 @@ const duplicateProject = (base: BaseType) => {
selectedProjectToDuplicate.value = base
isDuplicateDlgOpen.value = true
}
const { $poller } = useNuxtApp()
const DlgProjectDuplicateOnOk = async (jobData: { id: string; base_id: string }) => {
await loadProjects('workspace')
$poller.subscribe(
{ id: jobData.id },
async (data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
if (data.status === JobStatus.COMPLETED) {
await loadProjects('workspace')
const base = bases.value.get(jobData.base_id)
// open base after duplication
if (base) {
await navigateToProject({
baseId: base.id,
type: base.type,
})
}
} else if (data.status === JobStatus.FAILED) {
message.error('Failed to duplicate base')
await loadProjects('workspace')
}
}
},
)
$e('a:base:duplicate')
}
const tableDelete = () => {
isTableDeleteDialogVisible.value = true
@ -477,7 +437,7 @@ const projectDelete = () => {
</span>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(base)"></div>
<NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']">
<NcDropdown v-if="!isSharedBase" v-model:visible="isOptionsOpen" :trigger="['click']">
<NcButton
v-e="['c:base:options']"
class="nc-sidebar-node-btn"
@ -554,7 +514,7 @@ const projectDelete = () => {
@click.stop="
() => {
$e('c:base:api-docs')
openLink(`/api/v1/meta/bases/${base.id}/swagger`, appInfo.ncSiteUrl)
openLink(`/api/v1/db/meta/projects/${base.id}/swagger`, appInfo.ncSiteUrl)
}
"
>
@ -791,12 +751,7 @@ const projectDelete = () => {
:base-id="base?.id"
/>
<DlgProjectDelete v-model:visible="isProjectDeleteDialogVisible" :base-id="base?.id" />
<DlgProjectDuplicate
v-if="selectedProjectToDuplicate"
v-model="isDuplicateDlgOpen"
:base="selectedProjectToDuplicate"
:on-ok="DlgProjectDuplicateOnOk"
/>
<DlgProjectDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
<GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]">
<LazyDashboardSettingsErd :source-id="activeBaseId" />

2
packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -182,7 +182,7 @@ const isTableOpened = computed(() => {
:class="{ '!rotate-180': isExpanded }"
/>
</NcButton>
<div v-else class="min-w-5.75"></div>
<div v-else class="sm:min-w-5.75 xs:min-w-7.5 h-2"></div>
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<div
class="flex items-center nc-table-icon"

50
packages/nc-gui/components/dashboard/TreeView/index.vue

@ -1,11 +1,8 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import ProjectWrapper from './ProjectWrapper.vue'
import type { TabType } from '#imports'
import {
TreeViewInj,
computed,
@ -22,16 +19,13 @@ import {
useNuxtApp,
useRoles,
useTablesStore,
useTabs,
} from '#imports'
import { useRouter } from '#app'
const { isUIAllowed } = useRoles()
const { addTab } = useTabs()
const { $e, $poller } = useNuxtApp()
const { $e } = useNuxtApp()
const router = useRouter()
@ -45,22 +39,14 @@ const { bases, basesList, activeProjectId } = storeToRefs(basesStore)
const { isWorkspaceLoading } = storeToRefs(useWorkspace())
const { openTable } = useTablesStore()
const baseCreateDlg = ref(false)
const baseStore = useBase()
const { loadTables } = baseStore
const { tables, isSharedBase } = storeToRefs(baseStore)
const { t } = useI18n()
const { isSharedBase } = storeToRefs(baseStore)
const { activeTable: _activeTable } = storeToRefs(useTablesStore())
const { refreshCommandPalette } = useCommandPalette()
const contextMenuTarget = reactive<{ type?: 'base' | 'source' | 'table' | 'main' | 'layout'; value?: any }>({})
const setMenuContext = (type: 'base' | 'source' | 'table' | 'main' | 'layout', value?: any) => {
@ -120,38 +106,6 @@ const duplicateTable = async (table: TableType) => {
const { close } = useDialog(resolveComponent('DlgTableDuplicate'), {
'modelValue': isOpen,
'table': table,
'onOk': async (jobData: { id: string }) => {
$poller.subscribe(
{ id: jobData.id },
async (data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
if (data.status === JobStatus.COMPLETED) {
await loadTables()
refreshCommandPalette()
const newTable = tables.value.find((el) => el.id === data?.data?.result?.id)
if (newTable) addTab({ title: newTable.title, id: newTable.id, type: newTable.type as TabType })
openTable(newTable!)
} else if (data.status === JobStatus.FAILED) {
message.error(t('msg.error.failedToDuplicateTable'))
await loadTables()
}
}
},
)
$e('a:table:duplicate')
},
'onUpdate:modelValue': closeDialog,
})

11
packages/nc-gui/components/dashboard/View.vue

@ -111,6 +111,14 @@ watch(isMobileMode, () => {
isLeftSidebarOpen.value = !isMobileMode.value
})
watch(sidebarState, () => {
if (sidebarState.value === 'peekCloseEnd') {
setTimeout(() => {
sidebarState.value = 'hiddenEnd'
}, animationDuration)
}
})
onMounted(() => {
handleSidebarOpenOnMobileForNonViews()
})
@ -132,7 +140,7 @@ onMounted(() => {
>
<div
ref="wrapperRef"
class="nc-sidebar-wrapper relative flex flex-col h-full justify-center !min-w-32 absolute overflow-visible"
class="nc-sidebar-wrapper relative flex flex-col h-full justify-center !min-w-12 absolute overflow-visible"
:class="{
'mobile': isMobileMode,
'minimized-height': !isLeftSidebarOpen,
@ -171,6 +179,7 @@ onMounted(() => {
> * {
@apply opacity-0;
z-index: -1 !important;
transform: translateX(-100%);
}
}

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

@ -125,14 +125,14 @@ async function createOrUpdate() {
const { id, ...payload } = syncSource.value
if (id !== '') {
await $fetch(`/api/v1/meta/syncs/${id}`, {
await $fetch(`/api/v1/db/meta/syncs/${id}`, {
baseURL,
method: 'PATCH',
headers: { 'xc-auth': $state.token.value as string },
body: payload,
})
} else {
syncSource.value = await $fetch(`/api/v1/meta/bases/${baseId}/syncs/${sourceId}`, {
syncSource.value = await $fetch(`/api/v1/db/meta/projects/${baseId}/syncs/${sourceId}`, {
baseURL,
method: 'POST',
headers: { 'xc-auth': $state.token.value as string },
@ -144,12 +144,12 @@ async function createOrUpdate() {
}
}
async function listenForUpdates() {
async function listenForUpdates(id?: string) {
if (listeningForUpdates.value) return
listeningForUpdates.value = true
const job = await $api.jobs.status({ syncId: syncSource.value.id })
const job = id ? { id } : await $api.jobs.status({ syncId: syncSource.value.id })
if (!job) {
listeningForUpdates.value = false
@ -184,7 +184,7 @@ async function listenForUpdates() {
}
async function loadSyncSrc() {
const data: any = await $fetch(`/api/v1/meta/bases/${baseId}/syncs/${sourceId}`, {
const data: any = await $fetch(`/api/v1/db/meta/projects/${baseId}/syncs/${sourceId}`, {
baseURL,
method: 'GET',
headers: { 'xc-auth': $state.token.value as string },
@ -226,12 +226,12 @@ async function loadSyncSrc() {
async function sync() {
try {
await $fetch(`/api/v1/meta/syncs/${syncSource.value.id}/trigger`, {
const jobData: any = await $fetch(`/api/v1/db/meta/syncs/${syncSource.value.id}/trigger`, {
baseURL,
method: 'POST',
headers: { 'xc-auth': $state.token.value as string },
})
listenForUpdates()
listenForUpdates(jobData.id)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -245,12 +245,15 @@ async function abort() {
"This is a highly experimental feature and only marks job as not started, please don't abort the job unless you are sure job is stuck.",
onOk: async () => {
try {
await $fetch(`/api/v1/meta/syncs/${syncSource.value.id}/abort`, {
await $fetch(`/api/v1/db/meta/syncs/${syncSource.value.id}/abort`, {
baseURL,
method: 'POST',
headers: { 'xc-auth': $state.token.value as string },
})
step.value = 1
progress.value = []
goBack.value = false
enableAbort.value = false
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -258,6 +261,13 @@ async function abort() {
})
}
function cancel() {
step.value = 1
progress.value = []
goBack.value = false
enableAbort.value = false
}
function migrateSync(src: any) {
if (!src.details?.options) {
src.details.options = {
@ -456,7 +466,7 @@ onMounted(async () => {
<a-button v-if="showGoToDashboardButton" class="mt-4" size="large" @click="dialogShow = false">
{{ $t('labels.goToDashboard') }}
</a-button>
<a-button v-else-if="goBack" class="mt-4 uppercase" size="large" danger @click="step = 1">{{
<a-button v-else-if="goBack" class="mt-4 uppercase" size="large" danger @click="cancel()">{{
$t('general.cancel')
}}</a-button>
<a-button v-else-if="enableAbort" class="mt-4 uppercase" size="large" danger @click="abort()">{{

77
packages/nc-gui/components/dlg/ProjectDuplicate.vue

@ -1,12 +1,11 @@
<script setup lang="ts">
import tinycolor from 'tinycolor2'
import type { BaseType } from 'nocodb-sdk'
import { useVModel } from '#imports'
import { isEeUI, useVModel } from '#imports'
const props = defineProps<{
modelValue: boolean
base: BaseType
onOk: (jobData: { name: string; id: string }) => Promise<void>
}>()
const emit = defineEmits(['update:modelValue'])
@ -15,6 +14,15 @@ const { refreshCommandPalette } = useCommandPalette()
const { api } = useApi()
const { $e, $poller } = useNuxtApp()
const basesStore = useBases()
const { loadProjects, createProject: _createProject } = basesStore
const { bases } = storeToRefs(basesStore)
const { navigateToProject } = useGlobal()
const dialogShow = useVModel(props, 'modelValue', emit)
const options = ref({
@ -57,12 +65,51 @@ const _duplicate = async () => {
}),
},
})
props.onOk(jobData as any)
$poller.subscribe(
{ id: jobData.id },
async (data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
if (data.status === JobStatus.COMPLETED) {
await loadProjects('workspace')
const base = bases.value.get(jobData.base_id)
// open project after duplication
if (base) {
await navigateToProject({
workspaceId: isEeUI ? base.fk_workspace_id : undefined,
baseId: base.id,
type: base.type,
})
}
refreshCommandPalette()
isLoading.value = false
dialogShow.value = false
} else if (data.status === JobStatus.FAILED) {
message.error('Failed to duplicate project')
await loadProjects('workspace')
refreshCommandPalette()
isLoading.value = false
dialogShow.value = false
}
}
},
)
$e('a:base:duplicate')
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
refreshCommandPalette()
dialogShow.value = false
}
}
@ -78,7 +125,15 @@ const isEaster = ref(false)
</script>
<template>
<GeneralModal v-if="base" v-model:visible="dialogShow" class="!w-[30rem]" wrap-class-name="nc-modal-base-duplicate">
<GeneralModal
v-if="base"
v-model:visible="dialogShow"
:closable="!isLoading"
:mask-closable="!isLoading"
:keyboard="!isLoading"
class="!w-[30rem]"
wrap-class-name="nc-modal-base-duplicate"
>
<div>
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster">
{{ $t('general.duplicate') }} {{ $t('objects.project') }}
@ -91,13 +146,15 @@ const isEaster = ref(false)
<a-divider class="!m-0 !p-0 !my-2" />
<div class="text-xs p-2">
<a-checkbox v-model:checked="options.includeData">{{ $t('labels.includeData') }}</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">{{ $t('labels.includeView') }}</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">{{ $t('labels.includeWebhook') }}</a-checkbox>
<a-checkbox v-model:checked="options.includeData" :disabled="isLoading">{{ $t('labels.includeData') }}</a-checkbox>
<a-checkbox v-model:checked="options.includeViews" :disabled="isLoading">{{ $t('labels.includeView') }}</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks" :disabled="isLoading">
{{ $t('labels.includeWebhook') }}
</a-checkbox>
</div>
</div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton v-if="!isLoading" key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" v-e="['a:base:duplicate']" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>

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

@ -149,10 +149,12 @@ const dialogShow = useVModel(rest, 'modelValue', emit)
if (isWorkerSupport) {
watch(
dialogShow,
(val) => {
async (val) => {
if (val) {
importWorker = initWorker(importWorkerUrl)
} else importWorker?.terminate()
importWorker = await initWorker(importWorkerUrl)
} else {
importWorker?.terminate()
}
},
{ immediate: true },
)

138
packages/nc-gui/components/dlg/SharedBaseDuplicate.vue

@ -0,0 +1,138 @@
<script setup lang="ts">
import { ProjectTypes } from 'nocodb-sdk'
import { isEeUI, useApi, useVModel, useWorkspace } from '#imports'
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const { api } = useApi()
const { sharedBaseId } = useCopySharedBase()
const workspaceStore = useWorkspace()
const { populateWorkspace } = workspaceStore
const { workspacesList } = storeToRefs(workspaceStore)
const { ncNavigateTo } = useGlobal()
const dialogShow = useVModel(props, 'modelValue', emit)
const options = ref({
includeData: true,
includeViews: true,
})
const optionsToExclude = computed(() => {
const { includeData, includeViews } = options.value
return {
excludeData: !includeData,
excludeViews: !includeViews,
}
})
const isLoading = ref(false)
const selectedWorkspace = ref<string>()
const { $e, $poller } = useNuxtApp()
const _duplicate = async () => {
if (!selectedWorkspace.value && isEeUI) return
try {
isLoading.value = true
const jobData = await api.base.duplicateShared(selectedWorkspace.value ?? 'nc', sharedBaseId.value, {
options: optionsToExclude.value,
base: isEeUI
? {
fk_workspace_id: selectedWorkspace.value,
type: ProjectTypes.DATABASE,
}
: {},
})
sharedBaseId.value = null
$poller.subscribe(
{ id: jobData.id },
async (data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
if (data.status === JobStatus.COMPLETED) {
console.log('job completed', jobData)
await ncNavigateTo({
...(isEeUI ? { workspaceId: jobData.fk_workspace_id } : {}),
baseId: jobData.base_id,
})
isLoading.value = false
dialogShow.value = false
} else if (data.status === JobStatus.FAILED) {
message.error('Failed to duplicate shared base')
await populateWorkspace()
isLoading.value = false
dialogShow.value = false
}
}
},
)
$e('a:base:duplicate-shared-base')
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
isLoading.value = false
dialogShow.value = false
}
}
</script>
<template>
<GeneralModal v-model:visible="dialogShow" class="!w-[30rem]" wrap-class-name="nc-modal-project-duplicate">
<div>
<div class="prose-xl font-bold self-center">{{ $t('general.duplicate') }} {{ $t('labels.sharedBase') }}</div>
<template v-if="isEeUI">
<div class="my-4">Select workspace to duplicate shared base to:</div>
<NcSelect
v-model:value="selectedWorkspace"
class="w-full"
:options="workspacesList.map((w) => ({ label: `${w.title[0].toUpperCase()}${w.title.slice(1)}`, value: w.id }))"
placeholder="Select Workspace"
/>
</template>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>
<a-divider class="!m-0 !p-0 !my-2" />
<div class="text-xs p-2">
<a-checkbox v-model:checked="options.includeData">{{ $t('labels.includeData') }}</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">{{ $t('labels.includeView') }}</a-checkbox>
</div>
</div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
key="submit"
v-e="['a:shared-base:duplicate']"
:loading="isLoading"
:disabled="!selectedWorkspace && isEeUI"
@click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>
</div>
</GeneralModal>
</template>

75
packages/nc-gui/components/dlg/TableDuplicate.vue

@ -1,11 +1,12 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { useVModel } from '#imports'
import type { TabType } from '#imports'
const props = defineProps<{
modelValue: boolean
table: TableType
onOk: (jobData: { name: string; id: string }) => Promise<void>
}>()
const emit = defineEmits(['update:modelValue'])
@ -14,6 +15,26 @@ const { api } = useApi()
const dialogShow = useVModel(props, 'modelValue', emit)
const { addTab } = useTabs()
const { $e, $poller } = useNuxtApp()
const basesStore = useBases()
const { createProject: _createProject } = basesStore
const { openTable } = useTablesStore()
const baseStore = useBase()
const { loadTables } = baseStore
const { tables } = storeToRefs(baseStore)
const { t } = useI18n()
const { activeTable: _activeTable } = storeToRefs(useTablesStore())
const { refreshCommandPalette } = useCommandPalette()
const options = ref({
@ -37,13 +58,45 @@ const _duplicate = async () => {
try {
isLoading.value = true
const jobData = await api.dbTable.duplicate(props.table.base_id!, props.table.id!, { options: optionsToExclude.value })
props.onOk(jobData as any)
$poller.subscribe(
{ id: jobData.id },
async (data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
if (data.status === JobStatus.COMPLETED) {
await loadTables()
refreshCommandPalette()
const newTable = tables.value.find((el) => el.id === data?.data?.result?.id)
if (newTable) addTab({ title: newTable.title, id: newTable.id, type: newTable.type as TabType })
openTable(newTable!)
isLoading.value = false
dialogShow.value = false
} else if (data.status === JobStatus.FAILED) {
message.error(t('msg.error.failedToDuplicateTable'))
await loadTables()
isLoading.value = false
dialogShow.value = false
}
}
},
)
$e('a:table:duplicate')
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
dialogShow.value = false
refreshCommandPalette()
}
}
@ -61,10 +114,12 @@ const isEaster = ref(false)
<GeneralModal
v-model:visible="dialogShow"
:class="{ active: dialogShow }"
:closable="!isLoading"
:mask-closable="!isLoading"
:keyboard="!isLoading"
centered
wrap-class-name="nc-modal-table-duplicate"
:footer="null"
:closable="false"
class="!w-[30rem]"
@keydown.esc="dialogShow = false"
>
@ -80,13 +135,15 @@ const isEaster = ref(false)
<a-divider class="!m-0 !p-0 !my-2" />
<div class="text-xs p-2">
<a-checkbox v-model:checked="options.includeData">{{ $t('labels.includeData') }}</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">{{ $t('labels.includeView') }}</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">{{ $t('labels.includeWebhook') }}</a-checkbox>
<a-checkbox v-model:checked="options.includeData" :disabled="isLoading">{{ $t('labels.includeData') }}</a-checkbox>
<a-checkbox v-model:checked="options.includeViews" :disabled="isLoading">{{ $t('labels.includeView') }}</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks" :disabled="isLoading">
{{ $t('labels.includeWebhook') }}
</a-checkbox>
</div>
</div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton v-if="!isLoading" key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" v-e="['a:table:duplicate']" type="primary" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>

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

@ -10,7 +10,7 @@ const { base } = storeToRefs(useBase())
const route = useRoute()
const openSwaggerLink = () => {
openLink(`./api/v1/meta/bases/${route.params.baseId}/swagger`, appInfo.value.ncSiteUrl)
openLink(`./api/v1/db/meta/projects/${route.params.baseId}/swagger`, appInfo.value.ncSiteUrl)
}
</script>

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

@ -6,7 +6,7 @@ import { iconMap } from '#imports'
<a
v-e="['c:navbar:join-cloud']"
class="flex !no-underline"
href="https://docs.google.com/forms/d/e/1FAIpQLSfKLe8Rcrq0uo2_jM5W1kbVBbzDiQ3IvlP8Iov61FTekVAvzA/viewform?usp=pp_url"
href="https://app.nocodb.com/#/signin?utm_source=OSS&utm_medium=OSS&utm_campaign=OSS&utm_content=OSS"
>
<div
class="flex justify-center items-center rounded-l-[3px] w-full cursor-pointer px-2 py-1 !text-current !no-underline text-primary border-1 border-[#cdd1d6] bg-[#EFF2F6] hover:bg-[#e9ebef] m-0"

13
packages/nc-gui/components/general/ShareProject.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { isDrawerOrModalExist, isEeUI, isMac, useNuxtApp, useRoles } from '#imports'
import { isDrawerOrModalExist, isMac, useNuxtApp, useRoles } from '#imports'
interface Props {
disabled?: boolean
@ -13,6 +13,7 @@ const { isMobileMode, getMainUrl } = useGlobal()
const { visibility, showShareModal } = storeToRefs(useShare())
const { activeTable } = storeToRefs(useTablesStore())
const { base, isSharedBase } = storeToRefs(useBase())
const { $e } = useNuxtApp()
@ -71,7 +72,7 @@ const copySharedBase = async () => {
</NcButton>
</div>
<template v-else-if="isSharedBase && isEeUI">
<template v-else-if="isSharedBase">
<div class="flex-1"></div>
<div class="flex flex-col justify-center h-full">
<div class="flex flex-row items-center w-full">
@ -91,11 +92,3 @@ const copySharedBase = async () => {
<LazyDlgShareAndCollaborateView :is-view-toolbar="isViewToolbar" />
</template>
<style lang="scss">
.share-status-tootltip {
.ant-tooltip-inner {
@apply !rounded-md !border-1 !border-gray-200;
}
}
</style>

8
packages/nc-gui/components/general/WorkspaceIcon.vue

@ -8,9 +8,11 @@ const props = defineProps<{
size?: 'small' | 'medium' | 'large'
}>()
const workspaceColor = computed(() =>
props.workspace ? props.workspace.meta?.color || stringToColor(props.workspace.id!) : undefined,
)
const workspaceColor = computed(() => {
const color = props.workspace ? props.workspace.meta?.color || stringToColor(props.workspace.id!) : undefined
return color || '#0A1433'
})
const size = computed(() => props.size || 'medium')
</script>

7
packages/nc-gui/components/nc/Button.vue

@ -20,6 +20,7 @@ interface Props {
type?: ButtonType | 'danger' | 'secondary' | undefined
size?: NcButtonSize
centered?: boolean
iconOnly?: boolean
}
const props = withDefaults(defineProps<Props>(), {
@ -51,10 +52,8 @@ const onFocus = (e: FocusEvent) => {
isFocused.value = false
} else {
const relatedTarget = e.relatedTarget as HTMLElement | null
const focusFromModal =
relatedTarget?.classList?.contains('ant-modal-wrap') || relatedTarget?.classList?.contains('ant-modal-wrap')
isFocused.value = !focusFromModal
isFocused.value = !!relatedTarget
}
isClicked.value = false
@ -107,7 +106,7 @@ useEventListener(NcButton, 'mousedown', () => {
<slot v-else name="icon" />
<div
v-if="!(size === 'xxsmall' && loading)"
v-if="!(size === 'xxsmall' && loading) && !props.iconOnly"
class="flex flex-row items-center"
:class="{
'font-medium': type === 'primary' || type === 'danger',

2
packages/nc-gui/components/nc/Pagination.vue

@ -4,7 +4,7 @@ const props = defineProps<{
total: number
pageSize: number
entityName?: string
mode: 'simple' | 'full'
mode?: 'simple' | 'full'
}>()
const emits = defineEmits(['update:current', 'update:pageSize'])

10
packages/nc-gui/components/nc/Select.vue

@ -55,6 +55,16 @@ const onChange = (value: string) => {
</template>
<style lang="scss">
.ant-select-item {
@apply !xs:h-13;
}
.ant-select-item-option-content {
@apply !xs:mt-2.5;
}
.ant-select-item-option-state {
@apply !xs:mt-1.75;
}
.nc-select.ant-select {
height: fit-content;
.ant-select-selector {

2
packages/nc-gui/components/shared-view/Gallery.vue

@ -17,6 +17,8 @@ provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
</script>

6
packages/nc-gui/components/shared-view/Grid.vue

@ -37,7 +37,7 @@ provide(FieldsInj, columns)
provide(IsPublicInj, ref(true))
provide(IsLockedInj, isLocked)
const { loadGridViewColumns } = useProvideGridViewColumn(sharedView, true)
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
if (signedIn.value) {
try {
@ -55,10 +55,6 @@ watch(
immediate: true,
},
)
onMounted(async () => {
await loadGridViewColumns()
})
</script>
<template>

2
packages/nc-gui/components/shared-view/Kanban.vue

@ -25,6 +25,8 @@ provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
useProvideKanbanViewStore(meta, sharedView, true)

2
packages/nc-gui/components/shared-view/Map.vue

@ -25,6 +25,8 @@ provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
useProvideMapViewStore(meta, sharedView, true)

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

@ -84,7 +84,7 @@ const selectedLangName = ref(langs[0].name)
const apiUrl = computed(
() =>
new URL(
`/api/v1/data/noco/${base.value.id}/${meta.value?.title}/views/${view.value?.title}`,
`/api/v1/db/data/noco/${base.value.id}/${meta.value?.title}/views/${view.value?.title}`,
(appInfo.value && appInfo.value.ncSiteUrl) || '/',
).href,
)

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

@ -33,7 +33,7 @@ const openedSubTab = computed({
watch(openedSubTab, () => {
// TODO: Find a good way to know when the roles are populated and check
// Re-enable this check for first render
if (openedSubTab.value === 'field' && !isUIAllowed('hookList')) {
if (openedSubTab.value === 'field' && !isUIAllowed('fieldAdd')) {
onViewsTabChange('relation')
}
if (openedSubTab.value === 'webhook' && !isUIAllowed('hookList')) {

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

@ -23,7 +23,7 @@ import {
useI18n,
useNuxtApp,
useRoles,
useViewColumns,
useViewColumnsOrThrow,
useViewData,
watch,
} from '#imports'
@ -58,16 +58,14 @@ const isPublic = inject(IsPublicInj, ref(false))
const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view)
const reloadEventHook = createEventHook<boolean | void>()
provide(ReloadViewDataHookInj, reloadEventHook)
const reloadEventHook = inject(ReloadViewDataHookInj, createEventHook<boolean | void>())
reloadEventHook.on(async () => {
await loadFormView()
setFormData()
})
const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta, async () => reloadEventHook.trigger())
const { showAll, hideAll, saveOrUpdate } = useViewColumnsOrThrow()
const { syncLTARRefs, row } = useProvideSmartsheetRowStore(
meta,

13
packages/nc-gui/components/smartsheet/Pagination.vue

@ -14,6 +14,7 @@ interface Props {
fixedSize?: number
extraStyle?: string
showApiTiming?: boolean
alignLeft?: boolean
}
const props = defineProps<Props>()
@ -34,6 +35,8 @@ const extraStyle = toRef(props, 'extraStyle')
const isGroupBy = inject(IsGroupByInj, ref(false))
const alignLeft = computed(() => props.alignLeft ?? false)
const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
@ -69,7 +72,12 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
isGroupBy ? 'margin-top:1px; border-radius: 0 0 12px 12px !important;' : ''
}${extraStyle}`"
>
<div class="flex-1 flex items-center">
<div
class="flex items-center"
:class="{
'flex-1': !alignLeft,
}"
>
<slot name="add-record" />
<span
v-if="!alignCountOnRight && count !== null && count !== Infinity"
@ -84,7 +92,8 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
v-if="!hidePagination"
class="transition-all duration-350"
:class="{
'-ml-17': isLeftSidebarOpen,
'-ml-17': isLeftSidebarOpen && !alignLeft,
'ml-8': alignLeft,
}"
>
<div v-if="isPaginationLoading" class="flex flex-row justify-center item-center min-h-10 min-w-42">

13
packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue

@ -10,13 +10,7 @@ const props = defineProps<{
const emit = defineEmits(['update:modelValue'])
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const { fields, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
const { fields, metaColumnById } = useViewColumnsOrThrow()
const vModel = useVModel(props, 'modelValue', emit)
@ -59,11 +53,12 @@ onMounted(() => {
...vModel.value.meta,
}
vModel.value.fk_barcode_value_column_id =
(column?.value?.colOptions as Record<string, any>)?.fk_barcode_value_column_id || columnsAllowedAsBarcodeValue.value?.[0]
(column?.value?.colOptions as Record<string, any>)?.fk_barcode_value_column_id ||
columnsAllowedAsBarcodeValue.value?.[0]?.value
})
watch(columnsAllowedAsBarcodeValue, (newColumnsAllowedAsBarcodeValue) => {
if (vModel.value.fk_barcode_value_column_id == null) {
if (vModel.value.fk_barcode_value_column_id === null) {
vModel.value.fk_barcode_value_column_id = newColumnsAllowedAsBarcodeValue?.[0]?.value
}
})

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

@ -66,7 +66,7 @@ const isForm = inject(IsFormInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false))
const { isMysql, isMssql } = useBase()
const { isMysql, isMssql, isXcdbBase } = useBase()
const reloadDataTrigger = inject(ReloadViewDataHookInj)
@ -86,9 +86,9 @@ const showDeprecated = ref(false)
const uiTypesOptions = computed<typeof uiTypes>(() => {
return [
...uiTypes.filter(
(t) => geoDataToggleCondition(t) && (!isEdit.value || !t.virtual) && (!t.deprecated || showDeprecated.value),
),
...uiTypes
.filter((t) => geoDataToggleCondition(t) && (!isEdit.value || !t.virtual) && (!t.deprecated || showDeprecated.value))
.filter((t) => !(t.name === UITypes.SpecificDBType && isXcdbBase(meta.value?.source_id))),
...(!isEdit.value && meta?.value?.columns?.every((c) => !c.pk)
? [
{
@ -214,7 +214,7 @@ if (props.fromTableExplorer) {
'bg-white': !props.fromTableExplorer,
'w-[400px]': !props.embedMode,
'!w-[600px]': formState.uidt === UITypes.Formula && !props.embedMode,
'!w-[500px]': formState.uidt === UITypes.Attachment && !props.embedMode,
'!w-[500px]': formState.uidt === UITypes.Attachment && !props.embedMode && !appInfo.ee,
'shadow-lg border-1 border-gray-50 shadow-gray-100 rounded-md p-6': !embedMode,
}"
@keydown="handleEscape"
@ -331,7 +331,7 @@ if (props.fromTableExplorer) {
</div>
<div
v-if="!props.hideAdditionalOptions && !isVirtualCol(formState.uidt) && !appInfo.ee"
v-if="!props.hideAdditionalOptions && !isVirtualCol(formState.uidt) && (!appInfo.ee || (appInfo.ee && !isXcdbBase(meta!.source_id) && formState.uidt === UITypes.SpecificDBType))"
class="text-xs cursor-pointer text-gray-400 nc-more-options mb-1 mt-4 flex items-center gap-1 justify-end"
@click="advancedOptions = !advancedOptions"
>

13
packages/nc-gui/components/smartsheet/column/LookupOptions.vue

@ -2,7 +2,7 @@
import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { MetaInj, inject, ref, storeToRefs, useBase, useColumnCreateStoreOrThrow, useMetas, useVModel } from '#imports'
import { MetaInj, inject, ref, storeToRefs, useBase, useColumnCreateStoreOrThrow, useI18n, useMetas, useVModel } from '#imports'
const props = defineProps<{
value: any
@ -16,11 +16,10 @@ const meta = inject(MetaInj, ref())
const { t } = useI18n()
const { appInfo } = useGlobal()
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow()
const baseStore = useBase()
const { tables } = storeToRefs(baseStore)
const { metas } = useMetas()
@ -39,13 +38,7 @@ const refTables = computed(() => {
}
const _refTables = meta.value.columns
.filter(
(column) =>
isLinksOrLTAR(column) &&
!column.system &&
column.source_id === meta.value?.source_id &&
(!appInfo.value.ee || vModel.value.fk_relation_column_id === column.id || (column?.colOptions as any)?.type === 'bt'),
)
.filter((column) => isLinksOrLTAR(column) && !column.system && column.source_id === meta.value?.source_id)
.map((column) => ({
col: column.colOptions,
column,

11
packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue

@ -10,15 +10,9 @@ const props = defineProps<{
const emit = defineEmits(['update:modelValue'])
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const { t } = useI18n()
const reloadDataHook = inject(ReloadViewDataHookInj)!
const { fields, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
const { fields, metaColumnById } = useViewColumnsOrThrow()
const vModel = useVModel(props, 'modelValue', emit)
@ -40,7 +34,8 @@ const columnsAllowedAsQrValue = computed<SelectProps['options']>(() => {
onMounted(() => {
// set default value
vModel.value.fk_qr_value_column_id = (column?.value?.colOptions as Record<string, any>)?.fk_qr_value_column_id || ''
vModel.value.fk_qr_value_column_id =
(column?.value?.colOptions as Record<string, any>)?.fk_qr_value_column_id || columnsAllowedAsQrValue.value?.[0]?.value
})
setAdditionalValidations({

2
packages/nc-gui/components/smartsheet/details/Api.vue

@ -84,7 +84,7 @@ const selectedLangName = ref(langs[0].name)
const apiUrl = computed(
() =>
new URL(
`/api/v1/data/noco/${base.value?.id}/${meta.value?.title}/views/${view.value?.title}`,
`/api/v1/db/data/noco/${base.value?.id}/${meta.value?.title}/views/${view.value?.title}`,
(appInfo.value && appInfo.value.ncSiteUrl) || '/',
).href,
)

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

@ -45,12 +45,7 @@ const visibilityOps = ref<fieldsVisibilityOps[]>([])
const fieldsListWrapperDomRef = ref<HTMLElement>()
const {
fields: viewFields,
toggleFieldVisibility,
loadViewColumns,
isViewColumnsLoading,
} = useViewColumns(view, meta as Ref<TableType | undefined>)
const { fields: viewFields, toggleFieldVisibility, loadViewColumns, isViewColumnsLoading } = useViewColumnsOrThrow()
const loading = ref(false)

78
packages/nc-gui/components/smartsheet/expanded-form/Comments.vue

@ -4,12 +4,18 @@ import type { AuditType } from 'nocodb-sdk'
import { Icon } from '@iconify/vue'
import { ref, timeAgo, useExpandedFormStoreOrThrow, useGlobal, useRoles, watch } from '#imports'
const props = defineProps<{
isLoading: boolean
}>()
const { loadCommentsAndLogs, commentsAndLogs, saveComment: _saveComment, comment, updateComment } = useExpandedFormStoreOrThrow()
const commentsWrapperEl = ref<HTMLDivElement>()
const { user, appInfo } = useGlobal()
const isExpandedFormLoading = computed(() => props.isLoading)
const tab = ref<'comments' | 'audits'>('comments')
const { isUIAllowed } = useRoles()
@ -69,7 +75,7 @@ onKeyStroke('Enter', (event) => {
})
const comments = computed(() => commentsAndLogs.value.filter((log) => log.op_type === 'COMMENT'))
const audits = computed(() => commentsAndLogs.value.filter((log) => log.op_type !== 'COMMENT'))
const audits = computed(() => commentsAndLogs.value.filter((log) => log.op_type !== 'COMMENT' && log.details))
function editComment(log: AuditType) {
editLog.value = log
@ -90,9 +96,22 @@ function scrollComments() {
if (commentsWrapperEl.value) commentsWrapperEl.value.scrollTop = commentsWrapperEl.value?.scrollHeight
}
const isSaving = ref(false)
const saveComment = async () => {
await _saveComment()
scrollComments()
if (isSaving.value) return
isSaving.value = true
try {
await _saveComment()
scrollComments()
} catch (e) {
console.error(e)
} finally {
isSaving.value = false
}
}
watch(commentsWrapperEl, () => {
@ -157,10 +176,13 @@ const onClickAudit = () => {
<div
class="h-[calc(100%-4rem)]"
:class="{
'pb-2': tab !== 'comments' && !appInfo.ee,
'pb-1': tab !== 'comments' && !appInfo.ee,
}"
>
<div v-if="tab === 'comments'" class="flex flex-col h-full">
<div v-if="isExpandedFormLoading" class="flex flex-col h-full">
<GeneralLoader class="!mt-16" size="xlarge" />
</div>
<div v-else-if="tab === 'comments'" class="flex flex-col h-full">
<div v-if="comments.length === 0" class="flex flex-col my-1 text-center justify-center h-full">
<div class="text-center text-3xl text-gray-700">
<GeneralIcon icon="commentHere" />
@ -229,15 +251,17 @@ const onClickAudit = () => {
v-e="['a:row-expand:comment:save']"
size="medium"
class="!w-8"
:disabled="!comment.length"
:loading="isSaving"
:disabled="!isSaving && !comment.length"
:icon-only="isSaving"
@click="saveComment"
>
<GeneralIcon icon="send" />
<GeneralIcon v-if="!isSaving" icon="send" />
</NcButton>
</div>
</div>
</div>
<div v-if="tab === 'audits'" ref="commentsWrapperEl" class="flex flex-col h-full pl-2 pr-1 pt-2 nc-scrollbar-md space-y-2">
<div v-if="tab === 'audits'" ref="commentsWrapperEl" class="flex flex-col h-full nc-scrollbar-md !overflow-y-auto">
<template v-if="audits.length === 0">
<div class="flex flex-col text-center justify-center h-full">
<div class="text-center text-3xl text-gray-600">
@ -246,25 +270,24 @@ const onClickAudit = () => {
<div class="font-bold text-center my-1 text-gray-600">See changes to this record</div>
</div>
</template>
<div v-for="log of audits" :key="log.id">
<div v-if="log.details" class="bg-white rounded-xl border-1 gap-3 border-gray-200">
<div class="flex flex-col p-4 gap-3">
<div class="flex justify-between">
<div class="flex items-center gap-2">
<GeneralUserIcon size="base" :email="log.user" />
<div class="flex flex-col">
<span class="truncate font-bold max-w-50">
{{ log.display_name ?? log.user.split('@')[0].slice(0, 2) ?? 'Shared source' }}
</span>
<div v-if="log.id !== editLog?.id" class="text-xs font-medium text-gray-500">
{{ timeAgo(log.created_at) }}
</div>
<div v-for="log of audits" :key="log.id" class="nc-audit-item">
<div class="flex flex-col p-4 gap-3">
<div class="flex justify-between">
<div class="flex items-center gap-2">
<GeneralUserIcon size="base" :email="log.user" />
<div class="flex flex-col">
<span class="truncate font-bold max-w-50">
{{ log.display_name ?? log.user.split('@')[0].slice(0, 2) ?? 'Shared source' }}
</span>
<div v-if="log.id !== editLog?.id" class="text-xs font-medium text-gray-500">
{{ timeAgo(log.created_at) }}
</div>
</div>
</div>
<div v-dompurify-html="log.details" class="text-sm font-medium"></div>
</div>
<div v-dompurify-html="log.details" class="text-sm font-medium"></div>
</div>
</div>
</div>
@ -276,6 +299,15 @@ const onClickAudit = () => {
.tab {
@apply max-w-1/2;
}
.nc-audit-item {
@apply border-b-1 gap-3 border-gray-200;
}
.nc-audit-item:last-child {
@apply border-b-0;
}
.tab .tab-title {
@apply min-w-0 flex justify-center gap-2 font-semibold items-center;
word-break: 'keep-all';

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

@ -437,7 +437,7 @@ export default {
:class="{ active: isExpanded }"
>
<div class="h-[85vh] xs:(max-h-full) max-h-215 flex flex-col p-6">
<div class="flex h-8 flex-shrink-0 w-full items-center nc-expanded-form-header relative mb-4 justify-between">
<div class="flex h-9.5 flex-shrink-0 w-full items-center nc-expanded-form-header relative mb-4 justify-between">
<template v-if="!isMobileMode">
<div class="flex gap-3 w-100">
<div class="flex gap-2">
@ -483,7 +483,7 @@ export default {
{{ isRecordLinkCopied ? $t('labels.copiedRecordURL') : $t('labels.copyRecordURL') }}
</div>
</NcButton>
<NcDropdown v-if="!isNew">
<NcDropdown v-if="!isNew" placement="bottomRight">
<NcButton type="secondary" class="nc-expand-form-more-actions w-10">
<GeneralIcon icon="threeDotVertical" class="text-md text-gray-700" />
</NcButton>
@ -545,7 +545,7 @@ export default {
<template v-else>
<div class="flex flex-row w-full">
<NcButton
v-if="props.showNextPrevIcons"
v-if="props.showNextPrevIcons && !isFirstRow"
v-e="['c:row-expand:prev']"
type="secondary"
class="nc-prev-arrow !w-10"
@ -553,11 +553,12 @@ export default {
>
<GeneralIcon icon="arrowLeft" class="text-lg text-gray-700" />
</NcButton>
<div v-else class="min-w-10.5"></div>
<div class="flex flex-grow justify-center items-center font-semibold text-lg">
<div>{{ meta.title }}</div>
</div>
<NcButton
v-if="props.showNextPrevIcons && !props.lastRow"
v-if="props.showNextPrevIcons && !islastRow"
v-e="['c:row-expand:next']"
type="secondary"
class="nc-next-arrow !w-10"
@ -565,6 +566,7 @@ export default {
>
<GeneralIcon icon="arrowRight" class="text-lg text-gray-700" />
</NcButton>
<div v-else class="min-w-10.5"></div>
</div>
</template>
</div>
@ -577,7 +579,7 @@ export default {
}"
>
<div
class="flex flex-col flex-grow mt-2 h-full max-h-full nc-scrollbar-md !pb-2 items-center w-full bg-white p-4 xs:p-0"
class="flex flex-col flex-grow mt-2 h-full max-h-full nc-scrollbar-md pb-6 items-center w-full bg-white p-4 xs:p-0"
>
<div
v-for="(col, i) of fields"
@ -589,14 +591,14 @@ export default {
:data-testid="`nc-expand-col-${col.title}`"
>
<div class="flex items-start flex-row sm:(gap-x-6) xs:(flex-col w-full) nc-expanded-cell min-h-10">
<div class="w-[12rem] xs:(w-full) mt-0.25 !h-[35px]">
<div class="w-48 xs:(w-full) mt-0.25 !h-[35px]">
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(col)"
class="nc-expanded-cell-header h-full !text-gray-500"
class="nc-expanded-cell-header h-full"
:column="col"
/>
<LazySmartsheetHeaderCell v-else class="nc-expanded-cell-header !text-gray-500" :column="col" />
<LazySmartsheetHeaderCell v-else class="nc-expanded-cell-header" :column="col" />
</div>
<template v-if="isLoading">
@ -615,7 +617,7 @@ export default {
<SmartsheetDivDataCell
v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="!bg-white rounded-lg !w-[20rem] !xs:w-full border-1 border-gray-200 overflow-hidden px-1 min-h-[35px] flex items-center relative"
class="!bg-white rounded-lg !w-[20rem] !xs:w-full border-1 border-gray-200 overflow-hidden px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
>
<LazySmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="_row.row[col.title]" :row="_row" :column="col" />
@ -632,29 +634,34 @@ export default {
</template>
</div>
</div>
<div v-if="hiddenFields.length > 0" class="flex w-full px-12 items-center py-3">
<div v-if="hiddenFields.length > 0" class="flex w-full sm:px-12 xs:(px-1 mt-2) items-center py-3">
<div class="flex-grow h-px mr-1 bg-gray-100"></div>
<NcButton type="secondary" size="small" class="flex-shrink-1 !text-sm" @click="toggleHiddenFields">
<NcButton
type="secondary"
:size="isMobileMode ? 'medium' : 'small'"
class="flex-shrink-1 !text-sm"
@click="toggleHiddenFields"
>
{{ showHiddenFields ? `Hide ${hiddenFields.length} hidden` : `Show ${hiddenFields.length} hidden` }}
{{ hiddenFields.length > 1 ? `fields` : `field` }}
<MdiChevronDown class="ml-1" :class="showHiddenFields ? 'transform rotate-180' : ''" />
</NcButton>
<div class="flex-grow h-px ml-1 bg-gray-100"></div>
</div>
<div v-if="hiddenFields.length > 0 && showHiddenFields" class="mb-3">
<div v-if="hiddenFields.length > 0 && showHiddenFields" class="flex flex-col w-full mb-3 items-center">
<div
v-for="(col, i) of hiddenFields"
v-show="isFormula(col) || !isVirtualCol(col) || !isNew || isLinksOrLTAR(col)"
:key="col.title"
class="mt-2 py-2"
class="sm:(mt-2) py-2 xs:w-full"
:class="`nc-expand-col-${col.title}`"
:data-testid="`nc-expand-col-${col.title}`"
>
<div class="flex flex-row items-start min-h-10">
<div class="w-[12rem] scale-110 !h-[35px] mt-2.5">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" class="!text-gray-600" />
<div class="sm:gap-x-6 flex sm:flex-row xs:(flex-col) items-start min-h-10">
<div class="sm:w-48 xs:w-full scale-110 !h-[35px]">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" class="nc-expanded-cell-header" />
<LazySmartsheetHeaderCell v-else class="!text-gray-600" :column="col" />
<LazySmartsheetHeaderCell v-else class="nc-expanded-cell-header" :column="col" />
</div>
<template v-if="isLoading">
@ -673,7 +680,7 @@ export default {
<LazySmartsheetDivDataCell
v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="!bg-white rounded-lg !w-[20rem] border-1 overflow-hidden border-gray-200 px-1 min-h-[35px] flex items-center relative"
class="!bg-white rounded-lg !w-[20rem] border-1 overflow-hidden border-gray-200 px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(col)"
@ -702,7 +709,7 @@ export default {
v-if="isUIAllowed('dataEdit')"
class="w-full h-16 border-t-1 border-gray-200 bg-white flex items-center justify-end p-3 xs:(p-0 mt-4 border-t-0 gap-x-4 justify-between)"
>
<NcDropdown v-if="!isNew && isMobileMode">
<NcDropdown v-if="!isNew && isMobileMode" placement="bottomRight">
<NcButton type="secondary" class="nc-expand-form-more-actions w-10">
<GeneralIcon icon="threeDotVertical" class="text-md text-gray-700" />
</NcButton>
@ -760,7 +767,7 @@ export default {
class="nc-comments-drawer border-1 relative border-gray-200 w-1/3 max-w-125 bg-gray-50 rounded-xl min-w-0 overflow-hidden h-full xs:hidden"
:class="{ active: commentsDrawer && isUIAllowed('commentList') }"
>
<SmartsheetExpandedFormComments />
<SmartsheetExpandedFormComments :loading="isLoading" />
</div>
</div>
</div>
@ -810,7 +817,7 @@ export default {
}
.nc-expanded-cell-header {
@apply w-full text-gray-700 xs:mb-2;
@apply w-full text-gray-500 xs:(text-gray-600 mb-2);
}
.nc-expanded-cell-header > :nth-child(2) {
@ -824,3 +831,13 @@ export default {
@apply !p-0;
}
</style>
<style lang="scss" scoped>
:deep(.ant-select-selector) {
@apply !xs:(h-full);
}
:deep(.ant-select-selection-item) {
@apply !xs:(mt-1.75 ml-1);
}
</style>

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

@ -18,6 +18,7 @@ const props = defineProps<{
viewWidth?: number
scrollLeft?: number
fullPage?: boolean
depth?: number
maxDepth?: number
@ -40,6 +41,12 @@ const wrapper = ref<HTMLElement | undefined>()
const scrollable = ref<HTMLElement | undefined>()
const tableHeader = ref<HTMLElement | undefined>()
const fullPage = computed<boolean>(() => {
return props.fullPage ?? (tableHeader.value?.offsetWidth ?? 0) > (props.viewWidth ?? 0)
})
const _activeGroupKeys = ref<string[] | string>()
const activeGroups = computed<string[]>(() => {
@ -149,7 +156,7 @@ const onScroll = (e: Event) => {
style="background-color: #f9f9fa; border-color: #e7e7e9; border-bottom-width: 1px"
:style="{ 'padding-left': `${(maxDepth || 1) * 13}px` }"
></div>
<Table class="mb-2" :data="[]" :header-only="true" />
<Table ref="tableHeader" class="mb-2" :data="[]" :header-only="true" />
</div>
<div :class="{ 'px-[12px]': vGroup.root === true }">
<a-collapse
@ -258,11 +265,12 @@ const onScroll = (e: Event) => {
:row-height="rowHeight"
:redistribute-rows="redistributeRows"
:expand-form="expandForm"
:pagination-fixed-size="props.viewWidth"
:pagination-fixed-size="fullPage ? props.viewWidth : undefined"
:pagination-hide-sidebars="true"
:scroll-left="props.scrollLeft || _scrollLeft"
:view-width="viewWidth"
:scrollable="scrollable"
:full-page="fullPage"
/>
<GroupBy
v-else
@ -277,6 +285,7 @@ const onScroll = (e: Event) => {
:view-width="viewWidth"
:depth="_depth + 1"
:scroll-left="scrollBump"
:full-page="fullPage"
/>
</a-collapse-panel>
</a-collapse>
@ -288,6 +297,7 @@ const onScroll = (e: Event) => {
v-model:pagination-data="vGroup.paginationData"
align-count-on-right
custom-label="groups"
show-api-timing
:change-page="(p: number) => groupWrapperChangePage(p, vGroup)"
:style="`${props.depth && props.depth > 0 ? 'border-radius: 0 0 12px 12px !important;' : ''}`"
></LazySmartsheetPagination>
@ -296,10 +306,11 @@ const onScroll = (e: Event) => {
v-model:pagination-data="vGroup.paginationData"
align-count-on-right
custom-label="groups"
show-api-timing
:change-page="(p: number) => groupWrapperChangePage(p, vGroup)"
:hide-sidebars="true"
:style="`${props.depth && props.depth > 0 ? 'border-radius: 0 0 12px 12px !important;' : ''}margin-left: ${scrollBump}px;`"
:fixed-size="props.viewWidth"
:fixed-size="fullPage ? props.viewWidth : undefined"
></LazySmartsheetPagination>
</div>
</template>

30
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -28,7 +28,6 @@ import {
provide,
ref,
useEventListener,
useGridViewColumnOrThrow,
useI18n,
useMultiSelect,
useNuxtApp,
@ -36,6 +35,7 @@ import {
useRoute,
useSmartsheetStoreOrThrow,
useUndoRedo,
useViewColumnsOrThrow,
useViewsStore,
watch,
} from '#imports'
@ -123,8 +123,6 @@ const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook())
const { isViewColumnsLoading } = useViewColumns(view, meta, () => reloadViewDataHook.trigger())
const { isMobileMode } = useGlobal()
const scrollParent = inject(ScrollParentInj, ref<undefined>())
@ -141,6 +139,8 @@ const { getMeta } = useMetas()
const { addUndo, clone, defineViewScope } = useUndoRedo()
const { isViewColumnsLoading, updateGridViewColumn, gridViewCols, resizingColOldWith } = useViewColumnsOrThrow()
const {
predictingNextColumn,
predictedNextColumn,
@ -414,7 +414,8 @@ const closeAddColumnDropdown = (scrollToLastCol = false) => {
preloadColumn.value = {}
if (scrollToLastCol) {
setTimeout(() => {
const lastAddNewRowHeader = tableHeadEl.value?.querySelector('th:last-child')
const lastAddNewRowHeader =
tableHeadEl.value?.querySelector('.nc-grid-add-edit-column') ?? tableHeadEl.value?.querySelector('th:last-child')
if (lastAddNewRowHeader) {
lastAddNewRowHeader.scrollIntoView({ behavior: 'smooth' })
}
@ -893,8 +894,6 @@ const saveOrUpdateRecords = async (args: { metaValue?: TableType; viewMetaValue?
}
// #Grid Resize
const { updateGridViewColumn, gridViewCols, resizingColOldWith } = useGridViewColumnOrThrow()
const onresize = (colID: string | undefined, event: any) => {
if (!colID) return
updateGridViewColumn(colID, { width: event.detail })
@ -1210,7 +1209,7 @@ const loaderText = computed(() => {
<div class="table-overlay" :class="{ 'nc-grid-skelton-loader': showSkeleton }">
<table
ref="smartTable"
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white"
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white relative"
:class="{
mobile: isMobileMode,
desktop: !isMobileMode,
@ -1553,11 +1552,15 @@ const loaderText = computed(() => {
@mouseup.stop
@click="addEmptyRow()"
>
<td class="text-left pointer sticky left-0 !border-r-0">
<div class="px-2 w-full flex items-center text-gray-500">
<component :is="iconMap.plus" class="text-pint-500 text-base ml-2 text-gray-600 group-hover:text-black" />
</div>
</td>
<div
class="h-10.5 border-b-1 border-gray-100 bg-white group-hover:bg-gray-50 absolute left-0 bottom-0 px-2 sticky z-40 w-full flex items-center text-gray-500"
>
<component
:is="iconMap.plus"
v-if="!isViewColumnsLoading"
class="text-pint-500 text-base ml-2 mt-0 text-gray-600 group-hover:text-black"
/>
</div>
<td class="!border-gray-100" :colspan="visibleColLength"></td>
</tr>
</tbody>
@ -1681,8 +1684,9 @@ const loaderText = computed(() => {
v-if="headerOnly !== true"
:key="isMobileMode"
v-model:pagination-data="paginationDataRef"
show-api-timing
:show-api-timing="!isGroupBy"
align-count-on-right
:align-left="isGroupBy"
:change-page="changePage"
:hide-sidebars="paginationStyleRef?.hideSidebars === true"
:fixed-size="paginationStyleRef?.fixedSize"

2
packages/nc-gui/components/smartsheet/header/CellIcon.ts

@ -63,7 +63,7 @@ const renderIcon = (column: ColumnType, abstractType: any) => {
} else if (isYear(column, abstractType)) {
return iconMap.calendar
} else if (isTime(column, abstractType)) {
return iconMap.calendar
return iconMap.clock
} else if (isRating(column)) {
return iconMap.rating
} else if (isAttachment(column)) {

2
packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue

@ -21,7 +21,7 @@ const activeView = inject(ActiveViewInj, ref())
const meta = inject(MetaInj, ref())
const { showSystemFields, metaColumnById } = useViewColumns(activeView, meta)
const { showSystemFields, metaColumnById } = useViewColumnsOrThrow(activeView, meta)
const { sorts } = useViewSorts(activeView)

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

@ -2,7 +2,7 @@
import type { SelectProps } from 'ant-design-vue'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { ActiveViewInj, MetaInj, computed, inject, ref, resolveComponent, useViewColumns } from '#imports'
import { ActiveViewInj, MetaInj, computed, inject, ref, resolveComponent, useViewColumnsOrThrow } from '#imports'
const { modelValue, isSort, allowEmpty, ...restProps } = defineProps<{
modelValue?: string
@ -24,7 +24,7 @@ const localValue = computed({
const activeView = inject(ActiveViewInj, ref())
const { showSystemFields, metaColumnById } = useViewColumns(activeView, meta)
const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const options = computed<SelectProps['options']>(() =>
(

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

@ -21,16 +21,12 @@ import {
useNuxtApp,
useSmartsheetStoreOrThrow,
useUndoRedo,
useViewColumns,
useViewColumnsOrThrow,
watch,
} from '#imports'
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const reloadViewMetaHook = inject(ReloadViewMetaHookInj, undefined)!
const rootFields = inject(FieldsInj)
@ -55,7 +51,7 @@ const {
metaColumnById,
loadViewColumns,
toggleFieldVisibility,
} = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
} = useViewColumnsOrThrow()
const { eventBus } = useSmartsheetStoreOrThrow()

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

@ -14,6 +14,7 @@ import {
useMetas,
useNuxtApp,
useSmartsheetStoreOrThrow,
useViewColumnsOrThrow,
} from '#imports'
const groupingUidt = [
@ -33,7 +34,7 @@ const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj, ref(false))
const { gridViewCols, updateGridViewColumn } = useGridViewColumnOrThrow()
const { gridViewCols, updateGridViewColumn } = useViewColumnsOrThrow()
const { $e } = useNuxtApp()

6
packages/nc-gui/components/smartsheet/toolbar/MappedBy.vue

@ -12,7 +12,7 @@ import {
iconMap,
inject,
ref,
useViewColumns,
useViewColumnsOrThrow,
watch,
} from '#imports'
@ -22,13 +22,11 @@ const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const isLocked = inject(IsLockedInj, ref(false))
const IsPublic = inject(IsPublicInj, ref(false))
const { fields, loadViewColumns, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
const { fields, loadViewColumns, metaColumnById } = useViewColumnsOrThrow()
const { loadMapData, loadMapMeta, updateMapMeta, mapMetaData, geoDataFieldColumn } = useMapViewStoreOrThrow()

16
packages/nc-gui/components/smartsheet/toolbar/SearchData.vue

@ -98,20 +98,8 @@ watch(columns, () => {
<div v-if="!isMobileMode" class="w-16 text-[0.75rem] font-medium text-gray-400 truncate">
{{ displayColumnLabel }}
</div>
<div
:class="{
'opacity-0 group-hover:opacity-100': !isMobileMode,
'text-gray-700': isMobileMode,
}"
>
<component
:is="iconMap.arrowDown"
class="text-sm"
:class="{
'text-gray-400': !isMobileMode,
'text-gray-600': isMobileMode,
}"
/>
<div class="xs:(text-gray-600) group-hover:text-gray-700 sm:(text-gray-400)">
<component :is="iconMap.arrowDown" class="text-sm text-inherit" />
</div>
<a-select
v-model:value="search.field"

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

@ -28,7 +28,7 @@ const { eventBus } = useSmartsheetStoreOrThrow()
const { sorts, saveOrUpdate, loadSorts, addSort: _addSort, deleteSort } = useViewSorts(view, () => reloadDataHook?.trigger())
const { showSystemFields, metaColumnById } = useViewColumns(view, meta)
const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const showCreateSort = ref(false)
@ -79,14 +79,6 @@ const getColumnUidtByID = (key?: string) => {
return columnByID.value[key]?.uidt || ''
}
watch(
() => view.value?.id,
(viewId) => {
if (viewId) loadSorts()
},
{ immediate: true },
)
const open = ref(false)
useMenuCloseOnEsc(open)
@ -105,6 +97,10 @@ watch(open, () => {
showCreateSort.value = false
}
})
onMounted(() => {
loadSorts()
})
</script>
<template>

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

@ -14,7 +14,7 @@ import {
useKanbanViewStoreOrThrow,
useMenuCloseOnEsc,
useUndoRedo,
useViewColumns,
useViewColumnsOrThrow,
watch,
} from '#imports'
@ -30,7 +30,7 @@ const reloadDataHook = inject(ReloadViewDataHookInj)!
const isLocked = inject(IsLockedInj, ref(false))
const { fields, loadViewColumns, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
const { fields, loadViewColumns, metaColumnById } = useViewColumnsOrThrow(activeView, meta)
const { kanbanMetaData, loadKanbanMeta, loadKanbanData, updateKanbanMeta, groupingField, groupingFieldColumn } =
useKanbanViewStoreOrThrow()

12
packages/nc-gui/components/smartsheet/topbar/SelectMode.vue

@ -3,7 +3,17 @@ import { storeToRefs, useViewsStore } from '#imports'
const { openedViewsTab, activeView } = storeToRefs(useViewsStore())
const { isUIAllowed } = useRoles()
const { onViewsTabChange } = useViewsStore()
const onClickDetails = () => {
if (isUIAllowed('fieldAdd')) {
onViewsTabChange('field')
} else {
onViewsTabChange('relation')
}
}
</script>
<template>
@ -26,7 +36,7 @@ const { onViewsTabChange } = useViewsStore()
:class="{
active: openedViewsTab !== 'view',
}"
@click="onViewsTabChange('field')"
@click="onClickDetails"
>
<GeneralIcon
icon="erd"

3
packages/nc-gui/components/tabs/Smartsheet.vue

@ -63,7 +63,6 @@ const openNewRecordFormHook = createEventHook<void>()
useProvideKanbanViewStore(meta, activeView)
useProvideMapViewStore(meta, activeView)
useProvideGridViewColumn(activeView)
// todo: move to store
provide(MetaInj, meta)
@ -80,6 +79,8 @@ provide(
computed(() => !isUIAllowed('dataEdit')),
)
useProvideViewColumns(activeView, meta, () => reloadEventHook?.trigger())
const grid = ref()
const onDrop = async (event: DragEvent) => {

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

@ -128,10 +128,7 @@ const validators = computed(() =>
hasSelectColumn.value[tableIdx] = false
table.columns?.forEach((column, columnIdx) => {
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [
fieldRequiredValidator(),
fieldLengthValidator(base.value?.sources?.[0].type || ClientType.MYSQL),
]
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [fieldRequiredValidator(), fieldLengthValidator()]
acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()]
if (isSelect(column)) {
hasSelectColumn.value[tableIdx] = true
@ -434,7 +431,7 @@ async function importTemplate() {
let input = row[col.srcCn]
// parse potential boolean values
if (v.uidt === UITypes.Checkbox) {
input = input.replace(/["']/g, '').toLowerCase().trim()
input = input ? input.replace(/["']/g, '').toLowerCase().trim() : 'false'
if (input === 'false' || input === 'no' || input === 'n') {
input = '0'
} else if (input === 'true' || input === 'yes' || input === 'y') {

43
packages/nc-gui/components/workspace/ProjectList.vue

@ -11,19 +11,16 @@ const { updateProjectTitle } = workspaceStore
const { activePage } = storeToRefs(workspaceStore)
const basesStore = useBases()
const { loadProjects } = basesStore
const { basesList, isProjectsLoading } = storeToRefs(basesStore)
const { navigateToProject } = useGlobal()
// const filteredProjects = computed(() => bases.value?.filter((p) => !p.deleted) || [])
const { $e, $poller } = useNuxtApp()
const { $e } = useNuxtApp()
const { isUIAllowed } = useRoles()
const { refreshCommandPalette } = useCommandPalette()
const showProjectDeleteModal = ref(false)
const toBeDeletedProjectId = ref<string | undefined>()
@ -142,37 +139,6 @@ const workspaceMoveProjectOnSuccess = async (workspaceId: string) => {
const isDuplicateDlgOpen = ref(false)
const selectedProjectToDuplicate = ref()
const DlgProjectDuplicateOnOk = async (jobData: { id: string }) => {
await loadProjects('workspace')
$poller.subscribe(
{ id: jobData.id },
async (data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
if (data.status === JobStatus.COMPLETED) {
await loadProjects('workspace')
refreshCommandPalette()
} else if (data.status === JobStatus.FAILED) {
message.error('Failed to duplicate base')
await loadProjects('workspace')
}
}
},
)
$e('a:base:duplicate')
}
const duplicateProject = (base: BaseType) => {
selectedProjectToDuplicate.value = base
isDuplicateDlgOpen.value = true
@ -404,12 +370,7 @@ const setIcon = async (icon: string, base: BaseType) => {
:base="selectedProjectToMove"
@success="workspaceMoveProjectOnSuccess"
/>
<DlgProjectDuplicate
v-if="selectedProjectToDuplicate"
v-model="isDuplicateDlgOpen"
:base="selectedProjectToDuplicate"
:on-ok="DlgProjectDuplicateOnOk"
/>
<DlgProjectDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
</div>
</template>

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

@ -28,8 +28,9 @@ const useAttachment = () => {
} catch {}
}
// if no source can be fetched, it could be probably blocked by CORS
// return original url or built url anyway
return item.url || `${appInfo.value.ncSiteUrl}/${item.path}`
// return signed url / original url / built url anyway
// which we can extract from the sources array since it's ordered based on priority
return sources[0]
}
const openAttachment = async (item: Record<string, any>) => {

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

@ -35,7 +35,9 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
tableExplorerColumns?: Ref<ColumnType[] | undefined>,
) => {
const baseStore = useBase()
const { isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc, isXcdbBase: isXcdbBaseFunc, getBaseType } = baseStore
const { isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc, isXcdbBase: isXcdbBaseFunc } = baseStore
const { sqlUis } = storeToRefs(baseStore)
const { $api } = useNuxtApp()
@ -60,8 +62,6 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isXcdbBaseFunc(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]),
)
const baseType = computed(() => getBaseType(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]))
const idType = null
const additionalValidations = ref<ValidationsObj>({})
@ -126,7 +126,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
})
},
},
fieldLengthValidator(baseType.value || ClientType.MYSQL),
fieldLengthValidator(),
],
uidt: [
{

9
packages/nc-gui/composables/useCopySharedBase.ts

@ -0,0 +1,9 @@
import { createSharedComposable, ref } from '#imports'
export const useCopySharedBase = createSharedComposable(() => {
const sharedBaseId = ref<string | null>(null)
return {
sharedBaseId,
}
})

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

@ -141,11 +141,11 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
description: `The following comment has been created: ${comment.value}`,
})
comment.value = ''
reloadTrigger?.trigger()
await loadCommentsAndLogs()
comment.value = ''
} catch (e: any) {
message.error(e.message)
}

97
packages/nc-gui/composables/useGridViewColumn.ts

@ -1,97 +0,0 @@
import type { ColumnType, GridColumnReqType, GridColumnType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { IsPublicInj, computed, inject, ref, useMetas, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports'
const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
(view: Ref<(ViewType & { columns?: GridColumnType[] }) | undefined>, statePublic = false) => {
const { isUIAllowed } = useRoles()
const { $api } = useNuxtApp()
const { metas } = useMetas()
const { addUndo, defineViewScope } = useUndoRedo()
const gridViewCols = ref<Record<string, GridColumnType>>({})
const resizingColOldWith = ref('200px')
const isPublic = inject(IsPublicInj, ref(statePublic))
const columns = computed<ColumnType[]>(() => metas.value?.[view.value?.fk_model_id as string]?.columns || [])
const loadGridViewColumns = async () => {
if (!view.value?.id && !isPublic.value) return
const colsData: GridColumnType[] =
(isPublic.value ? view.value?.columns : await $api.dbView.gridColumnsList(view.value!.id!)) ?? []
gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>(
(o, col) => ({
...o,
[col.fk_column_id as string]: col,
}),
{},
)
}
/** when columns changes(create/delete) reload grid columns
* or when view changes reload columns width */
watch(
[() => columns.value?.length, () => view.value?.id],
async (n) => {
if (n[1]) await loadGridViewColumns()
},
{ immediate: true },
)
const updateGridViewColumn = async (id: string, props: Partial<GridColumnReqType>, undo = false) => {
if (!undo) {
const oldProps = Object.keys(props).reduce<Partial<GridColumnReqType>>((o: any, k) => {
if (gridViewCols.value[id][k as keyof GridColumnType]) {
if (k === 'width') o[k] = `${resizingColOldWith.value}px`
else o[k] = gridViewCols.value[id][k as keyof GridColumnType]
}
return o
}, {})
addUndo({
redo: {
fn: (w: Partial<GridColumnReqType>) => updateGridViewColumn(id, w, true),
args: [props],
},
undo: {
fn: (w: Partial<GridColumnReqType>) => updateGridViewColumn(id, w, true),
args: [oldProps],
},
scope: defineViewScope({ view: view.value }),
})
}
// sync with server if allowed
if (!isPublic.value && isUIAllowed('viewFieldEdit') && gridViewCols.value[id]?.id) {
await $api.dbView.gridColumnUpdate(gridViewCols.value[id].id as string, {
...props,
})
}
if (gridViewCols.value?.[id]) {
Object.assign(gridViewCols.value[id], {
...gridViewCols.value[id],
...props,
})
} else {
// fallback to reload
await loadGridViewColumns()
}
}
return { loadGridViewColumns, updateGridViewColumn, gridViewCols, resizingColOldWith }
},
'useGridViewColumn',
)
export { useProvideGridViewColumn }
export function useGridViewColumnOrThrow() {
const gridViewColumn = useGridViewColumn()
if (gridViewColumn == null) throw new Error('Please call `useProvideGridViewColumn` on the appropriate parent component')
return gridViewColumn
}

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

@ -56,8 +56,6 @@ export function useTableNew(param: { onTableCreate?: (tableMeta: TableType) => v
const tables = computed(() => baseTables.value.get(param.baseId) || [])
const base = computed(() => bases.value.get(param.baseId))
const { loadViews } = useViewsStore()
const openTable = async (table: TableType) => {
if (!table.base_id) return
@ -82,12 +80,10 @@ export function useTableNew(param: { onTableCreate?: (tableMeta: TableType) => v
baseIdOrBaseId = route.value.params.baseId as string
}
await loadViews({
tableId: table.id,
})
const views = viewsByTable.value.get(table.id as string) ?? []
getMeta(table.id as string, (route.value.params?.viewId as string) !== table.id)
if (openedViewsTab.value !== 'view' && views[0].id) {
await navigateTo({
path: `/${workspaceIdOrType}/${baseIdOrBaseId}/${table?.id}/${views[0].id}/${openedViewsTab.value}`,
@ -98,8 +94,6 @@ export function useTableNew(param: { onTableCreate?: (tableMeta: TableType) => v
path: `/${workspaceIdOrType}/${baseIdOrBaseId}/${table?.id}`,
query: route.value.query,
})
await getMeta(table.id as string)
}
const createTable = async () => {

557
packages/nc-gui/composables/useViewColumns.ts

@ -1,296 +1,363 @@
import { ViewTypes, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, MapType, TableType, ViewType } from 'nocodb-sdk'
import type { ColumnType, GridColumnReqType, GridColumnType, MapType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { IsPublicInj, computed, inject, ref, storeToRefs, useBase, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports'
import { computed, ref, storeToRefs, useBase, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports'
import type { Field } from '#imports'
export function useViewColumns(
view: Ref<ViewType | undefined>,
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
reloadData?: () => void,
) {
const isPublic = inject(IsPublicInj, ref(false))
const [useProvideViewColumns, useViewColumns] = useInjectionState(
(
view: Ref<ViewType | undefined>,
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
reloadData?: () => void,
isPublic = false,
) => {
const fields = ref<Field[]>()
const fields = ref<Field[]>()
const filterQuery = ref('')
const filterQuery = ref('')
const { $api, $e } = useNuxtApp()
const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useRoles()
const { isUIAllowed } = useRoles()
const { isSharedBase } = storeToRefs(useBase())
const { isSharedBase } = storeToRefs(useBase())
const isViewColumnsLoading = ref(false)
const isViewColumnsLoading = ref(false)
const { addUndo, defineViewScope } = useUndoRedo()
const { addUndo, defineViewScope } = useUndoRedo()
const isLocalMode = computed(
() => isPublic || !isUIAllowed('viewFieldEdit') || !isUIAllowed('viewFieldEdit') || isSharedBase.value,
)
const isLocalMode = computed(
() => isPublic.value || !isUIAllowed('viewFieldEdit') || !isUIAllowed('viewFieldEdit') || isSharedBase.value,
)
const localChanges = ref<Field[]>([])
const localChanges = ref<Field[]>([])
const isColumnViewEssential = (column: ColumnType) => {
// TODO: consider at some point ti delegate this via a cleaner design pattern to view specific check logic
// which could be inside of a view specific helper class (and generalized via an interface)
// (on the other hand, the logic complexity is still very low atm - might be overkill)
return view.value?.type === ViewTypes.MAP && (view.value?.view as MapType)?.fk_geo_data_col_id === column.id
}
const isColumnViewEssential = (column: ColumnType) => {
// TODO: consider at some point ti delegate this via a cleaner design pattern to view specific check logic
// which could be inside of a view specific helper class (and generalized via an interface)
// (on the other hand, the logic complexity is still very low atm - might be overkill)
return view.value?.type === ViewTypes.MAP && (view.value?.view as MapType)?.fk_geo_data_col_id === column.id
}
const metaColumnById = computed<Record<string, ColumnType>>(() => {
if (!meta.value?.columns) return {}
const metaColumnById = computed<Record<string, ColumnType>>(() => {
if (!meta.value?.columns) return {}
return (meta.value.columns as ColumnType[]).reduce(
(acc, curr) => ({
...acc,
[curr.id!]: curr,
}),
{},
) as Record<string, ColumnType>
})
return (meta.value.columns as ColumnType[]).reduce(
(acc, curr) => ({
...acc,
[curr.id!]: curr,
}),
{},
) as Record<string, ColumnType>
})
const gridViewCols = ref<Record<string, GridColumnType>>({})
const loadViewColumns = async () => {
if (!meta || !view) return
const loadViewColumns = async () => {
if (!meta || !view) return
let order = 1
let order = 1
if (view.value?.id) {
const data = (isPublic.value ? meta.value?.columns : (await $api.dbViewColumn.list(view.value.id)).list) as any[]
if (view.value?.id) {
const data = (isPublic ? meta.value?.columns : (await $api.dbViewColumn.list(view.value.id)).list) as any[]
const fieldById = data.reduce<Record<string, any>>((acc, curr) => {
curr.show = !!curr.show
return {
...acc,
[curr.fk_column_id]: curr,
}
}, {})
fields.value = meta.value?.columns
?.map((column: ColumnType) => {
const currentColumnField = fieldById[column.id!] || {}
const fieldById = data.reduce<Record<string, any>>((acc, curr) => {
curr.show = !!curr.show
return {
title: column.title,
fk_column_id: column.id,
...currentColumnField,
show: currentColumnField.show || isColumnViewEssential(currentColumnField),
order: currentColumnField.order || order++,
system: isSystemColumn(metaColumnById?.value?.[currentColumnField.fk_column_id!]),
isViewEssentialField: isColumnViewEssential(column),
...acc,
[curr.fk_column_id]: curr,
}
})
.sort((a: Field, b: Field) => a.order - b.order)
if (isLocalMode.value && fields.value) {
for (const field of localChanges.value) {
const fieldIndex = fields.value.findIndex((f) => f.fk_column_id === field.fk_column_id)
if (fieldIndex !== undefined && fieldIndex > -1) {
fields.value[fieldIndex] = field
fields.value = fields.value.sort((a: Field, b: Field) => a.order - b.order)
}, {})
fields.value = meta.value?.columns
?.map((column: ColumnType) => {
const currentColumnField = fieldById[column.id!] || {}
return {
title: column.title,
fk_column_id: column.id,
...currentColumnField,
show: currentColumnField.show || isColumnViewEssential(currentColumnField),
order: currentColumnField.order || order++,
system: isSystemColumn(metaColumnById?.value?.[currentColumnField.fk_column_id!]),
isViewEssentialField: isColumnViewEssential(column),
}
})
.sort((a: Field, b: Field) => a.order - b.order)
if (isLocalMode.value && fields.value) {
for (const field of localChanges.value) {
const fieldIndex = fields.value.findIndex((f) => f.fk_column_id === field.fk_column_id)
if (fieldIndex !== undefined && fieldIndex > -1) {
fields.value[fieldIndex] = field
fields.value = fields.value.sort((a: Field, b: Field) => a.order - b.order)
}
}
}
const colsData: GridColumnType[] = (isPublic.value ? view.value?.columns : fields.value) ?? []
gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>(
(o, col) => ({
...o,
[col.fk_column_id as string]: col,
}),
{},
)
}
}
}
const showAll = async (ignoreIds?: any) => {
if (isLocalMode.value) {
fields.value = fields.value?.map((field: Field) => ({
...field,
show: true,
}))
reloadData?.()
return
}
if (view?.value?.id) {
if (ignoreIds) {
await $api.dbView.showAllColumn(view.value.id, {
ignoreIds,
})
} else {
await $api.dbView.showAllColumn(view.value.id)
const showAll = async (ignoreIds?: any) => {
if (isLocalMode.value) {
fields.value = fields.value?.map((field: Field) => ({
...field,
show: true,
}))
reloadData?.()
return
}
if (view?.value?.id) {
if (ignoreIds) {
await $api.dbView.showAllColumn(view.value.id, {
ignoreIds,
})
} else {
await $api.dbView.showAllColumn(view.value.id)
}
}
await loadViewColumns()
reloadData?.()
$e('a:fields:show-all')
}
const hideAll = async (ignoreIds?: any) => {
if (isLocalMode.value) {
fields.value = fields.value?.map((field: Field) => ({
...field,
show: !!field.isViewEssentialField,
}))
reloadData?.()
return
}
if (view?.value?.id) {
if (ignoreIds) {
await $api.dbView.hideAllColumn(view.value.id, {
ignoreIds,
})
} else {
await $api.dbView.hideAllColumn(view.value.id)
}
}
await loadViewColumns()
reloadData?.()
$e('a:fields:show-all')
}
const hideAll = async (ignoreIds?: any) => {
if (isLocalMode.value) {
fields.value = fields.value?.map((field: Field) => ({
...field,
show: !!field.isViewEssentialField,
}))
await loadViewColumns()
reloadData?.()
return
$e('a:fields:show-all')
}
if (view?.value?.id) {
if (ignoreIds) {
await $api.dbView.hideAllColumn(view.value.id, {
ignoreIds,
const saveOrUpdate = async (field: any, index: number) => {
if (isLocalMode.value && fields.value) {
fields.value[index] = field
meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => {
if (column.id === field.fk_column_id) {
return {
...column,
...field,
id: field.fk_column_id,
}
}
return column
})
} else {
await $api.dbView.hideAllColumn(view.value.id)
localChanges.value.push(field)
}
}
await loadViewColumns()
reloadData?.()
$e('a:fields:show-all')
}
if (isUIAllowed('viewFieldEdit')) {
if (field.id && view?.value?.id) {
await $api.dbViewColumn.update(view.value.id, field.id, field)
} else if (view.value?.id) {
const insertedField = (await $api.dbViewColumn.create(view.value.id, field)) as any
const saveOrUpdate = async (field: any, index: number) => {
if (isLocalMode.value && fields.value) {
fields.value[index] = field
meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => {
if (column.id === field.fk_column_id) {
return {
...column,
...field,
id: field.fk_column_id,
}
/** update the field in fields if defined */
if (fields.value) fields.value[index] = insertedField
return insertedField
}
return column
})
}
localChanges.value.push(field)
await loadViewColumns()
reloadData?.()
}
if (isUIAllowed('viewFieldEdit')) {
if (field.id && view?.value?.id) {
await $api.dbViewColumn.update(view.value.id, field.id, field)
} else if (view.value?.id) {
const insertedField = (await $api.dbViewColumn.create(view.value.id, field)) as any
const showSystemFields = computed({
get() {
return (view.value?.show_system_fields as boolean) || false
},
set(v: boolean) {
if (view?.value?.id) {
if (!isLocalMode.value) {
$api.dbView
.update(view.value.id, {
show_system_fields: v,
})
.finally(() => {
loadViewColumns()
reloadData?.()
})
}
view.value.show_system_fields = v
}
$e('a:fields:system-fields')
},
})
/** update the field in fields if defined */
if (fields.value) fields.value[index] = insertedField
const filteredFieldList = computed(() => {
return (
fields.value?.filter((field: Field) => {
if (metaColumnById?.value?.[field.fk_column_id!]?.pv) return true
return insertedField
}
}
// hide system columns if not enabled
if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) {
return false
}
await loadViewColumns()
reloadData?.()
}
if (filterQuery.value === '') {
return true
} else {
return field.title.toLowerCase().includes(filterQuery.value.toLowerCase())
}
}) || []
)
})
const showSystemFields = computed({
get() {
return (view.value?.show_system_fields as boolean) || false
},
set(v: boolean) {
if (view?.value?.id) {
if (!isLocalMode.value) {
$api.dbView
.update(view.value.id, {
show_system_fields: v,
})
.finally(() => {
loadViewColumns()
reloadData?.()
})
}
view.value.show_system_fields = v
}
$e('a:fields:system-fields')
},
})
const filteredFieldList = computed(() => {
return (
fields.value?.filter((field: Field) => {
if (metaColumnById?.value?.[field.fk_column_id!]?.pv) return true
// hide system columns if not enabled
if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) {
return false
}
const sortedAndFilteredFields = computed<ColumnType[]>(() => {
return (fields?.value
?.filter((field: Field) => {
// hide system columns if not enabled
if (
!showSystemFields.value &&
metaColumnById.value &&
metaColumnById?.value?.[field.fk_column_id!] &&
isSystemColumn(metaColumnById.value?.[field.fk_column_id!]) &&
!metaColumnById.value?.[field.fk_column_id!]?.pv
) {
return false
}
return field.show && metaColumnById?.value?.[field.fk_column_id!]
})
?.sort((a: Field, b: Field) => a.order - b.order)
?.map((field: Field) => metaColumnById?.value?.[field.fk_column_id!]) || []) as ColumnType[]
})
if (filterQuery.value === '') {
return true
} else {
return field.title.toLowerCase().includes(filterQuery.value.toLowerCase())
}
}) || []
)
})
const sortedAndFilteredFields = computed<ColumnType[]>(() => {
return (fields?.value
?.filter((field: Field) => {
// hide system columns if not enabled
if (
!showSystemFields.value &&
metaColumnById.value &&
metaColumnById?.value?.[field.fk_column_id!] &&
isSystemColumn(metaColumnById.value?.[field.fk_column_id!]) &&
!metaColumnById.value?.[field.fk_column_id!]?.pv
) {
return false
}
return field.show && metaColumnById?.value?.[field.fk_column_id!]
})
?.sort((a: Field, b: Field) => a.order - b.order)
?.map((field: Field) => metaColumnById?.value?.[field.fk_column_id!]) || []) as ColumnType[]
})
const toggleFieldVisibility = (checked: boolean, field: any) => {
const fieldIndex = fields.value?.findIndex((f) => f.fk_column_id === field.fk_column_id)
if (!fieldIndex && fieldIndex !== 0) return
addUndo({
undo: {
fn: (v: boolean) => {
field.show = !v
saveOrUpdate(field, fieldIndex)
const toggleFieldVisibility = (checked: boolean, field: any) => {
const fieldIndex = fields.value?.findIndex((f) => f.fk_column_id === field.fk_column_id)
if (!fieldIndex && fieldIndex !== 0) return
addUndo({
undo: {
fn: (v: boolean) => {
field.show = !v
saveOrUpdate(field, fieldIndex)
},
args: [checked],
},
args: [checked],
},
redo: {
fn: (v: boolean) => {
field.show = v
saveOrUpdate(field, fieldIndex)
redo: {
fn: (v: boolean) => {
field.show = v
saveOrUpdate(field, fieldIndex)
},
args: [checked],
},
args: [checked],
},
scope: defineViewScope({ view: view.value }),
})
saveOrUpdate(field, fieldIndex)
}
// reload view columns when active view changes
// or when columns count changes(delete/add)
watch(
[() => view?.value?.id, () => meta.value?.columns?.length],
async ([newViewId]) => {
// reload only if view belongs to current table
if (newViewId && view.value?.fk_model_id === meta.value?.id) {
isViewColumnsLoading.value = true
try {
await loadViewColumns()
} catch (e) {
console.error(e)
scope: defineViewScope({ view: view.value }),
})
saveOrUpdate(field, fieldIndex)
}
// reload view columns when active view changes
// or when columns count changes(delete/add)
watch(
[() => view?.value?.id, () => meta.value?.columns?.length],
async ([newViewId]) => {
// reload only if view belongs to current table
if (newViewId && view.value?.fk_model_id === meta.value?.id) {
isViewColumnsLoading.value = true
try {
await loadViewColumns()
} catch (e) {
console.error(e)
}
isViewColumnsLoading.value = false
}
isViewColumnsLoading.value = false
},
{ immediate: true },
)
const resizingColOldWith = ref('200px')
const updateGridViewColumn = async (id: string, props: Partial<GridColumnReqType>, undo = false) => {
if (!undo) {
const oldProps = Object.keys(props).reduce<Partial<GridColumnReqType>>((o: any, k) => {
if (gridViewCols.value[id][k as keyof GridColumnType]) {
if (k === 'width') o[k] = `${resizingColOldWith.value}px`
else o[k] = gridViewCols.value[id][k as keyof GridColumnType]
}
return o
}, {})
addUndo({
redo: {
fn: (w: Partial<GridColumnReqType>) => updateGridViewColumn(id, w, true),
args: [props],
},
undo: {
fn: (w: Partial<GridColumnReqType>) => updateGridViewColumn(id, w, true),
args: [oldProps],
},
scope: defineViewScope({ view: view.value }),
})
}
// sync with server if allowed
if (!isPublic.value && isUIAllowed('viewFieldEdit') && gridViewCols.value[id]?.id) {
await $api.dbView.gridColumnUpdate(gridViewCols.value[id].id as string, {
...props,
})
}
if (gridViewCols.value?.[id]) {
Object.assign(gridViewCols.value[id], {
...gridViewCols.value[id],
...props,
})
} else {
// fallback to reload
await loadViewColumns()
}
},
{ immediate: true },
)
return {
fields,
loadViewColumns,
filteredFieldList,
filterQuery,
showAll,
hideAll,
saveOrUpdate,
sortedAndFilteredFields,
showSystemFields,
metaColumnById,
toggleFieldVisibility,
isViewColumnsLoading,
}
}
return {
fields,
loadViewColumns,
filteredFieldList,
filterQuery,
showAll,
hideAll,
saveOrUpdate,
sortedAndFilteredFields,
showSystemFields,
metaColumnById,
toggleFieldVisibility,
isViewColumnsLoading,
updateGridViewColumn,
gridViewCols,
resizingColOldWith,
}
},
'useViewColumnsOrThrow',
)
export { useProvideViewColumns }
export function useViewColumnsOrThrow() {
const viewColumns = useViewColumns()
if (viewColumns == null) throw new Error('Please call `useProvideViewColumns` on the appropriate parent component')
return viewColumns
}

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

@ -81,7 +81,7 @@ export function useViewFilters(
const activeView = inject(ActiveViewInj, ref())
const { showSystemFields, metaColumnById } = useViewColumns(activeView, meta)
const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const options = computed<SelectProps['options']>(() =>
meta.value?.columns?.filter((c: ColumnType) => {

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

@ -1,6 +1,6 @@
import { type ColumnType, type SelectOptionsType, UITypes, type ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { GROUP_BY_VARS, ref, storeToRefs, useApi, useBase } from '#imports'
import { GROUP_BY_VARS, ref, storeToRefs, useApi, useBase, useViewColumnsOrThrow } from '#imports'
import type { Group, GroupNestedIn, Row } from '#imports'
export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: ComputedRef<string | undefined>) => {
@ -12,7 +12,7 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
const meta = inject(MetaInj)
const { gridViewCols } = useGridViewColumnOrThrow()
const { gridViewCols } = useViewColumnsOrThrow()
const groupBy = computed<{ column: ColumnType; sort: string; order?: number }[]>(() => {
const tempGroupBy: { column: ColumnType; sort: string; order?: number }[] = []

196
packages/nc-gui/ee/assets/img/fieldPlaceholder.svg

@ -1,196 +0,0 @@
<svg width="166" height="80" viewBox="0 0 166 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_784_33028" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="166" height="80">
<path d="M163.451 0.283691H2.54954C1.29794 0.283691 0.283325 1.29978 0.283325 2.55319V77.4468C0.283325 78.7002 1.29794 79.7163 2.54954 79.7163H163.451C164.702 79.7163 165.717 78.7002 165.717 77.4468V2.55319C165.717 1.29978 164.702 0.283691 163.451 0.283691Z" fill="white"/>
</mask>
<g mask="url(#mask0_784_33028)">
<path d="M163.451 0.283691H2.54954C1.29794 0.283691 0.283325 1.29978 0.283325 2.55319V77.4468C0.283325 78.7002 1.29794 79.7163 2.54954 79.7163H163.451C164.702 79.7163 165.717 78.7002 165.717 77.4468V2.55319C165.717 1.29978 164.702 0.283691 163.451 0.283691Z" fill="white"/>
<g filter="url(#filter0_dd_784_33028)">
<path d="M7.08193 3.68799H4.81572C4.18993 3.68799 3.68262 4.19603 3.68262 4.82274V7.09224C3.68262 7.71895 4.18993 8.227 4.81572 8.227H7.08193C7.70773 8.227 8.21504 7.71895 8.21504 7.09224V4.82274C8.21504 4.19603 7.70773 3.68799 7.08193 3.68799Z" fill="#3366FF"/>
<path d="M7.0819 3.82983H4.81569C4.26811 3.82983 3.82422 4.27437 3.82422 4.82274V7.09225C3.82422 7.64061 4.26811 8.08515 4.81569 8.08515H7.0819C7.62947 8.08515 8.07337 7.64061 8.07337 7.09225V4.82274C8.07337 4.27437 7.62947 3.82983 7.0819 3.82983Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 5.10645L5.52399 6.66673L4.8158 5.95751" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 3.68799H11.6144C10.9886 3.68799 10.4813 4.19603 10.4813 4.82274V7.09224C10.4813 7.71895 10.9886 8.227 11.6144 8.227H13.8806C14.5064 8.227 15.0137 7.71895 15.0137 7.09224V4.82274C15.0137 4.19603 14.5064 3.68799 13.8806 3.68799Z" fill="#E7E7E9"/>
<path d="M50.14 3.89429H18.413C17.7872 3.89429 17.2799 4.40233 17.2799 5.02904V6.88592C17.2799 7.51262 17.7872 8.02067 18.413 8.02067H50.14C50.7658 8.02067 51.2731 7.51262 51.2731 6.88592V5.02904C51.2731 4.40233 50.7658 3.89429 50.14 3.89429Z" fill="#E7E7E9"/>
<path d="M165.717 11.3475H0.283325V11.9149H165.717V11.3475Z" fill="#E7E7E9"/>
<g filter="url(#filter1_dd_784_33028)">
<path d="M7.08193 15.0355H4.81572C4.18993 15.0355 3.68262 15.5436 3.68262 16.1703V18.4398C3.68262 19.0665 4.18993 19.5745 4.81572 19.5745H7.08193C7.70773 19.5745 8.21504 19.0665 8.21504 18.4398V16.1703C8.21504 15.5436 7.70773 15.0355 7.08193 15.0355Z" fill="#3366FF"/>
<path d="M7.0819 15.1774H4.81569C4.26811 15.1774 3.82422 15.6219 3.82422 16.1703V18.4398C3.82422 18.9881 4.26811 19.4327 4.81569 19.4327H7.0819C7.62947 19.4327 8.07337 18.9881 8.07337 18.4398V16.1703C8.07337 15.6219 7.62947 15.1774 7.0819 15.1774Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 16.4539L5.52399 18.0141L4.8158 17.3049" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 15.0355H11.6144C10.9886 15.0355 10.4813 15.5436 10.4813 16.1703V18.4398C10.4813 19.0665 10.9886 19.5745 11.6144 19.5745H13.8806C14.5064 19.5745 15.0137 19.0665 15.0137 18.4398V16.1703C15.0137 15.5436 14.5064 15.0355 13.8806 15.0355Z" fill="#E7E7E9"/>
<path d="M75.6349 15.0355H18.413C17.7872 15.0355 17.2799 15.5436 17.2799 16.1703V18.4398C17.2799 19.0665 17.7872 19.5745 18.413 19.5745H75.6349C76.2607 19.5745 76.768 19.0665 76.768 18.4398V16.1703C76.768 15.5436 76.2607 15.0355 75.6349 15.0355Z" fill="#E7E7E9"/>
<path d="M165.717 22.6951H0.283325V23.2624H165.717V22.6951Z" fill="#E7E7E9"/>
<path d="M165.575 23.1206H0.424927V34.1844H165.575V23.1206Z" fill="#EBF0FF"/>
<g filter="url(#filter2_dd_784_33028)">
<path d="M7.08193 26.3829H4.81572C4.18993 26.3829 3.68262 26.891 3.68262 27.5177V29.7872C3.68262 30.4139 4.18993 30.9219 4.81572 30.9219H7.08193C7.70773 30.9219 8.21504 30.4139 8.21504 29.7872V27.5177C8.21504 26.891 7.70773 26.3829 7.08193 26.3829Z" fill="#3366FF"/>
<path d="M7.0819 26.5248H4.81569C4.26811 26.5248 3.82422 26.9693 3.82422 27.5177V29.7872C3.82422 30.3356 4.26811 30.7801 4.81569 30.7801H7.0819C7.62947 30.7801 8.07337 30.3356 8.07337 29.7872V27.5177C8.07337 26.9693 7.62947 26.5248 7.0819 26.5248Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 27.8014L5.52399 29.3617L4.8158 28.6525" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 26.3829H11.6144C10.9886 26.3829 10.4813 26.891 10.4813 27.5177V29.7872C10.4813 30.4139 10.9886 30.9219 11.6144 30.9219H13.8806C14.5064 30.9219 15.0137 30.4139 15.0137 29.7872V27.5177C15.0137 26.891 14.5064 26.3829 13.8806 26.3829Z" fill="#E7E7E9"/>
<path d="M50.14 26.5894H18.413C17.7872 26.5894 17.2799 27.0974 17.2799 27.7241V29.581C17.2799 30.2077 17.7872 30.7157 18.413 30.7157H50.14C50.7658 30.7157 51.2731 30.2077 51.2731 29.581V27.7241C51.2731 27.0974 50.7658 26.5894 50.14 26.5894Z" fill="#E7E7E9"/>
<path d="M165 23H1V34H165V23Z" stroke="#3366FF"/>
<g filter="url(#filter3_dd_784_33028)">
<path d="M7.08193 37.7305H4.81572C4.18993 37.7305 3.68262 38.2385 3.68262 38.8652V41.1347C3.68262 41.7614 4.18993 42.2695 4.81572 42.2695H7.08193C7.70773 42.2695 8.21504 41.7614 8.21504 41.1347V38.8652C8.21504 38.2385 7.70773 37.7305 7.08193 37.7305Z" fill="#3366FF"/>
<path d="M7.0819 37.8723H4.81569C4.26811 37.8723 3.82422 38.3169 3.82422 38.8652V41.1347C3.82422 41.6831 4.26811 42.1276 4.81569 42.1276H7.0819C7.62947 42.1276 8.07337 41.6831 8.07337 41.1347V38.8652C8.07337 38.3169 7.62947 37.8723 7.0819 37.8723Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 39.1489L5.52399 40.7092L4.8158 40" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 37.7305H11.6144C10.9886 37.7305 10.4813 38.2385 10.4813 38.8652V41.1347C10.4813 41.7614 10.9886 42.2695 11.6144 42.2695H13.8806C14.5064 42.2695 15.0137 41.7614 15.0137 41.1347V38.8652C15.0137 38.2385 14.5064 37.7305 13.8806 37.7305Z" fill="#E7E7E9"/>
<path d="M75.6349 37.7305H18.413C17.7872 37.7305 17.2799 38.2385 17.2799 38.8652V41.1347C17.2799 41.7614 17.7872 42.2695 18.413 42.2695H75.6349C76.2607 42.2695 76.768 41.7614 76.768 41.1347V38.8652C76.768 38.2385 76.2607 37.7305 75.6349 37.7305Z" fill="#E7E7E9"/>
<path d="M165.717 45.3901H0.283325V45.9575H165.717V45.3901Z" fill="#E7E7E9"/>
<g filter="url(#filter4_dd_784_33028)">
<path d="M7.08193 49.078H4.81572C4.18993 49.078 3.68262 49.586 3.68262 50.2128V52.4823C3.68262 53.109 4.18993 53.617 4.81572 53.617H7.08193C7.70773 53.617 8.21504 53.109 8.21504 52.4823V50.2128C8.21504 49.586 7.70773 49.078 7.08193 49.078Z" fill="#3366FF"/>
<path d="M7.0819 49.2198H4.81569C4.26811 49.2198 3.82422 49.6644 3.82422 50.2128V52.4823C3.82422 53.0306 4.26811 53.4752 4.81569 53.4752H7.0819C7.62947 53.4752 8.07337 53.0306 8.07337 52.4823V50.2128C8.07337 49.6644 7.62947 49.2198 7.0819 49.2198Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 50.4965L5.52399 52.0567L4.8158 51.3475" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 49.078H11.6144C10.9886 49.078 10.4813 49.586 10.4813 50.2128V52.4823C10.4813 53.109 10.9886 53.617 11.6144 53.617H13.8806C14.5064 53.617 15.0137 53.109 15.0137 52.4823V50.2128C15.0137 49.586 14.5064 49.078 13.8806 49.078Z" fill="#E7E7E9"/>
<path d="M50.14 49.2843H18.413C17.7872 49.2843 17.2799 49.7923 17.2799 50.4191V52.2759C17.2799 52.9026 17.7872 53.4107 18.413 53.4107H50.14C50.7658 53.4107 51.2731 52.9026 51.2731 52.2759V50.4191C51.2731 49.7923 50.7658 49.2843 50.14 49.2843Z" fill="#E7E7E9"/>
<path d="M165.717 56.7375H0.283325V57.3049H165.717V56.7375Z" fill="#E7E7E9"/>
<g filter="url(#filter5_dd_784_33028)">
<path d="M7.08193 60.4255H4.81572C4.18993 60.4255 3.68262 60.9336 3.68262 61.5603V63.8298C3.68262 64.4565 4.18993 64.9645 4.81572 64.9645H7.08193C7.70773 64.9645 8.21504 64.4565 8.21504 63.8298V61.5603C8.21504 60.9336 7.70773 60.4255 7.08193 60.4255Z" fill="#3366FF"/>
<path d="M7.0819 60.5674H4.81569C4.26811 60.5674 3.82422 61.0119 3.82422 61.5603V63.8298C3.82422 64.3782 4.26811 64.8227 4.81569 64.8227H7.0819C7.62947 64.8227 8.07337 64.3782 8.07337 63.8298V61.5603C8.07337 61.0119 7.62947 60.5674 7.0819 60.5674Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 61.844L5.52399 63.4043L4.8158 62.6951" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 60.4255H11.6144C10.9886 60.4255 10.4813 60.9336 10.4813 61.5603V63.8298C10.4813 64.4565 10.9886 64.9645 11.6144 64.9645H13.8806C14.5064 64.9645 15.0137 64.4565 15.0137 63.8298V61.5603C15.0137 60.9336 14.5064 60.4255 13.8806 60.4255Z" fill="#E7E7E9"/>
<path d="M75.6349 60.4255H18.413C17.7872 60.4255 17.2799 60.9336 17.2799 61.5603V63.8298C17.2799 64.4565 17.7872 64.9645 18.413 64.9645H75.6349C76.2607 64.9645 76.768 64.4565 76.768 63.8298V61.5603C76.768 60.9336 76.2607 60.4255 75.6349 60.4255Z" fill="#E7E7E9"/>
<path d="M165.717 68.0851H0.283325V68.6525H165.717V68.0851Z" fill="#E7E7E9"/>
<g filter="url(#filter6_dd_784_33028)">
<path d="M7.08193 71.7731H4.81572C4.18993 71.7731 3.68262 72.2811 3.68262 72.9078V75.1773C3.68262 75.804 4.18993 76.3121 4.81572 76.3121H7.08193C7.70773 76.3121 8.21504 75.804 8.21504 75.1773V72.9078C8.21504 72.2811 7.70773 71.7731 7.08193 71.7731Z" fill="#3366FF"/>
<path d="M7.0819 71.9149H4.81569C4.26811 71.9149 3.82422 72.3595 3.82422 72.9078V75.1773C3.82422 75.7257 4.26811 76.1702 4.81569 76.1702H7.0819C7.62947 76.1702 8.07337 75.7257 8.07337 75.1773V72.9078C8.07337 72.3595 7.62947 71.9149 7.0819 71.9149Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 73.1915L5.52399 74.7518L4.8158 74.0426" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 71.7731H11.6144C10.9886 71.7731 10.4813 72.2811 10.4813 72.9078V75.1773C10.4813 75.804 10.9886 76.3121 11.6144 76.3121H13.8806C14.5064 76.3121 15.0137 75.804 15.0137 75.1773V72.9078C15.0137 72.2811 14.5064 71.7731 13.8806 71.7731Z" fill="#E7E7E9"/>
<path d="M50.14 71.9792H18.413C17.7872 71.9792 17.2799 72.4873 17.2799 73.114V74.9709C17.2799 75.5976 17.7872 76.1056 18.413 76.1056H50.14C50.7658 76.1056 51.2731 75.5976 51.2731 74.9709V73.114C51.2731 72.4873 50.7658 71.9792 50.14 71.9792Z" fill="#E7E7E9"/>
<path d="M165.717 79.4326H0.283325V80H165.717V79.4326Z" fill="#E7E7E9"/>
</g>
<g filter="url(#filter7_d_784_33028)">
<path d="M115.669 34.6749C115.494 34.4499 115.275 33.9937 114.894 33.4249C114.675 33.1124 114.138 32.5187 113.975 32.2124C113.858 32.0264 113.824 31.7997 113.881 31.5874C113.979 31.1838 114.362 30.9161 114.775 30.9624C115.094 31.0266 115.388 31.1831 115.619 31.4124C115.78 31.5644 115.929 31.7296 116.063 31.9062C116.163 32.0312 116.188 32.0812 116.3 32.2249C116.413 32.3687 116.488 32.5124 116.431 32.2999C116.388 31.9874 116.313 31.4624 116.206 30.9937C116.125 30.6374 116.106 30.5812 116.031 30.3124C115.956 30.0437 115.913 29.8187 115.831 29.5124C115.757 29.2116 115.699 28.907 115.656 28.5999C115.577 28.2073 115.635 27.7995 115.819 27.4437C116.037 27.2383 116.357 27.1841 116.631 27.3062C116.907 27.5095 117.112 27.7935 117.219 28.1187C117.383 28.5189 117.492 28.9394 117.544 29.3687C117.644 29.9937 117.838 30.9062 117.844 31.0937C117.844 30.8624 117.8 30.3749 117.844 30.1562C117.887 29.9282 118.046 29.7389 118.263 29.6562C118.449 29.5991 118.646 29.5862 118.838 29.6187C119.031 29.6592 119.203 29.7707 119.319 29.9312C119.464 30.2958 119.544 30.6828 119.556 31.0749C119.573 30.7316 119.632 30.3916 119.731 30.0624C119.836 29.9153 119.988 29.8092 120.163 29.7624C120.369 29.7247 120.581 29.7247 120.788 29.7624C120.957 29.8191 121.105 29.9259 121.213 30.0687C121.345 30.4002 121.425 30.7502 121.45 31.1062C121.45 31.1937 121.494 30.8624 121.631 30.6437C121.703 30.4316 121.882 30.2737 122.101 30.2295C122.321 30.1853 122.547 30.2616 122.695 30.4295C122.843 30.5974 122.89 30.8316 122.819 31.0437C122.819 31.4499 122.819 31.4312 122.819 31.7062C122.819 31.9812 122.819 32.2249 122.819 32.4562C122.796 32.8219 122.746 33.1854 122.669 33.5437C122.56 33.8606 122.409 34.1613 122.219 34.4374C121.915 34.7749 121.665 35.1563 121.475 35.5687C121.428 35.7736 121.407 35.9836 121.413 36.1937C121.412 36.3879 121.437 36.5812 121.488 36.7687C121.232 36.7957 120.974 36.7957 120.719 36.7687C120.475 36.7312 120.175 36.2437 120.094 36.0937C120.054 36.0132 119.971 35.9622 119.881 35.9622C119.791 35.9622 119.709 36.0132 119.669 36.0937C119.531 36.3312 119.225 36.7624 119.013 36.7874C118.594 36.8374 117.731 36.7874 117.05 36.7874C117.05 36.7874 117.163 36.1624 116.906 35.9374C116.65 35.7124 116.388 35.4499 116.194 35.2749L115.669 34.6749Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M115.669 34.6749C115.494 34.4499 115.275 33.9937 114.894 33.4249C114.675 33.1124 114.138 32.5187 113.975 32.2124C113.858 32.0264 113.824 31.7997 113.881 31.5874C113.979 31.1838 114.362 30.9161 114.775 30.9624C115.094 31.0266 115.388 31.1831 115.619 31.4124C115.78 31.5644 115.929 31.7296 116.063 31.9062C116.163 32.0312 116.188 32.0812 116.3 32.2249C116.413 32.3687 116.488 32.5124 116.431 32.2999C116.388 31.9874 116.313 31.4624 116.206 30.9937C116.125 30.6374 116.106 30.5812 116.031 30.3124C115.956 30.0437 115.913 29.8187 115.831 29.5124C115.757 29.2116 115.699 28.907 115.656 28.5999C115.577 28.2073 115.635 27.7995 115.819 27.4437C116.037 27.2383 116.357 27.1841 116.631 27.3062C116.907 27.5095 117.112 27.7935 117.219 28.1187C117.383 28.5189 117.492 28.9394 117.544 29.3687C117.644 29.9937 117.838 30.9062 117.844 31.0937C117.844 30.8624 117.8 30.3749 117.844 30.1562C117.887 29.9282 118.046 29.7389 118.263 29.6562C118.449 29.5991 118.646 29.5862 118.838 29.6187C119.031 29.6592 119.203 29.7707 119.319 29.9312C119.464 30.2958 119.544 30.6828 119.556 31.0749C119.573 30.7316 119.632 30.3916 119.731 30.0624C119.836 29.9153 119.988 29.8092 120.163 29.7624C120.369 29.7247 120.581 29.7247 120.788 29.7624C120.957 29.8191 121.105 29.9259 121.213 30.0687C121.345 30.4002 121.425 30.7502 121.45 31.1062C121.45 31.1937 121.494 30.8624 121.631 30.6437C121.703 30.4316 121.882 30.2737 122.101 30.2295C122.321 30.1853 122.547 30.2616 122.695 30.4295C122.843 30.5974 122.89 30.8316 122.819 31.0437C122.819 31.4499 122.819 31.4312 122.819 31.7062C122.819 31.9812 122.819 32.2249 122.819 32.4562C122.796 32.8219 122.746 33.1854 122.669 33.5437C122.56 33.8606 122.409 34.1613 122.219 34.4374C121.915 34.7749 121.665 35.1563 121.475 35.5687C121.428 35.7736 121.407 35.9836 121.413 36.1937C121.412 36.3879 121.437 36.5812 121.488 36.7687C121.232 36.7957 120.974 36.7957 120.719 36.7687C120.475 36.7312 120.175 36.2437 120.094 36.0937C120.054 36.0132 119.971 35.9622 119.881 35.9622C119.791 35.9622 119.709 36.0132 119.669 36.0937C119.531 36.3312 119.225 36.7624 119.013 36.7874C118.594 36.8374 117.731 36.7874 117.05 36.7874C117.05 36.7874 117.163 36.1624 116.906 35.9374C116.65 35.7124 116.388 35.4499 116.194 35.2749L115.669 34.6749Z" stroke="black" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M120.969 35.0162V32.8588C120.969 32.7297 120.864 32.625 120.734 32.625C120.605 32.625 120.5 32.7297 120.5 32.8588V35.0162C120.5 35.1453 120.605 35.25 120.734 35.25C120.864 35.25 120.969 35.1453 120.969 35.0162Z" fill="black"/>
<path d="M119.731 35.0154L119.719 32.8569C119.718 32.7281 119.612 32.6243 119.483 32.625C119.354 32.6258 119.249 32.7308 119.25 32.8596L119.263 35.0181C119.263 35.1469 119.369 35.2508 119.498 35.25C119.628 35.2493 119.732 35.1442 119.731 35.0154Z" fill="black"/>
<path d="M118 32.8619L118.013 35.0159C118.013 35.1459 118.119 35.2508 118.248 35.25C118.378 35.2493 118.482 35.1432 118.481 35.0131L118.469 32.8591C118.468 32.7291 118.362 32.6243 118.233 32.625C118.104 32.6258 117.999 32.7318 118 32.8619Z" fill="black"/>
</g>
<path d="M163.451 0.283691H2.54954C1.29794 0.283691 0.283325 1.29978 0.283325 2.55319V77.4468C0.283325 78.7002 1.29794 79.7163 2.54954 79.7163H163.451C164.702 79.7163 165.717 78.7002 165.717 77.4468V2.55319C165.717 1.29978 164.702 0.283691 163.451 0.283691Z" stroke="#E7E7E9"/>
<defs>
<filter id="filter0_dd_784_33028" x="0.324219" y="3.32983" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter1_dd_784_33028" x="0.324219" y="14.6774" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter2_dd_784_33028" x="0.324219" y="26.0248" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter3_dd_784_33028" x="0.324219" y="37.3723" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter4_dd_784_33028" x="0.324219" y="48.7198" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter5_dd_784_33028" x="0.324219" y="60.0674" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter6_dd_784_33028" x="0.324219" y="71.4149" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter7_d_784_33028" x="112.679" y="26.8667" width="11.3476" height="12.118" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="0.4"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_784_33028" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 24 KiB

19
packages/nc-gui/helpers/parsers/CSVTemplateAdapter.ts

@ -85,7 +85,7 @@ export default class CSVTemplateAdapter {
detectInitialUidt(v: string) {
if (!isNaN(Number(v)) && !isNaN(parseFloat(v))) return UITypes.Number
if (validateDateWithUnknownFormat(v)) return UITypes.DateTime
if (['true', 'True', 'false', 'False', '1', '0', 'T', 'F', 'Y', 'N'].includes(v)) return UITypes.Checkbox
if (isCheckboxType(v)) return UITypes.Checkbox
return UITypes.SingleLineText
}
@ -101,18 +101,14 @@ export default class CSVTemplateAdapter {
} else if (colProps.uidt === UITypes.SingleLineText) {
if (isEmailType(colData)) {
colProps.uidt = UITypes.Email
}
if (isUrlType(colData)) {
} else if (isUrlType(colData)) {
colProps.uidt = UITypes.URL
} else if (isCheckboxType(colData)) {
colProps.uidt = UITypes.Checkbox
} else {
const checkboxType = isCheckboxType(colData)
if (checkboxType.length === 1) {
colProps.uidt = UITypes.Checkbox
} else {
if (data[columnIdx] && columnIdx < this.config.maxRowsToParse) {
this.columnValues[columnIdx].push(data[columnIdx])
colProps.uidt = UITypes.SingleSelect
}
if (data[columnIdx] && columnIdx < this.config.maxRowsToParse) {
this.columnValues[columnIdx].push(data[columnIdx])
colProps.uidt = UITypes.SingleSelect
}
}
} else if (colProps.uidt === UITypes.Number) {
@ -224,7 +220,6 @@ export default class CSVTemplateAdapter {
const data = (row.data as [])[columnIdx] === '' ? null : (row.data as [])[columnIdx]
if (column.uidt === UITypes.Checkbox) {
rowData[column.column_name] = getCheckboxValue(data)
rowData[column.column_name] = data
} else if (column.uidt === UITypes.SingleSelect || column.uidt === UITypes.MultiSelect) {
rowData[column.column_name] = (data || '').toString().trim() || null
} else {

11
packages/nc-gui/helpers/parsers/ExcelTemplateAdapter.ts

@ -144,13 +144,9 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
// check for long text
if (isMultiLineTextType(rows, col)) {
column.uidt = UITypes.LongText
}
if (isEmailType(rows, col)) {
} else if (isEmailType(rows, col)) {
column.uidt = UITypes.Email
}
if (isUrlType(rows, col)) {
} else if (isUrlType(rows, col)) {
column.uidt = UITypes.URL
} else {
const vals = rows
@ -158,8 +154,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
.map((r: any) => r[col])
.filter((v: any) => v !== null && v !== undefined && v.toString().trim() !== '')
const checkboxType = isCheckboxType(vals, col)
if (checkboxType.length === 1) {
if (isCheckboxType(vals, col)) {
column.uidt = UITypes.Checkbox
} else {
// Single Select / Multi Select

11
packages/nc-gui/helpers/parsers/parserHelpers.ts

@ -29,17 +29,15 @@ export const isCheckboxType: any = (values: [], col?: number) => {
let options = booleanOptions
for (let i = 0; i < values.length; i++) {
const val = getColVal(values[i], col)
if (val === null || val === undefined || val.toString().trim() === '') {
continue
}
options = options.filter((v) => val in v)
if (!options.length) {
return false
}
}
return options
return true
}
export const getCheckboxValue = (value: any) => {
@ -142,7 +140,9 @@ export const isEmailType = (colData: [], col?: number) =>
export const isUrlType = (colData: [], col?: number) =>
colData.some((r: any) => {
const v = getColVal(r, col)
return v && isURL(v)
// convert to string since isURL only accepts string
// and cell data value can be number or any other types
return v && isURL(v.toString())
})
export const getColumnUIDTAndMetas = (colData: [], defaultType: string) => {
@ -159,8 +159,7 @@ export const getColumnUIDTAndMetas = (colData: [], defaultType: string) => {
if (isUrlType(colData)) {
colProps.uidt = UITypes.URL
} else {
const checkboxType = isCheckboxType(colData)
if (checkboxType.length === 1) {
if (isCheckboxType(colData)) {
colProps.uidt = UITypes.Checkbox
} else {
Object.assign(colProps, extractMultiOrSingleSelectProps(colData))

5
packages/nc-gui/package.json

@ -48,6 +48,7 @@
"@vueuse/integrations": "^10.2.1",
"ant-design-vue": "^3.2.11",
"chart.js": "^4.3.0",
"crossoriginworker": "^1.1.0",
"d3-scale": "^4.0.2",
"dagre": "^0.8.5",
"dayjs": "^1.11.9",
@ -64,7 +65,7 @@
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",
"monaco-sql-languages": "^0.11.0",
"nocodb-sdk": "workspace:^",
"nocodb-sdk": "0.202.5",
"papaparse": "^5.3.2",
"parse-github-url": "^1.0.2",
"pinia": "^2.1.4",
@ -154,4 +155,4 @@
"vitest": "^0.30.1",
"windicss": "^3.5.6"
}
}
}

21
packages/nc-gui/pages/copy-shared-base.vue

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { useBase, useCopySharedBase, useRoute } from '#imports'
const route = useRoute()
const { sharedBaseId } = useCopySharedBase()
const { forcedProjectId } = storeToRefs(useBase())
onMounted(() => {
sharedBaseId.value = route.query.base as string
if (forcedProjectId?.value) forcedProjectId.value = undefined
navigateTo(`/`)
})
</script>
<template>
<div></div>
</template>
<style scoped></style>

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

@ -18,6 +18,8 @@ const { populateWorkspace } = useWorkspace()
const { signedIn } = useGlobal()
const { isUIAllowed } = useRoles()
const router = useRouter()
const route = router.currentRoute
@ -46,6 +48,10 @@ const isSharedFormView = computed(() => {
return routeName.startsWith('index-typeOrId-form-viewId')
})
const { sharedBaseId } = useCopySharedBase()
const isDuplicateDlgOpen = ref(false)
async function handleRouteTypeIdChange() {
// avoid loading bases for shared views
if (isSharedView.value) {
@ -82,7 +88,29 @@ watch(
// immediate watch, because if route is changed during page transition
// It will error out nuxt
onMounted(() => {
handleRouteTypeIdChange()
if (route.value.query?.continueAfterSignIn) {
localStorage.removeItem('continueAfterSignIn')
return navigateTo(route.value.query.continueAfterSignIn as string)
} else {
const continueAfterSignIn = localStorage.getItem('continueAfterSignIn')
if (continueAfterSignIn) {
return navigateTo({
path: continueAfterSignIn,
query: route.value.query,
})
}
}
handleRouteTypeIdChange().then(() => {
if (sharedBaseId.value) {
if (!isUIAllowed('baseDuplicate')) {
message.error('You are not allowed to create base')
return
}
isDuplicateDlgOpen.value = true
}
})
})
function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: string) {
@ -117,6 +145,7 @@ provide(ToggleDialogInj, toggleDialog)
v-model:data-sources-state="dataSourcesState"
:base-id="baseId"
/>
<DlgSharedBaseDuplicate v-if="isUIAllowed('baseDuplicate')" v-model="isDuplicateDlgOpen" />
</div>
</template>

2
packages/nc-gui/pages/index/[typeOrId]/[baseId]/index/index/[viewId]/[[viewTitle]].vue

@ -20,7 +20,7 @@ watch(
until(tables)
.toMatch((tables) => tables.length > 0)
.then(() => {
getMeta(viewId as string, true)
getMeta(viewId as string)
})
},
{ immediate: true },

6
packages/nc-gui/store/config.ts

@ -7,7 +7,9 @@ export const useConfigStore = defineStore('configStore', () => {
const sidebarStore = useSidebarStore()
const viewsStore = useViewsStore()
const { activeViewTitleOrId } = storeToRefs(viewsStore)
const tablesStore = useTablesStore()
const { activeTableId } = storeToRefs(tablesStore)
const isViewPortMobile = () => width.value < MAX_WIDTH_FOR_MOBILE_MODE
@ -47,7 +49,7 @@ export const useConfigStore = defineStore('configStore', () => {
const handleSidebarOpenOnMobileForNonViews = () => {
if (!isViewPortMobile()) return
if (!viewsStore.activeViewTitleOrId && !tablesStore.activeTableId) {
if (!activeViewTitleOrId && !activeTableId) {
nextTick(() => {
sidebarStore.isLeftSidebarOpen = true
})
@ -56,7 +58,7 @@ export const useConfigStore = defineStore('configStore', () => {
}
}
watch([viewsStore.activeViewTitleOrId, tablesStore.activeTableId], () => {
watch([activeViewTitleOrId, activeTableId], () => {
handleSidebarOpenOnMobileForNonViews()
})

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

@ -3,10 +3,22 @@ import { MAX_WIDTH_FOR_MOBILE_MODE } from '~/lib'
export const useSidebarStore = defineStore('sidebarStore', () => {
const { width } = useWindowSize()
const isViewPortMobile = () => width.value < MAX_WIDTH_FOR_MOBILE_MODE
const isViewPortMobile = () => {
return width.value < MAX_WIDTH_FOR_MOBILE_MODE
}
const { isMobileMode } = useGlobal()
const isLeftSidebarOpen = ref(!isViewPortMobile())
const tablesStore = useTablesStore()
const _isLeftSidebarOpen = ref(!isViewPortMobile())
const isLeftSidebarOpen = computed({
get() {
return (isMobileMode.value && !tablesStore.activeTableId) || _isLeftSidebarOpen.value
},
set(value) {
_isLeftSidebarOpen.value = value
},
})
const isRightSidebarOpen = ref(true)
const leftSidebarWidthPercent = ref(isViewPortMobile() ? 0 : 20)

3
packages/nc-gui/store/views.ts

@ -170,15 +170,12 @@ export const useViewsStore = defineStore('viewsStore', () => {
return
}
isViewsLoading.value = true
isViewDataLoading.value = true
try {
await loadViews()
} catch (e) {
console.error(e)
} finally {
isViewsLoading.value = false
}
},
{ immediate: true },

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

@ -98,21 +98,17 @@ export const fieldRequiredValidator = () => {
}
}
export const fieldLengthValidator = (sqlClientType: string) => {
export const fieldLengthValidator = () => {
return {
validator: (rule: any, value: any) => {
const { t } = getI18n().global
// no limit for sqlite but set as 255
let fieldLengthLimit = 255
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
fieldLengthLimit = 64
} else if (sqlClientType === 'pg') {
fieldLengthLimit = 59
} else if (sqlClientType === 'mssql') {
fieldLengthLimit = 128
}
// mysql allows 64 characters for column_name
// postgres allows 59 characters for column_name
// mssql allows 128 characters for column_name
// sqlite allows any number of characters for column_name
// We allow 255 for all databases, truncate will be handled by backend for column_name
const fieldLengthLimit = 255
return new Promise((resolve, reject) => {
if (value?.length > fieldLengthLimit) {

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

@ -1,18 +1,11 @@
// Returns a blob:// URL which points
// to a javascript file which will call
// importScripts with the given URL
export function getWorkerURL(url: string) {
const content = `importScripts( "${url}" );`
return URL.createObjectURL(new Blob([content], { type: 'text/javascript' }))
}
import getCrossOriginWorkerURL from 'crossoriginworker'
export function initWorker(url: string) {
export async function initWorker(url: string) {
let worker: Worker | null = null
try {
if (/^https?:\/\/'/.test(url)) {
const worker_url = getWorkerURL(url)
worker = new Worker(worker_url)
URL.revokeObjectURL(worker_url)
if (/^https?:\/\//.test(url)) {
const workerURL = await getCrossOriginWorkerURL(url)
worker = new Worker(workerURL)
} else {
worker = new Worker(url, {
type: 'module',

9
packages/nc-gui/windi.config.ts

@ -69,6 +69,15 @@ export default defineConfig({
min: '1780px',
},
},
fontWeight: {
thin: 150,
extraLight: 250,
light: 350,
normal: 450,
medium: 550,
bold: 650,
black: 750,
},
textColor: {
primary: 'rgba(var(--color-primary), var(--tw-text-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-text-opacity))',

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

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

30
packages/noco-docs/docs/010.index.md

@ -4,32 +4,30 @@ description: 'NocoDB Documentation'
slug: /
---
## Welcome!
![image](/img/banner.png)
NocoDB is a no-code database platform that allows teams to collaborate and build applications with ease of a familiar and intuitive spreadsheet interface. This allows even non-developers or business users to become software creators.
NocoDB works by connecting to any relational database and transforming them into a smart spreadsheet interface! This allows you to build no-code applications collaboratively with teams. NocoDB currently works with MySQL, PostgreSQL, Microsoft SQL Server, SQLite, Amazon Aurora & MariaDB databases.
NocoDB works by connecting to any relational database and transforming them into a smart spreadsheet interface! This allows you to build no-code applications collaboratively with teams. NocoDB currently works with MySQL, PostgreSQL & SQLite (only in Open Source) databases.
Also NocoDB's app store allows you to build business workflows on views with combination of Slack, Microsoft Teams, Discord, Twilio, Whatsapp, Email & any 3rd party APIs too. Plus NocoDB provides programmatic access to APIs so that you can build integrations with Zapier / Integromat and custom applications too.
Also, NocoDB's app store allows you to build business workflows on views with combination of Slack, Microsoft Teams, Discord, Twilio, Whatsapp, Email & any 3rd party APIs too. Plus NocoDB provides programmatic access to APIs so that you can build integrations with Zapier / Integromat and custom applications too.
## Features
### Rich Spreadsheet Interface
- ⚡ &nbsp;Basic Operations: Create, Read, Update and Delete on Tables, Columns, and Rows
- ⚡ &nbsp;Fields Operations: Sort, Filter, Hide / Unhide Columns
- ⚡ &nbsp;Basic Operations: Create, Read, Update and Delete on Tables, Fields, and Records
- ⚡ &nbsp;Fields Operations: Sort, Filter, Hide / Un-hide Fields
- ⚡ &nbsp;Multiple Views Types: Grid (By default), Gallery, Form View and Kanban View
- ⚡ &nbsp;View Permissions Types: Collaborative Views, & Locked Views
- ⚡ &nbsp;View Permissions Types: Collaborative Views & Locked Views
- ⚡ &nbsp;Share Bases / Views: either Public or Private (with Password Protected)
- ⚡ &nbsp;Variant Cell Types: ID, LinkToAnotherRecord, Lookup, Rollup, SingleLineText, Attachement, Currency, Formula and etc
- ⚡ &nbsp;Variant Cell Types: ID, Links, Lookup, Rollup, SingleLineText, Attachment, Currency, Formula and etc
- ⚡ &nbsp;Access Control with Roles : Fine-grained Access Control at different levels
- ⚡ &nbsp;and more ...
### App Store for Workflow Automations
We provide different integrations in three main categories. See <a href="/setup-and-usages/account-settings#app-store" target="_blank">App Store</a> for details.
We provide different integrations in three main categories. See <a href="/account-settings/oss-specific-details#app-store" target="_blank">App Store</a> for details.
- ⚡ &nbsp;Chat : Slack, Discord, Mattermost, and etc
- ⚡ &nbsp;Email : AWS SES, SMTP, MailerSend, and etc
@ -37,24 +35,25 @@ We provide different integrations in three main categories. See <a href="/setup-
### Programmatic Access
We provide the following ways to let users to invoke actions in a programmatic way. You can use a token (either JWT or Social Auth) to sign your requests for authorization to NocoDB.
We provide the following ways to let users invoke actions in a programmatic way. You can use a token (either JWT or Social Auth) to sign your requests for authorization to NocoDB.
- ⚡ &nbsp;REST APIs
- ⚡ &nbsp;NocoDB SDK
- ⚡ &nbsp;SQL Access : Run SQL queries on your data source from NocoDB UI itself (Coming soon!)
### Sync Schema
We allow you to sync schema changes if you have made changes outside NocoDB GUI. However, it has to be noted then you will have to bring your own schema migrations for moving from environment to others. See <a href="/setup-and-usages/sync-schema" target="_blank">Sync Schema</a> for details.
We allow you to sync schema changes if you have made changes outside NocoDB GUI. However, it has to be noted then you will have to bring your own schema migrations for moving from environment to others. See <a href="/data-sources/sync-with-data-source" target="_blank">Sync Schema</a> for details.
### Audit
### Audit
We are keeping all the user operation logs under one place. See <a href="/setup-and-usages/audit" target="_blank">Audit</a> for details.
We are keeping all the user operation logs under one place. See <a href="/data-sources/actions-on-data-sources#audit-logs" target="_blank">Audit</a> for details.
## Why are we building this?
Most internet businesses equip themselves with either spreadsheet or a database to solve their business needs. Spreadsheets are used by a Billion+ humans collaboratively every single day. However, we are way off working at similar speeds on databases which are way more powerful tools when it comes to computing. Attempts to solve this with SaaS offerings has meant horrible access controls, vendor lockin, data lockin, abrupt price changes & most importantly a glass ceiling on what's possible in future.
Most internet businesses equip themselves with either spreadsheet or a database to solve their business needs. Spreadsheets are used by a Billion+ humans collaboratively every single day. However, we are way off working at similar speeds on databases which are way more powerful tools when it comes to computing. Attempts to solve this with SaaS offerings has meant horrible access controls, vendor lockin, data lockin, abrupt price changes & most importantly a glass ceiling on what's possible in the future.
## Our Mission
Our mission is to provide the most powerful no-code interface for databases which is open source to every single internet business in the world. This would not only democratise access to a powerful computing tool but also bring forth a billion+ people who will have radical tinkering-and-building abilities on internet.
Our mission is to provide the most powerful no-code interface for databases which is Open Source to every single internet business in the world. This would not only democratise access to a powerful computing tool but also bring forth a billion+ people who will have radical tinkering-and-building abilities on internet.
## How can I contribute to NocoDB's development ?
@ -73,3 +72,4 @@ Follow us for daily updates, content, and other activities with our community:
- [Community Forums](https://community.nocodb.com/)
- [LinkedIn](https://www.linkedin.com/company/nocodb)
- [Reddit](https://www.reddit.com/r/NocoDB/)

67
packages/noco-docs/docs/020.getting-started/020.environment-variables.md

@ -1,67 +0,0 @@
---
title: 'Environment Variables'
description: 'Environment Variables for NocoDB!'
hide_table_of_contents: true
---
For production usecases, it is **recommended** to configure
- `NC_DB`,
- `NC_AUTH_JWT_SECRET`,
- `NC_PUBLIC_URL`,
- `NC_REDIS_URL`
| Variable | Comments | If absent |
|---|---|---|
| NC_DB | See our example database URLs [here](https://github.com/nocodb/nocodb#docker). | A local SQLite will be created in root folder if `NC_DB` is not provided |
| NC_DB_JSON | Can be used instead of `NC_DB` and value should be valid knex connection JSON | |
| NC_DB_JSON_FILE | Can be used instead of `NC_DB` and value should be a valid path to knex connection JSON | |
| DATABASE_URL | JDBC URL Format. Can be used instead of NC_DB. | |
| DATABASE_URL_FILE | Can be used instead of DATABASE_URL: path to file containing JDBC URL Format. | |
| NC_AUTH_JWT_SECRET | JWT secret used for auth and storing other secrets | A random secret will be generated |
| PORT | For setting app running port | `8080` |
| DB_QUERY_LIMIT_DEFAULT | Default pagination limit | 25 |
| DB_QUERY_LIMIT_MAX | Maximum allowed pagination limit | 1000 |
| DB_QUERY_LIMIT_MIN | Minimum allowed pagination limit | 1 |
| NC_TOOL_DIR | App directory to keep metadata and app related files | Defaults to current working directory. In docker maps to `/usr/app/data/` for mounting volume. |
| NC_PUBLIC_URL | Used for sending Email invitations | Best guess from http request params |
| NC_JWT_EXPIRES_IN | JWT token expiry time | `10h` |
| NC_CONNECT_TO_EXTERNAL_DB_DISABLED | Disable Project creation with external database | |
| NC_INVITE_ONLY_SIGNUP | Removed since version 0.99.0 and now it's recommended to use [super admin settings menu](/setup-and-usages/account-settings#enable--disable-signup). Allow users to signup only via invite url, value should be any non-empty string. | |
| NUXT_PUBLIC_NC_BACKEND_URL | Custom Backend URL | ``http://localhost:8080`` will be used |
| NC_REQUEST_BODY_SIZE | Request body size [limit](https://expressjs.com/en/resources/middleware/body-parser.html#limit) | `1048576` |
| NC_EXPORT_MAX_TIMEOUT | After NC_EXPORT_MAX_TIMEOUT csv gets downloaded in batches | Default value 5000(in millisecond) will be used |
| NC_DISABLE_TELE | Disable telemetry | |
| NC_DASHBOARD_URL | Custom dashboard url path | `/dashboard` |
| NC_GOOGLE_CLIENT_ID | Google client id to enable google authentication | |
| NC_GOOGLE_CLIENT_SECRET | Google client secret to enable google authentication | |
| NC_MIGRATIONS_DISABLED | Disable NocoDB migration | |
| NC_MIN | If set to any non-empty string the default splash screen(initial welcome animation) and matrix screensaver will disable | |
| NC_SENTRY_DSN | For Sentry monitoring | |
| NC_REDIS_URL | Custom Redis URL. Example: `redis://:authpassword@127.0.0.1:6380/4` | Meta data will be stored in memory |
| NC_DISABLE_ERR_REPORT | Disable error reporting | |
| NC_DISABLE_CACHE | To be used only while debugging. On setting this to `true` - meta data be fetched from db instead of redis/cache. | `false` |
| AWS_ACCESS_KEY_ID | For Litestream - S3 access key id | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 |
| AWS_SECRET_ACCESS_KEY | For Litestream - S3 secret access key | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 |
| AWS_BUCKET | For Litestream - S3 bucket | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 |
| AWS_BUCKET_PATH | For Litestream - S3 bucket path (like folder within S3 bucket) | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 |
| NC_SMTP_FROM | For SMTP plugin - Email sender address | |
| NC_SMTP_HOST | For SMTP plugin - SMTP host value | |
| NC_SMTP_PORT | For SMTP plugin - SMTP port value | |
| NC_SMTP_USERNAME | For SMTP plugin (Optional) - SMTP username value for authentication | |
| NC_SMTP_PASSWORD | For SMTP plugin (Optional) - SMTP password value for authentication | |
| NC_SMTP_SECURE | For SMTP plugin (Optional) - To enable secure set value as `true` any other value treated as false | |
| NC_SMTP_IGNORE_TLS | For SMTP plugin (Optional) - To ignore tls set value as `true` any other value treated as false. For more info visit https://nodemailer.com/smtp/ | |
| NC_S3_BUCKET_NAME | For S3 storage plugin - AWS S3 bucket name | |
| NC_S3_REGION | For S3 storage plugin - AWS S3 region | |
| NC_S3_ACCESS_KEY | For S3 storage plugin - AWS access key credential for accessing resource | |
| NC_S3_ACCESS_SECRET | For S3 storage plugin - AWS access secret credential for accessing resource | |
| NC_ADMIN_EMAIL | For updating/creating super admin with provided email and password | |
| NC_ATTACHMENT_FIELD_SIZE | For setting the attachment field size(in Bytes) | Defaults to 20MB |
| NC_ADMIN_PASSWORD | For updating/creating super admin with provided email and password. Your password should have at least 8 letters with one uppercase, one number and one special letter(Allowed special chars $&+,:;=?@#\|'.^*()%!_-" ) | |
| NODE_OPTIONS | For passing Node.js [options](https://nodejs.org/api/cli.html#node_optionsoptions) to instance | |
| NC_MINIMAL_DBS | Create a new SQLite file for each project. All the db files are stored in `nc_minimal_dbs` folder in current working directory. (This option restricts project creation on external sources) | |
| NC_DISABLE_AUDIT | Disable Audit Log | `false` |
| NC_AUTOMATION_LOG_LEVEL | Possible Values: `OFF`, `ERROR`, `ALL`. See [Webhooks](/developer-resources/webhooks#call-log) for details. | `OFF` |
| NC_SECURE_ATTACHMENTS | Allow accessing attachments only through presigned urls. To enable set value as `true` any other value treated as false. (⚠ this will make existing links inaccessible ⚠) | `false` |
| NC_ATTACHMENT_EXPIRE_SECONDS | How many seconds before expiring presigned attachment urls. (Attachments will expire in at least set seconds and at most 10mins after set time) | 7200 (2 hours) |
| NC_ALLOW_LOCAL_HOOKS | To enable set value as `true` any other value treated as false. (⚠ this will allow webhooks to call local links which can raise security issues ⚠) | `false` |

83
packages/noco-docs/docs/020.getting-started/020.quick-start.md

@ -0,0 +1,83 @@
---
title: 'Quick start'
keywords: ['NocoDB', 'quick start', 'getting started']
---
NocoDB offers you the flexibility of two distinct variants: self-hosted and SaaS (cloud-hosted). In this section, we'll walk you through the initial steps to embark on your NocoDB journey, whether you choose to set it up on your own infrastructure or opt for the convenience of our cloud-hosted service. Let's dive in and unlock the potential of NocoDB for your data management needs.
:::tip
For this introductory guide, we suggest utilizing the hosted (SaaS) option. New users can take advantage of a complimentary trial period to explore the platform's features.
:::
## Self Hosted
When you opt for self-hosting, you gain the advantage of running an application on a server that you have direct control over. This grants you the flexibility to choose whether to host the database on your premises or within a server leased from a data center. In this self-administered arrangement, typically found on-premises, you assume full responsibility for the management and upkeep of your server. This level of control ensures that you have complete authority over all aspects of your data and services, eliminating any reliance on third-party service providers. This option is ideal for organizations that require a high degree of control over their data and services, but it does necessitate a higher level of technical expertise.
Setting up NocoDB on your own server is a straightforward process & the articles below will guide you through the steps to get started.
- [Installation](/getting-started/self-hosted/installation)
- [Environment Variables](/getting-started/self-hosted/environment-variables)
- [Upgrading](/getting-started/self-hosted/upgrading)
## SaaS (Cloud Hosted)
### Sign up for a NocoDB account
To get started with NocoDB's cloud-hosted service, follow these steps to sign up for an account:
1. Visit the [NocoDB website](https://www.nocodb.com).
2. Click on the "Start For Free" button located in the top right corner.
3. `Sign Up` using Google account. Alternatively, you can also sign up using your email address.
4. Check your email for a verification link and follow the instructions to verify your account.
Once you've successfully signed up, you will land on the NocoDB dashboard with a default workspace created for you.
### Create another workspace
After signing up and logging in to your NocoDB account, a default workspace is automatically created for you. You can use this workspace or [create a new one](/workspaces/create-workspace)
Your new workspace is now ready, and you can start building your base within it.
### Build a Base
Building a base in NocoDB is where you define the structure of your database, create tables to store your data, add fields to those tables, and establish relationships between tables using Links. This relational approach helps you organize and manage your data effectively, making it easier to work with complex datasets and build powerful applications. Add a [new Base](/bases/create-base). Alternatively, you can also [import existing base from Airtable](/bases/import-base-from-airtable) as a NocoDB project in minutes.
#### Create Tables
Tables are where you store your data like a spreadsheet grid, with data arranged in rows & columns. Once the base is created, you can start [adding new tables](/tables/create-table) to it. You can also [import existing structure data from a CSV, Excel, or JSON file](/tables/create-table-via-import) to populate your tables.
#### Add Fields
Within each table, [define the fields](/fields/fields-overview) that will hold your data. Fields are columnar data containers that hold a specific type of data. Fields can represent various types of information, such as text, numbers, dates, and more. You can find complete list of supported field types [here](/fields/fields-overview).
Use multi-fields editor to manage fields in bulk quickly - add fields, edit them, reorder, change visibility, reconfigure - all from one single window. [Read more](/fields/multi-fields-editor).
#### Establish Relationships with Links
One of NocoDB's powerful features is the ability to establish relationships between tables using [Links](/fields/field-types/links-based/links). Links enable you to connect related data across tables. For example, you can link "Tasks" to a specific "Project" by creating a Link field in the "Projects" table that points to the "Tasks" table.
#### Add Records
Once you have created tables and defined the necessary fields, it's time to start adding records to your base tables. Records are individual entries or rows within your tables, and they contain the actual data you want to store and manage. You can [add records manually](/records/create-record) or [upload existing data sets from CSV](/tables/import-data-into-existing-table) files.
#### Create views
Views are customized ways of displaying your data. You can [create multiple views](/views/create-view) for each table in your base, and each view can have its own set of fields, filters, and layouts. NocoDB offers a variety of view types for custom display, including [Grid](/views/view-types/grid), [Kanban](/views/view-types/kanban), and [Gallery](/views/view-types/gallery). If you are collecting data from a form, you can also create a [Form view](/views/view-types/form) to display the data in a form format.
#### Connect your data sources
Not just creating base & tables from the scratch, NocoDB also allows you to get started quickly by [connecting to your existing data sources](/data-sources/data-source-overview) & bring the comfort of spreadsheet UI to your data source.
### Collaborate with your team
NocoDB makes it easy to collaborate with your team members on your workspaces and bases. You can [invite team members to your workspace](/collaboration/workspace-collaboration) and [share your bases with them](/collaboration/base-collaboration). You can also [assign roles and permissions](/roles-and-permissions/roles-permissions-overview) to your team members to control their access to your bases.
Want to share information publicly? You can [create a public link to your base](/collaboration/share-base) and share it with anyone. You can also [embed your base](/collaboration/share-base#embeddable-frame) on your website or blog.
That's it! You're now ready to harness the power of NocoDB for your data management needs.

46
packages/noco-docs/docs/020.getting-started/030.terminologies.md

@ -0,0 +1,46 @@
---
title: 'NocoDB terminologies'
keywords: ['NocoDB terminologies', 'NocoDB terms', 'NocoDB keywords']
---
To maximize your use of NocoDB, it's beneficial to become familiar with some common terms.
Within this section, we've provided straightforward explanations for keywords and phrases that are relevant to NocoDB's functionality and capabilities. This resource is designed to assist you in gaining a deeper understanding of NocoDB as you embark on the process of setting up workspaces, bases, tables, and various other elements within the platform.
[//]: # (TBD : Link)
## Layout
The layout below shows the main parts of NocoDB's user interface.
![image](/img/v2/layout-overview.png)
1. **Sidebar**: This component provides quick access to workspaces, bases, tables, and views, facilitating easy navigation within NocoDB.
2. **Navbar**: In the Navbar, you'll find essential tools like the Switcher, allowing users to switch between data mode and details mode, and the Share button for collaborative workspace and view sharing.
3. **Toolbar**: The Toolbar equips users with various tools to manage records within tables and views effectively. It includes features for sorting, filtering, and editing data, enhancing your control over information.
4. **View Area**: The central workspace where data can be viewed in multiple formats - Grid, Gallery, Kanban or Form.
5. **Footbar**: A pagination bar for navigating between paginated records.
## Terminologies
| Term | Description |
|-----------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Base | A base is a collection of one or more tables, often related and linked to one another. Alternative term `Project` |
| Base Owner | Member who had created base. There can be only one owner for a base & ownership is non-transferable |
| Base member | Base member with specific access permissions. Alternative term `Member` `User` |
| Cell | A cell represents the space created where a column and a row intersect within a grid-like structure. It serves as the fundamental unit for storing data |
| Field | A field or column in a table is a container for values that share the same data type. Alternative term `Column` |
| Modal | In the context of user interfaces, a modal is a type of dialog or overlay that appears on top of the main content or interface and requires user interaction before the user can proceed. Modals are often used to gather user input, display alerts, or confirm actions. They typically "block" the rest of the interface until the user dismisses them, making them a focused and attention-grabbing element |
| Record | A record represents a row in a table. Alternative term `Row` |
| Table | A base is housed in tables, where data is logically arranged into rows and columns. A base can have multiple tables. Alternative term `Model` |
| Term | Description |
| View | A view defines how data within a table is presented and interacted with. Default view type is the grid view; other supported view types include form, gallery, and kanban views. |
| Webhook | A webhook is a mechanism that allows one system to send real-time data to another system or application. It enables automated notifications and data synchronization between different services or platforms by triggering predefined actions or events in response to specific events or updates in the source system. |
| Workspace Owner | Member who had created workspace. There can be only one owner for a workspace & ownership is non-transferable |
| Workspace member | Workspace member with specific access permissions. Alternative term `Member` `User` |
| Workspace | A workspace is a collection of one or more bases. It offers collaborative access to bases by enabling you to bring together interconnected tables, views, and various elements into a unified and organized entity |

72
packages/noco-docs/docs/020.getting-started/030.upgrading.md

@ -1,72 +0,0 @@
---
title: 'Upgrading'
description: 'Upgrading NocoDB : Docker, Node and Homebrew!'
---
By default, if `NC_DB` is not specified upon [installation](/getting-started/installation), then SQLite will be used to store metadata. We suggest users to separate the metadata and user data in different databases as pictured in our [architecture](/engineering/architecture).
## Docker
### Find, Stop & Delete NocoDB Docker Container
```bash
# find NocoDB container ID
docker ps
# stop NocoDB container
docker stop <YOUR_CONTAINER_ID>
# delete NocoDB container
docker rm <YOUR_CONTAINER_ID>
```
Note: Deleting your docker container without setting `NC_DB` or mounting to a persistent volume for a default SQLite database will result in losing your data. See examples below.
### Find & Remove NocoDB Docker Image
```bash
# find NocoDB image
docker images
# delete NocoDB image
docker rmi <YOUR_IMAGE_ID>
```
### Pull the latest NocoDB image with same environment variables
```bash
docker run -d -p 8080:8080 \
-e NC_DB="<YOUR_NC_DB_URL>" \
-e NC_AUTH_JWT_SECRET="<YOUR_NC_AUTH_JWT_SECRET_IF_GIVEN>" \
nocodb/nocodb:latest
```
Updating nocodb docker container is similar to updating [any other docker containers](https://www.whitesourcesoftware.com/free-developer-tools/blog/update-docker-images/).
### Example: Docker Upgrade
![Screen Shot 2021-09-16 at 09 23 07](https://user-images.githubusercontent.com/5435402/133578984-53c6b96b-3e8b-4a96-b6c2-36f3c09ffdde.png)
## Node
Updating docker container is similar to updating a npm package.
From your root folder
#### Uninstall NocoDB package
```bash
npm uninstall nocodb
```
#### Install NocoDB package
```bash
npm install --save nocodb
```
## Homebrew
Run following commands to upgrade Homebrew Nocodb version.
```bash
# Update the local homebrew formulas
brew update
# Upgrade nocodb package
brew upgrade nocodb
```

113
packages/noco-docs/docs/020.getting-started/040.keyboard-shortcuts.md

@ -0,0 +1,113 @@
---
title: 'Keyboard shortcuts'
tags: ['Getting Started', 'Shortcuts', 'Productivity hacks']
keywords: ['keyboard shortcuts', 'shortcuts', 'keyboard']
---
## Quick Actions ☁ {#quick-actions}
:::note
This feature is available only in NocoDB Cloud hosted version.
:::
`⌘` + `K` (or `Ctrl` + `K` on Windows) is a keyboard shortcut to quickly navigate between different workspace, table, view, or a menu items. For example, if you want to quickly navigate to the "API Tokens" page, you can open Quick Actions menu using ⌘+K, type "Token" in the search box and press enter.
This shortcut is often referred to as "Command-K". It's a great way to save time when you're navigating around NocoDB.
Command-K menu can also be accessed via the "Quick Actions" button in the top left corner of the screen.
![Quick Actions](/img/v2/cmd-k.png)
To navigate within ⌘+K menu,
- Use `↑` `↓` to navigate between listed items
- Use `Enter` to select an item
- Use `Backspace` to move to parent menu
- Use `Esc` to close the menu
## Recent Views
:::note
This feature is available only in NocoDB Cloud hosted version.
:::
Access recently visited views quickly using `⌘` + `L` (or `Ctrl` + `L` on Windows). Search results will be displayed in a modal window; click on the result to open the view.
![Recent Views](/img/v2/cmd-l.png)
To navigate within ⌘+K menu,
- Use `↑` `↓` to navigate between listed items
- Use `Enter` to select an item
- Use `Backspace` to move to parent menu
- Use `Esc` to close the menu
## Search in Docs
:::note
This feature is available only in NocoDB Cloud hosted version.
:::
Quickly search through docs from within NocoDB UI using `⌘` + `J` (or `Ctrl` + `J` on Windows). Search results will be displayed in a modal window; click on the result to open the page in a new tab.
To navigate within ⌘+K menu,
- Use `↑` `↓` to navigate between listed items
- Use `Enter` to select an item
- Use `Backspace` to move to parent menu
- Use `Esc` to close the menu
![Search in Docs](/img/v2/cmd-j.png)
## General shortcuts
| Key | Behaviour |
|------------:|:--------------------------------|
| `alt` + `t` | Opens new table modal |
| `alt` + `c` | Opens new field modal |
| `alt` + `f` | Toggles fullscreen mode |
| `alt` + `i` | Opens share button modal |
| `⌘` + `k` | Opens Quick Actions modal |
## Grid view shortcuts
| Key | Behaviour |
|----------------:|:-------------------------------------------------------------------------------------|
| `←` `→` `↑` `↓` | General cell navigation |
| `Delete` | Clear cell |
| `Space` | Expand current record |
| `Tab` | Move to next cell horizontally; if on last cell, move to beginning of next record |
| `Esc` | Exit cell EDIT mode |
| `Enter` | Switch cell in focus to EDIT mode; opens modal/picker if cell is associated with one |
| `⌘` + `↑` | Jump to first record in this field (in same page) |
| `⌘` + `↓` | Jump to last record in this field (in same page) |
| `⌘` + `←` | Jump to first field in this record |
| `⌘` + `→` | Jump to last field in this record |
| `⌘` + `c` | Copy cell contents |
| `⌘` + `v` | Paste copied contents |
| `alt` + `r` | Inserts new record in grid view |
## Field type specific shortcuts
| Datatype | Key | Behaviour |
|:----------------------:|------------:|:-----------------------------------|
| Text & Numerical cells | `←` `→` | Move cursor to the left / right |
| | `↑` `↓` | Move cursor to the beginning / end |
| Single Select | `↑` `↓` | Move between options |
| | `Enter` | Select option |
| Multi Select | `↑` `↓` | Move between options |
| | `Enter` | Select / deselect option |
| Link | `↑` `↓` | Move between options |
| | `Enter` | Link current selection |
| Checkbox | `Enter` | Toggle |
| Rating | `<0 ~ Max>` | Enter number to toggle rating |
## Expanded form shortcuts
| Key | Behaviour |
|--------------:|:----------------------------------|
| `⌘` + `Enter` | Save current expanded form item |
| `alt` + `→` | Switch to next record |
| `alt` + `←` | Switch to previous record |
| `alt` + `S` | Save current expanded form record |
| `alt` + `N` | Create a new record |

48
packages/noco-docs/docs/020.getting-started/010.installation.md → packages/noco-docs/docs/020.getting-started/050.self-hosted/010.installation.md

@ -1,9 +1,9 @@
---
title: 'Installation'
description: 'Simple installation - takes about three minutes!'
tags: ['Open Source']
keywords : ['NocoDB installation', 'NocoDB docker installation', 'NocoDB nodejs installation', 'NocoDB quick try', 'NocoDB prerequisites']
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
Simple installation - takes about three minutes!
@ -66,15 +66,11 @@ nocodb/nocodb:latest
</Tabs>
:::tip
To persist data in docker you can mount volume at `/usr/app/data/` since 0.10.6. In older version mount at `/usr/src/app`. Otherwise your data will be lost after recreating the container.
:::
:::tip
If you plan to input some special characters, you may need to change the character set and collation yourself when creating the database. Please check out the examples for [MySQL Docker](https://github.com/nocodb/nocodb/issues/1340#issuecomment-1049481043).
:::
### Docker Compose
@ -111,16 +107,9 @@ docker-compose up -d
</TabItem>
</Tabs>
:::tip
Tip 1: To persist data in docker you can mount volume at `/usr/app/data/` since 0.10.6. In older version mount at `/usr/src/app`.
:::
:::tip
Tip 2: If you plan to input some special characters, you may need to change the character set and collation yourself when creating the database. Please check out the examples for [MySQL Docker Compose](https://github.com/nocodb/nocodb/issues/1313#issuecomment-1046625974).
:::tip
To persist data in docker you can mount volume at `/usr/app/data/` since 0.10.6. In older version mount at `/usr/src/app`.
If you plan to input some special characters, you may need to change the character set and collation yourself when creating the database. Please check out the examples for [MySQL Docker Compose](https://github.com/nocodb/nocodb/issues/1313#issuecomment-1046625974).
:::
@ -148,10 +137,11 @@ nocodb
### Executables
You can download executables directly and run without any extra dependancy. Use the right command based on your platform.
You can download executables directly and run without any extra dependency. Use the right command based on your platform.
##### MacOS (x64)
<Tabs>
<TabItem value="MacOS (x64)" label="MacOS (x64)">
```bash
curl http://get.nocodb.com/macos-x64 -o nocodb -L \
@ -159,7 +149,8 @@ curl http://get.nocodb.com/macos-x64 -o nocodb -L \
&& ./nocodb
```
##### MacOS (arm64)
</TabItem>
<TabItem value="MacOS (arm64)" label="MacOS (arm64)">
```bash
curl http://get.nocodb.com/macos-arm64 -o nocodb -L \
@ -167,14 +158,16 @@ curl http://get.nocodb.com/macos-arm64 -o nocodb -L \
&& ./nocodb
```
##### Linux (x64)
</TabItem>
<TabItem value="Linux (x64)" label="Linux (x64)">
```bash
curl http://get.nocodb.com/linux-x64 -o nocodb -L \
&& chmod +x nocodb \
&& ./nocodb
```
##### Linux (arm64)
</TabItem>
<TabItem value="Linux (arm64)" label="Linux (arm64)">
```bash
curl http://get.nocodb.com/linux-arm64 -o nocodb -L \
@ -182,19 +175,26 @@ curl http://get.nocodb.com/linux-arm64 -o nocodb -L \
&& ./nocodb
```
##### Windows (x64)
</TabItem>
<TabItem value="Windows (x64)" label="Windows (x64)">
```bash
iwr http://get.nocodb.com/win-x64.exe
.\Noco-win-x64.exe
```
##### Windows (arm64)
</TabItem>
<TabItem value="Windows (arm64)" label="Windows (arm64)">
```bash
iwr http://get.nocodb.com/win-arm64.exe
.\Noco-win-arm64.exe
```
</TabItem>
</Tabs>
### Node Application
We provide a simple NodeJS Application for getting started.
@ -236,9 +236,7 @@ npm start
```
:::tip
This json file defines the container specification. You can define secrets such as NC_DB and environment variables here.
:::
Here's the sample Task Definition

69
packages/noco-docs/docs/020.getting-started/050.self-hosted/020.environment-variables.md

@ -0,0 +1,69 @@
---
title: 'Environment variables'
description: 'Environment Variables for NocoDB!'
hide_table_of_contents: true
tags: ['Open Source']
keywords : ['NocoDB environment variables', 'NocoDB env variables', 'NocoDB envs', 'NocoDB env']
---
For production use-cases, it is **recommended** to configure
- `NC_DB`,
- `NC_AUTH_JWT_SECRET`,
- `NC_PUBLIC_URL`,
- `NC_REDIS_URL`
| Variable | Comments | If absent |
|------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
| NC_DB | See our example database URLs [here](https://github.com/nocodb/nocodb#docker). | A local SQLite will be created in root folder if `NC_DB` is not provided |
| NC_DB_JSON | Can be used instead of `NC_DB` and value should be valid knex connection JSON | |
| NC_DB_JSON_FILE | Can be used instead of `NC_DB` and value should be a valid path to knex connection JSON | |
| DATABASE_URL | Can be used instead of `NC_DB` and value should be in JDBC URL format | |
| DATABASE_URL_FILE | Can be used instead of `DATABASE_URL` and value should be a valid path to file containing JDBC URL format. | |
| NC_AUTH_JWT_SECRET | JWT secret used for auth and storing other secrets | A random secret will be generated |
| PORT | For setting app running port | `8080` |
| DB_QUERY_LIMIT_DEFAULT | Pagination limit | 25 |
| DB_QUERY_LIMIT_MAX | Maximum allowed pagination limit | 1000 |
| DB_QUERY_LIMIT_MIN | Minimum allowed pagination limit | 1 |
| NC_TOOL_DIR | App directory to keep metadata and app related files | Defaults to current working directory. In docker maps to `/usr/app/data/` for mounting volume. |
| NC_PUBLIC_URL | Used for sending Email invitations | Best guess from http request params |
| NC_JWT_EXPIRES_IN | JWT token expiry time | `10h` |
| NC_CONNECT_TO_EXTERNAL_DB_DISABLED | Disable Project creation with external database | |
| NC_INVITE_ONLY_SIGNUP | Removed since version 0.99.0 and now it's recommended to use [super admin settings menu](/account-settings/oss-specific-details#enable--disable-signup). Allow users to signup only via invite URL, value should be any non-empty string. | |
| NUXT_PUBLIC_NC_BACKEND_URL | Custom Backend URL | ``http://localhost:8080`` will be used |
| NC_REQUEST_BODY_SIZE | Request body size [limit](https://expressjs.com/en/resources/middleware/body-parser.html#limit) | `1048576` |
| NC_EXPORT_MAX_TIMEOUT | After NC_EXPORT_MAX_TIMEOUT, CSV gets downloaded in batches | Default value 5000(in millisecond) will be used |
| NC_DISABLE_TELE | Disable telemetry | |
| NC_DASHBOARD_URL | Custom dashboard URL path | `/dashboard` |
| NC_GOOGLE_CLIENT_ID | Google client ID to enable Google authentication | |
| NC_GOOGLE_CLIENT_SECRET | Google client secret to enable Google authentication | |
| NC_MIGRATIONS_DISABLED | Disable NocoDB migration | |
| NC_MIN | If set to any non-empty string the default splash screen(initial welcome animation) and matrix screensaver will disable | |
| NC_SENTRY_DSN | For Sentry monitoring | |
| NC_REDIS_URL | Custom Redis URL. Example: `redis://:authpassword@127.0.0.1:6380/4` | Meta data will be stored in memory |
| NC_DISABLE_ERR_REPORT | Disable error reporting | |
| NC_DISABLE_CACHE | To be used only while debugging. On setting this to `true` - meta data be fetched from db instead of redis/cache. | `false` |
| AWS_ACCESS_KEY_ID | For Litestream - S3 access key id | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 |
| AWS_SECRET_ACCESS_KEY | For Litestream - S3 secret access key | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 |
| AWS_BUCKET | For Litestream - S3 bucket | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 |
| AWS_BUCKET_PATH | For Litestream - S3 bucket path (like folder within S3 bucket) | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 |
| NC_SMTP_FROM | For SMTP plugin - Email sender address | |
| NC_SMTP_HOST | For SMTP plugin - SMTP host value | |
| NC_SMTP_PORT | For SMTP plugin - SMTP port value | |
| NC_SMTP_USERNAME | For SMTP plugin (Optional) - SMTP username value for authentication | |
| NC_SMTP_PASSWORD | For SMTP plugin (Optional) - SMTP password value for authentication | |
| NC_SMTP_SECURE | For SMTP plugin (Optional) - To enable secure set value as `true` any other value treated as false | |
| NC_SMTP_IGNORE_TLS | For SMTP plugin (Optional) - To ignore tls set value as `true` any other value treated as false. For more info visit https://nodemailer.com/smtp/ | |
| NC_S3_BUCKET_NAME | For S3 storage plugin - AWS S3 bucket name | |
| NC_S3_REGION | For S3 storage plugin - AWS S3 region | |
| NC_S3_ACCESS_KEY | For S3 storage plugin - AWS access key credential for accessing resource | |
| NC_S3_ACCESS_SECRET | For S3 storage plugin - AWS access secret credential for accessing resource | |
| NC_ADMIN_EMAIL | For updating/creating super admin with provided email and password | |
| NC_ATTACHMENT_FIELD_SIZE | For setting the attachment field size(in Bytes) | Defaults to 20MB |
| NC_ADMIN_PASSWORD | For updating/creating super admin with provided email and password. Your password should have at least 8 letters with one uppercase, one number and one special letter(Allowed special chars $&+,:;=?@#\|'.^*()%!_-" ) | |
| NODE_OPTIONS | For passing Node.js [options](https://nodejs.org/api/cli.html#node_optionsoptions) to instance | |
| NC_MINIMAL_DBS | Create a new SQLite file for each project. All the db files are stored in `nc_minimal_dbs` folder in current working directory. (This option restricts project creation on external sources) | |
| NC_DISABLE_AUDIT | Disable Audit Log | `false` |
| NC_AUTOMATION_LOG_LEVEL | Possible Values: `OFF`, `ERROR`, `ALL`. See [Webhooks](/automation/webhook/create-webhook#call-log) for details. | `OFF` |
| NC_SECURE_ATTACHMENTS | Allow accessing attachments only through presigned urls. To enable set value as `true` any other value treated as false. (⚠ this will make existing links inaccessible ⚠) | `false` |
| NC_ATTACHMENT_EXPIRE_SECONDS | How many seconds before expiring presigned attachment urls. (Attachments will expire in at least set seconds and at most 10mins after set time) | 7200 (2 hours) |
| NC_ALLOW_LOCAL_HOOKS | To enable set value as `true` any other value treated as false. (⚠ this will allow webhooks to call local links which can raise security issues ⚠) | `false` |

149
packages/noco-docs/docs/020.getting-started/050.self-hosted/030.upgrading.md

@ -0,0 +1,149 @@
---
title: 'Upgrading'
description: 'Upgrading NocoDB : Docker, Node and Homebrew!'
tags: ['Open Source']
keywords: ['NocoDB upgrade', 'upgrade NocoDB', 'upgrade nocodb']
---
By default, if `NC_DB` is not specified upon [installation](/getting-started/self-hosted/installation), then SQLite will be used to store metadata. We suggest users to separate the metadata and user data in different databases as pictured in our [architecture](/engineering/architecture).
## Docker
### Find, Stop & Delete NocoDB Docker Container
```bash
# find NocoDB container ID
docker ps
# stop NocoDB container
docker stop <YOUR_CONTAINER_ID>
# delete NocoDB container
docker rm <YOUR_CONTAINER_ID>
```
Note: Deleting your docker container without setting `NC_DB` or mounting to a persistent volume for a default SQLite database will result in losing your data. See examples below.
### Find & Remove NocoDB Docker Image
```bash
# find NocoDB image
docker images
# delete NocoDB image
docker rmi <YOUR_IMAGE_ID>
```
### Pull the latest NocoDB image with same environment variables
```bash
docker run -d -p 8080:8080 \
-e NC_DB="<YOUR_NC_DB_URL>" \
-e NC_AUTH_JWT_SECRET="<YOUR_NC_AUTH_JWT_SECRET_IF_GIVEN>" \
nocodb/nocodb:latest
```
Updating NocoDB docker container is similar to updating [any other docker containers](https://www.whitesourcesoftware.com/free-developer-tools/blog/update-docker-images/).
### Example: Docker Upgrade
```bash
# Previous docker run
#
terminal % docker run -d --name myNocoDB \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="pg://host.docker.internal:5432?u=postgres&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:0.111.0
Unable to find image 'nocodb/nocodb:0.111.0' locally
0.111.0: Pulling from nocodb/nocodb
ad3fa0ea069c: Pull complete
e43b9156e769: Pull complete
c1bee0da1504: Pull complete
adf78ab024d9: Pull complete
cd8000d2c16a: Pull complete
Digest: sha256:93b6e1ba2c0b90a26b205f9c7d44053aa6d8fa037eff9eb4155ca017f6c9bed4
Status: Downloaded newer image for nocodb/nocodb:0.111.0
afdc8edd1005c93e1df8f90d02e46430ea7b5c5610a2bf9ba105238d6c4d927b
# Find, stop and delete NocoDB docker container
#
terminal % docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
afdc8edd1005 nocodb/nocodb:0.111.0 "/usr/bin/dumb-init …" 18 seconds ago Up 18 seconds 0.0.0.0:8080->8080/tcp myNocoDB
0202041b3607 postgres:14.7 "docker-entrypoint.s…" 2 days ago Up 8 hours (healthy) 0.0.0.0:5432->5432/tcp scripts_pg147_1
terminal % docker stop afdc8edd1005
afdc8edd1005
terminal % docker rm afdc8edd1005
afdc8edd1005
# Find and remove NocoDB docker image
#
terminal % docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nocodb/nocodb 0.111.0 34609411e87c 5 weeks ago 132MB
mysql 8.0 6a0560a40914 7 weeks ago 599MB
postgres 14.7 2075a95c7b3b 4 months ago 358MB
terminal % docker rmi 34609411e87c
Untagged: nocodb/nocodb:0.111.0
Untagged: nocodb/nocodb@sha256:93b6e1ba2c0b90a26b205f9c7d44053aa6d8fa037eff9eb4155ca017f6c9bed4
Deleted: sha256:3bfxxxx38e682742cbxxxx535b3503af45e931fb9bd15f46eca7d33cf4c54d72
Deleted: sha256:952152b5da42ae057c6688a04xxxx72e1a2f91825956f5c7e35f91d5b285d4d8
Deleted: sha256:3155197577xxxx673675ed1bce761714a24d7803f70a905740f7d4c248cxxxxx
# Pull & run the latest NocoDB image with same environment variables as before
#
terminal % docker run -d --name myNocoDB \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="pg://host.docker.internal:5432?u=postgres&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest
Unable to find image 'nocodb/nocodb:latest' locally
latest: Pulling from nocodb/nocodb
ad3fa0ea069c: Pull complete
e43b9156e769: Pull complete
c1bee0da1504: Pull complete
adf78ab024d9: Pull complete
28ce4fc94e48: Pull complete
Digest: sha256:5c6df5ff0eb1278e1dbfe684af630a743ca73dfec8c30cab3bae9c1d0d640287
Status: Downloaded newer image for nocodb/nocodb:latest
ae793a04b75f2f3ee78abbaef09891396a884ec83320151a266326195649a058
```
## Node
Updating docker container is similar to updating a npm package.
From your root folder
#### Uninstall NocoDB package
```bash
npm uninstall nocodb
```
#### Install NocoDB package
```bash
npm install --save nocodb
```
## Homebrew
Run following commands to upgrade Homebrew Nocodb version.
```bash
# Update the local homebrew formulas
brew update
# Upgrade nocodb package
brew upgrade nocodb
```

5
packages/noco-docs/docs/020.getting-started/050.self-hosted/_category_.json

@ -0,0 +1,5 @@
{
"label": "In Open Source",
"collapsible": true,
"collapsed": false
}

2
packages/noco-docs/docs/020.getting-started/_category_.json

@ -1,5 +1,5 @@
{
"label": "Getting Started",
"collapsible": true,
"collapsed": false
"collapsed": true
}

101
packages/noco-docs/docs/030.setup-and-usages/010.dashboard.md

@ -1,101 +0,0 @@
---
title: 'Dashboard'
description: 'Accessing the Dashboard!'
---
## Setup Your First Super Admin
Once you have started NocoDB, you can visit the dashboard via `example.com`. You will be redirected to `example.com/#/signup`.
Enter your work email and your password.
![signup](https://github.com/nocodb/nocodb/assets/86527202/f424f935-fef2-4080-8b67-3f6f1bd95c65)
:::info
Your password has at least 8 letters. No other constraints on case, numbers or special characters.
:::
On signup, landing page has a default project & a table created for you to quickly get started
![landing page](https://github.com/nocodb/nocodb/assets/86527202/cd09dbeb-f5e1-42e6-92bb-abd4b3ab48bf)
The data will be stored in `NC_DB`. If it is not specified, a local SQLite will be created and used.
:::info
NC_DB is an environment variable used to store the metadata.
:::
### Connecting to External Database
Click on project title in left sidebar to open the project dashboard. On the project dashboard, open `Data Sources` tab. Click on `+ New Source` button to add existing database.
:::tip
If you are running NocoDB on Docker and your local DB is running on your host machine, your Host Address would be host.docker.internal instead of localhost.
:::
![data source](/img/content/data-source.png)
You need to specify the project name, API type, and other database parameters.
![extDB connection modal](/img/content/extDB-connection-modal.png)
Currently it supports MySQL, Postgres, MSSQL and SQLite.
You can also configure associated SSL & advanced parameters.
<img width="689" alt="image" src="https://user-images.githubusercontent.com/35857179/189047293-05176c44-e162-495a-a7cd-e02377c1f42c.png" />
:::tip
You can click Edit Connection JSON and modify SSL settings in "ssl".
:::
```json
{
"client": "pg",
"connection": {
"host": "<YOUR_HOST>",
"port": "5432",
"user": "<YOUR_DB_USER>",
"password": "<YOUR_DB_PASSWORD>",
"database": "<YOUR_DB_NAME>",
"ssl": {
"require": true,
"rejectUnauthorized": false,
"sslMode": "no-verify"
}
}
}
```
:::tip
You can click Edit Connection JSON and specify the schema you want to use in "searchPath".
:::
```json
{
"client": "pg",
"connection": {
...
},
"searchPath": [ "<YOUR_TARGET_SCHEMA>" ]
}
```
Click `Test Database Connection` to see if the connection can be established or not. NocoDB creates a new **empty database** with specified parameters if the database doesn't exist.
<img width="505" alt="image" src="https://user-images.githubusercontent.com/35857179/194793513-feabf14f-1f62-4896-b06d-88548251511a.png" />

204
packages/noco-docs/docs/030.setup-and-usages/020.table-operations.md

@ -1,204 +0,0 @@
---
title: "Table Operations"
description: "Table Operations: Row, Column, Quick Import, Export & Import"
---
Once you have created a new NocoDB project you can open it, In the browser, the URL would be like `example.com/#/default/<project_id>`.
## Table
### Table Create
On project dashboard, click on `Add new table` button
![table-create-button](https://github.com/nocodb/nocodb/assets/86527202/0b1d0b89-b3c6-4c3c-8208-4b6afce67d23)
Provide a name for the table & click `Create Table` button.
![table-create-modal](https://github.com/nocodb/nocodb/assets/86527202/fceb6c48-e0d6-428c-bb16-d2da4e38a81f)
After the successful submission, the table will be created and open on a new grid.
![new-table-landing-page](https://github.com/nocodb/nocodb/assets/86527202/96ffae6f-c18f-4b80-81eb-847cfb8116e2)
New table can also be created by using `+` button on project tile in left sidebar
![new-table-on-hover](https://github.com/nocodb/nocodb/assets/86527202/5a4ce91b-f4bf-451c-8835-8e7144deec26)
### Table Rename
Right click on Table name on left sidebar, (OR)
Click on `...` to open `Table context menu`, select `Rename`.
Feed in the changes to the table name & press `Enter`
![table-context-menu](https://github.com/nocodb/nocodb/assets/86527202/67cb7563-5a10-4d91-bc83-b31ff185d18d)
### Table Duplicate
Right click on Table name on left sidebar, (OR)
Click on `...` to open `Table context menu`, select `Duplicate`
![table-context-menu](https://github.com/nocodb/nocodb/assets/86527202/67cb7563-5a10-4d91-bc83-b31ff185d18d)
Additionally, you can configure to duplicate
- `Include Data` : toggle this to include/exclude table records
- `Include Views` : toggle this to include/exclude table views
![table-duplicate-modal](https://github.com/nocodb/nocodb/assets/86527202/e668c382-530f-423c-9de7-f68896a6ba67)
### Table Delete
Right click on Table name on left sidebar, (OR)
Click on `...` to open `Table context menu`, select `Delete`
![table-context-menu](https://github.com/nocodb/nocodb/assets/86527202/67cb7563-5a10-4d91-bc83-b31ff185d18d)
Click on `Delete Table` to confirm
![table-delete-confirmation-modal](https://github.com/nocodb/nocodb/assets/86527202/a0d11ca6-0aed-4eaf-a855-c49ff987d546)
## Column
### Column Add
Click on `+` button to the right of Columns header, type `Column name`
![column-add](https://github.com/nocodb/nocodb/assets/86527202/d3350116-5047-4385-8eab-1fef4b3836ac)
Select a `type` for the column from the dropdown. Depending on the column type, you might find additional options to configure.
Click on `Save column` to finish creating column.
![column-add-type](https://github.com/nocodb/nocodb/assets/86527202/239bfbde-fd20-4f8e-966c-2c60507c195e)
#### Column create before OR after a specific column
You can also use context menu of an existing column to either insert before or after a specific column.
![column-context-menu - insert after before](https://github.com/nocodb/nocodb/assets/86527202/d15b62b4-5302-4c75-bd5b-e60ae8b81a65)
### Column Edit
Double click on Column name in column header to open `Column edit` modal
You can rename column & optionally change column-type.
![column-rename](https://github.com/nocodb/nocodb/assets/86527202/1a0f798e-6726-4810-9645-e531a88e495a)
Note:
- Changing column type might not be allowed in some scenarios & in some other, might lead to either loss or truncated data.
- Column name is also possible using Column context menu as described below
### Column Duplicate
Open `Column context menu` (click `v` on column header), select `Duplicate`
![column-duplicate](https://github.com/nocodb/nocodb/assets/86527202/8d1f36e6-5f6b-4f89-81d0-c0d3148bd056)
Note: Column duplicate only creates another column of same type & inserts it to the immediate right. Currently data in the column is not duplicated.
### Column Delete
Open `Column context menu` (click `v` on column header), select `Delete`
![column-delete](https://github.com/nocodb/nocodb/assets/86527202/2444124d-f12a-4bab-91ce-9b8c0625a263)
Click on `Delete Column` to confirm
![Column delete confirmation modal](https://github.com/nocodb/nocodb/assets/86527202/5675b2ef-9d2c-40fe-900b-d5dd58d1655d)
## Row
For adding new values to the table we need new rows, new rows can be added in two methods.
### Row Add (Using Form)
Click on `New Record` at the bottom of the grid (footbar), select `New Record - Form`
![new record-form](https://github.com/nocodb/nocodb/assets/86527202/c6e9acff-4d54-440a-9888-e24529747cf9)
Populate columns in the Expnaded form popup; click `Save`
![expanded record form](https://github.com/nocodb/nocodb/assets/86527202/ec264f05-8b0b-4029-adc0-a88932d69c9f)
### Row Add (Using Table Row at bottom of page)
![in grid insert](https://github.com/nocodb/nocodb/assets/86527202/cf0b6ccb-f598-4874-bb87-8fe291bab14e)
Click on any of the following options to insert a new record on the grid directly.
- `+`
- `New Record` : `New Record- Grid`
- Right click on any cell, click `Insert new row` from the cell context menu
Note that, any record inserted in the grid will always be appended to the end of the table by default.
### Row Add (Pressing Enter Key from Previous Row)
When you finish editing a cell and press Enter, the cell in the next row with the same column will be highlighted.
![image](https://user-images.githubusercontent.com/35857179/203271676-bab64ca4-e0e4-4deb-9a62-609a97158911.png)
### Row Edit
You can start editing by any of the following methods
- Double-click on cell to edit
- Click on cell and start typing (this way it will clear the previous content)
- Click on cell and press enter to start editing
- And it will automatically save on blur event or if inactive.
### Row Delete
Right-click on the row and then from the context menu select `Delete Row` option.
![delete row -1](https://github.com/nocodb/nocodb/assets/86527202/9eef834b-21cf-4828-90ec-3c2e86cd19db)
Bulk delete is also possible by selecting multiple rows by using the checkbox in first column and then `Delete Selected Rows` options from the right click context menu.
![delete row 2](https://github.com/nocodb/nocodb/assets/86527202/f83c702a-fa75-42a5-86eb-22ed85c0dec1)
## Quick Import
You can use Quick Import when you have data from external sources such as Airtable, CSV file or Microsoft Excel to an existing project by either
- Hover on `Project title` in tree-view, click `...` > `Quick Import From` > `Airtable` or `CSV file` or `Microsoft Excel` or `JSON file`
- Drag and drop CSV, JSON or Excel file to import
![image](/img/content/quick-import.png)
### Import Airtable into an Existing Project
- See [here](/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free)
### Import CSV data into an Existing Project
- Hover on `Project title` in tree-view, click `...` > `Quick Import From` > `CSV file`
- Drag & drop or select files (at most 5 files) to upload or specify CSV file URL, and Click Import
- **Auto-Select Field Types**: If it is checked, column types will be detected. Otherwise, it will default to `SingleLineText`.
- **Use First Row as Headers**: If it is checked, the first row will be treated as header row.
- **Import Data**: If it is checked, all data will be imported. Otherwise, only table will be created.
![image](/img/content/import-csv.png)
- You can revise the table name by double-clicking it, column name and column type. By default, the first column will be chosen as `Display Value` and cannot be deleted.
![image](/img/content/import-csv-2.png)
- Click `Import` to start importing process. The table will be created and the data will be imported.
![image](/img/content/import-csv-3.png)
### Import Excel data into an Existing Project
- Hover on `Project title` in tree-view, click `...` > `Quick Import From` > `Excel file`
- Drag & drop or select files (at most 5 files) to upload or specify CSV file URL, and Click Import
- **Auto-Select Field Types**: If it is checked, column types will be detected. Otherwise, it will default to `SingleLineText`.
- **Use First Row as Headers**: If it is checked, the first row will be treated as header row.
- **Import Data**: If it is checked, all data will be imported. Otherwise, only table will be created.
![image](/img/content/import-xls.png)
- You can revise the table name by double-clicking it, column name and column type. By default, the first column will be chosen as `Display Value` and cannot be deleted.
![image](/img/content/import-xls-2.png)
- Click `Import` to start importing process. The table will be created and the data will be imported.
![image](/img/content/import-xls-3.png)
## Export Data
You can export your data from a table as a CSV file by clicking the `...` menu in toolbar, and hover on `Download`. Currently only CSV and XLSX formats are supported for export.
![image](/img/content/export.png)
## Import Data
You can export your data from a table as a CSV file by clicking the `...` menu in toolbar, and hover on `Upload`. Currently only CSV and XLSX formats are supported for import.
![image](/img/content/import.png)

133
packages/noco-docs/docs/030.setup-and-usages/030.column-operations.md

@ -1,133 +0,0 @@
---
title: "Column Operations"
description: "Column Operations: Fields, Sort & Filter"
---
## Fields
Click `Fields` to control the visibility of a field.
By default, all system fields will be hid. However, you can tick `Show system fields` to make them visible.
<img width="1268" alt="image" src="https://user-images.githubusercontent.com/86527202/262226113-6ad574c2-93d0-49ac-be28-01b336df8de4.png" />
### Re-order Columns
Column positions can be re-ordered. Open `Fields` menu, and re-order fields as needed by dragging and dropping the `drag icon`.
<img width="1268" alt="image" src="https://user-images.githubusercontent.com/86527202/262226232-0b30d4b2-3390-470f-bf01-72b5b922726b.png" />
### Hide / Unhide Columns
To hide / un-hide columns, open Fields menu, click on associated toggle button to hide / un-hide.
:::tip
You can create different grid views with different fields shown in each view.
:::
<img alt="image" src="https://user-images.githubusercontent.com/86527202/262226271-b9a323e9-39b8-418d-9b7c-3da0f2d17d89.png" />
<!-- #### Demo -->
<!-- <img src="https://github.com/dstala/nocodb-files/blob/2c4ca2ff31460ee5636262e88ba303e2d436ba54/ColumnHide.gif?raw=true" width="100%"/> -- />
<!-- img src="https://media2.giphy.com/media/8NXvWfHDoul72dwLhk/giphy.gif?cid=790b76116fa008b45c79bb91bfe611e324fa38cde21a255a&rid=giphy.gif&ct=g" width="60%"/ -->
## Sort
Sorting allows you to order contents alphabetically (A → Z) / (Z → A) (OR) in ascending / descending order. NocoDB allows nested sorting. You can choose column fields & order in which to apply nested sorting. Lookup, Formula, Nested Data are also supported in Sorting.
### Add / Edit sort
- Click on `Sort` button in the toolbar menu
- Select `Field` to sort by
![sort](https://github.com/nocodb/nocodb/assets/86527202/5665b5db-7d66-4d17-8307-4a8bf32360c8)
- Configure sort `direction`
![sort direction](https://github.com/nocodb/nocodb/assets/86527202/56a6d29b-de43-4aaf-b77e-41f32bb58f93)
- Multiple fields can be configured for subsequent level sorting
![sort nested](https://github.com/nocodb/nocodb/assets/86527202/4e9393d7-953c-4a3f-bb4e-3b0161042ae4)
Note: Field configured on the top will be used for first level sorting, followed by subsequent fields in top-down order
### Delete sort
- Click on `Sort` button in the toolbar
- Click on the bin icon to the right of the sort you wish to delete
![sort delete](https://github.com/nocodb/nocodb/assets/86527202/d469110b-12b2-4b8a-83ee-fe8819da2dc1)
## Filter
Filters allow you to restrict / organize your data on the view as per your needs. NocoDB allows nested filters. You can choose multiple columns and conditions to apply filter. Between filters, you can opt for either `and` or `or` mode operation. Lookup, Formula, Nested Data are also supported in Filtering.
### Add / Edit filter
Click on `Filter` button in the toolbar, select either `Add filter` or `Add filter group`
![filter add](https://github.com/nocodb/nocodb/assets/86527202/f312d8e5-98f1-4e34-ad82-460bb5eacef3)
Configure filter : `Column` `Operation` and `Value` (if applicable)
![filter level-1](https://github.com/nocodb/nocodb/assets/86527202/62ac5ea5-64c7-4ab4-93bc-c2897e1a9122)
Multiple filter conditions can be combined by using either `and` or `or` mode of operation
![filter nested](https://github.com/nocodb/nocodb/assets/86527202/1e9af5bf-c19f-49ed-8fc4-a62093f6ee01)
### Delete filter
To delete a filter, click on the bin icon to the right of the associated filter
![filter delete](https://github.com/nocodb/nocodb/assets/86527202/c8f5abac-a550-4152-ab51-5f0765cd188b)
### Grouped filters
You can also group several filters together using Filter Group.
![filter grouped](https://github.com/nocodb/nocodb/assets/86527202/582c29de-28cd-4414-b7db-4b1b1eea131e)
## Enable NULL and EMPTY Filters
NULL filters (`is null` & `is not null`) and EMPTY filters (`is empty` & `is not empty`) are hidden by default. If you wish to filter out either one only, you may enable `Show NULL and EMPTY Filter` in Project Settings.
![project settings](https://github.com/nocodb/nocodb/assets/86527202/49d40f16-f8bd-4925-a4d0-65efb2d8f73e)
![filters show null empty](https://github.com/nocodb/nocodb/assets/86527202/a383f5e8-f2b9-461b-b725-9b172ac9975b)
Otherwise, we can use Blank filters to filter out cells with NULL values and EMPTY values.
### Supported Filters
Currently, we support different types of filters for corresponding columns. Please refer the below matrix for details.
<iframe width="100%" height="700vh" src="https://docs.google.com/spreadsheets/d/e/2PACX-1vTpCNKtA-szaXUKJEO5uuSIRnzUOK793MKnyBz9m2rQcwn7HqK19jPHeER-IIRWH9X56J78wfxXZuuv/pubhtml?gid=427284630&amp;single=true&amp;widget=true&amp;headers=false"></iframe>
## Group by
Group-by allows one to visually categorise records on grid into specific `Groups` & `Sub groups`. NocoDB allows three levels of record seggregation.
![group by](https://github.com/nocodb/nocodb/assets/86527202/d4bfcdee-85ed-4c98-92ac-12055f9715e4)
### Add / Edit group-by
- Click on `Group By` in the toolbar
- Choose field for the records to be grouped by
- Optionally- you can also sort groups, in either ascending or descending way.
- Add sub-groups of upto 3 levels
### Delete group-by
- Click on `Group By` in the toolbar
- Click on the bin icon to the right of the group you wish to delete
Note: To disable `Group by` & return back to standard spreadsheet grid view - you need to remove all the groups configured.
## Row Height
NocoDB allows you to change height of records displayed on spreadsheet grid to 4 different levels `Short` (default), `Medium`, `Tall`, and `Extra`. This allows you to bring more content per cell to be displayed, useful when working with lengthy text columns and multi select fields.
![Row height](https://github.com/nocodb/nocodb/assets/86527202/6f49e7b4-a3de-4325-a11e-3ffc670ddd2e)

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

Loading…
Cancel
Save