Browse Source

Merge branch 'develop' into nc-agg/tests

nc-agg/tests
Raju Udava 5 months ago
parent
commit
422f9f059f
  1. 4
      .github/workflows/ci-cd.yml
  2. 2
      .github/workflows/playwright-test-workflow.yml
  3. 2
      .github/workflows/pre-build-for-playwright.yml
  4. 2
      .github/workflows/release-docker.yml
  5. 2
      .github/workflows/release-npm.yml
  6. 2
      .github/workflows/sync-to-develop.yml
  7. 2
      .github/workflows/unit-test.yml
  8. 2
      .github/workflows/update-sdk-path.yml
  9. 2
      README.md
  10. 6
      packages/nc-gui/assets/nc-icons/camera.svg
  11. 10
      packages/nc-gui/assets/nc-icons/file.svg
  12. 135
      packages/nc-gui/components/cell/attachment/AttachFile.vue
  13. 123
      packages/nc-gui/components/cell/attachment/Modal.vue
  14. 147
      packages/nc-gui/components/cell/attachment/UploadProviders/Camera.vue
  15. 190
      packages/nc-gui/components/cell/attachment/UploadProviders/Local.vue
  16. 173
      packages/nc-gui/components/cell/attachment/UploadProviders/Url.vue
  17. 35
      packages/nc-gui/components/cell/attachment/index.vue
  18. 56
      packages/nc-gui/components/cell/attachment/utils.ts
  19. 1
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  20. 3
      packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue
  21. 3
      packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue
  22. 2
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  23. 63
      packages/nc-gui/components/dashboard/View.vue
  24. 13
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  25. 3
      packages/nc-gui/components/dashboard/settings/Erd.vue
  26. 15
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  27. 72
      packages/nc-gui/components/dlg/QuickImport.vue
  28. 40
      packages/nc-gui/components/dlg/ViewCreate.vue
  29. 20
      packages/nc-gui/components/erd/View.vue
  30. 5
      packages/nc-gui/components/general/MaintenanceAlert.vue
  31. 2
      packages/nc-gui/components/nc/PaginationV2.vue
  32. 3
      packages/nc-gui/components/project/ImportModal.vue
  33. 2
      packages/nc-gui/components/shared-view/Grid.vue
  34. 2
      packages/nc-gui/components/smartsheet/Kanban.vue
  35. 8
      packages/nc-gui/components/smartsheet/Pagination.vue
  36. 35
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  37. 16
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  38. 11
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  39. 10
      packages/nc-gui/components/smartsheet/calendar/index.vue
  40. 25
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  41. 4
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  42. 13
      packages/nc-gui/components/smartsheet/grid/Aggregation.vue
  43. 104
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  44. 183
      packages/nc-gui/components/smartsheet/grid/GroupByTable.vue
  45. 39
      packages/nc-gui/components/smartsheet/grid/PaginationV2.vue
  46. 7
      packages/nc-gui/components/smartsheet/grid/Table.vue
  47. 21
      packages/nc-gui/components/smartsheet/grid/index.vue
  48. 39
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Range.vue
  49. 16
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  50. 35
      packages/nc-gui/components/smartsheet/toolbar/FieldListWithSearch.vue
  51. 30
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  52. 7
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  53. 9
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  54. 289
      packages/nc-gui/components/template/Editor.vue
  55. 11
      packages/nc-gui/components/template/utils.ts
  56. 4
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  57. 2
      packages/nc-gui/components/virtual-cell/HasMany.vue
  58. 2
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  59. 4
      packages/nc-gui/components/virtual-cell/OneToOne.vue
  60. 49
      packages/nc-gui/components/virtual-cell/components/ItemChip.vue
  61. 16
      packages/nc-gui/components/workspace/AuditLogs.vue
  62. 65
      packages/nc-gui/composables/useCalendarViewStore.ts
  63. 4
      packages/nc-gui/composables/useData.ts
  64. 6
      packages/nc-gui/composables/useExpandedFormStore.ts
  65. 5
      packages/nc-gui/composables/useGlobal/actions.ts
  66. 2
      packages/nc-gui/composables/useGlobal/state.ts
  67. 2
      packages/nc-gui/composables/useGlobal/types.ts
  68. 2
      packages/nc-gui/composables/useMultiSelect/index.ts
  69. 15
      packages/nc-gui/composables/useServerConfig.ts
  70. 30
      packages/nc-gui/composables/useSharedView.ts
  71. 26
      packages/nc-gui/composables/useViewAggregate.ts
  72. 10
      packages/nc-gui/composables/useViewColumns.ts
  73. 91
      packages/nc-gui/composables/useViewGroupBy.ts
  74. 14
      packages/nc-gui/context/index.ts
  75. 10
      packages/nc-gui/helpers/parsers/ExcelTemplateAdapter.ts
  76. 31
      packages/nc-gui/lang/ar.json
  77. 31
      packages/nc-gui/lang/bn_IN.json
  78. 23
      packages/nc-gui/lang/cs.json
  79. 25
      packages/nc-gui/lang/da.json
  80. 41
      packages/nc-gui/lang/de.json
  81. 36
      packages/nc-gui/lang/en.json
  82. 21
      packages/nc-gui/lang/es.json
  83. 31
      packages/nc-gui/lang/eu.json
  84. 25
      packages/nc-gui/lang/fa.json
  85. 25
      packages/nc-gui/lang/fi.json
  86. 123
      packages/nc-gui/lang/fr.json
  87. 23
      packages/nc-gui/lang/he.json
  88. 31
      packages/nc-gui/lang/hi.json
  89. 25
      packages/nc-gui/lang/hr.json
  90. 83
      packages/nc-gui/lang/hu.json
  91. 25
      packages/nc-gui/lang/id.json
  92. 21
      packages/nc-gui/lang/it.json
  93. 25
      packages/nc-gui/lang/ja.json
  94. 25
      packages/nc-gui/lang/ko.json
  95. 23
      packages/nc-gui/lang/lv.json
  96. 25
      packages/nc-gui/lang/nl.json
  97. 31
      packages/nc-gui/lang/no.json
  98. 47
      packages/nc-gui/lang/pl.json
  99. 23
      packages/nc-gui/lang/pt.json
  100. 25
      packages/nc-gui/lang/pt_BR.json
  101. Some files were not shown because too many files have changed in this diff Show More

4
.github/workflows/ci-cd.yml

@ -61,7 +61,7 @@ jobs:
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: 8
- name: Setup Node
@ -96,7 +96,7 @@ jobs:
if: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'trigger-CI') || !github.event.pull_request.draft }}
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: 8
- name: Setup Node

2
.github/workflows/playwright-test-workflow.yml

@ -25,7 +25,7 @@ jobs:
with:
node-version: 18.19.1
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: 8
- name: Get pnpm store directory

2
.github/workflows/pre-build-for-playwright.yml

@ -15,7 +15,7 @@ jobs:
with:
node-version: 18.19.1
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: 8
- name: remove use-node-version from .npmrc

2
.github/workflows/release-docker.yml

@ -46,7 +46,7 @@ jobs:
working-directory: ./packages/nocodb
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: 8
- name: Get Docker Repository

2
.github/workflows/release-npm.yml

@ -38,7 +38,7 @@ jobs:
working-directory: ./packages/nocodb
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: 8
- name: Checkout

2
.github/workflows/sync-to-develop.yml

@ -14,7 +14,7 @@ jobs:
with:
node-version: 18.19.1
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: 8
- name: Checkout

2
.github/workflows/unit-test.yml

@ -25,7 +25,7 @@ jobs:
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: 8
- uses: actions/checkout@v3

2
.github/workflows/update-sdk-path.yml

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: 8
- name: Setup Node

2
README.md

@ -75,7 +75,7 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart spreadshe
docker run -d --name nocodb-postgres \
-v "$(pwd)"/nocodb:/usr/app/data/ \
-p 8080:8080 \
-e NC_DB="pg://host.docker.internal:5432?u=root&p=password&d=d1" \
-e NC_DB="pg://host.docker.internal:5432?u=root&p=password&d=d1" \
-e NC_AUTH_JWT_SECRET="569a1821-0a93-45e8-87ab-eb857f20a010" \
nocodb/nocodb:latest

6
packages/nc-gui/assets/nc-icons/camera.svg

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="camera">
<path id="Vector" d="M15.3333 12.6667C15.3333 13.0203 15.1928 13.3594 14.9428 13.6095C14.6928 13.8595 14.3536 14 14 14H1.99999C1.64637 14 1.30723 13.8595 1.05718 13.6095C0.807132 13.3594 0.666656 13.0203 0.666656 12.6667V5.33333C0.666656 4.97971 0.807132 4.64057 1.05718 4.39052C1.30723 4.14048 1.64637 4 1.99999 4H4.66666L5.99999 2H9.99999L11.3333 4H14C14.3536 4 14.6928 4.14048 14.9428 4.39052C15.1928 4.64057 15.3333 4.97971 15.3333 5.33333V12.6667Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M8.00001 11.3333C9.47277 11.3333 10.6667 10.1394 10.6667 8.66667C10.6667 7.19391 9.47277 6 8.00001 6C6.52725 6 5.33334 7.19391 5.33334 8.66667C5.33334 10.1394 6.52725 11.3333 8.00001 11.3333Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 982 B

10
packages/nc-gui/assets/nc-icons/file.svg

@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.33341 1.33333H4.00008C3.64646 1.33333 3.30732 1.4738 3.05727 1.72385C2.80722 1.9739 2.66675 2.31304 2.66675 2.66666V13.3333C2.66675 13.6869 2.80722 14.0261 3.05727 14.2761C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2761C13.1929 14.0261 13.3334 13.6869 13.3334 13.3333V5.33333L9.33341 1.33333Z" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6666 11.3333H5.33325" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6666 8.66667H5.33325" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.66659 6H5.99992H5.33325" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.33325 1.33333V5.33333H13.3333" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.33341 1.33333H4.00008C3.64646 1.33333 3.30732 1.4738 3.05727 1.72385C2.80722 1.9739 2.66675 2.31304 2.66675 2.66666V13.3333C2.66675 13.6869 2.80722 14.0261 3.05727 14.2761C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2761C13.1929 14.0261 13.3334 13.6869 13.3334 13.3333V5.33333L9.33341 1.33333Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6666 11.3333H5.33325" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6666 8.66667H5.33325" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.66659 6H5.99992H5.33325" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.33325 1.33333V5.33333H13.3333" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

135
packages/nc-gui/components/cell/attachment/AttachFile.vue

@ -0,0 +1,135 @@
<script setup lang="ts">
import { useAttachmentCell } from './utils'
const props = defineProps<{
value: boolean
}>()
const dialogShow = useVModel(props, 'value')
const { onDrop: saveAttachment, isPublic, stopCamera } = useAttachmentCell()!
const activeMenu = ref('local')
const selectMenu = (option: string) => {
activeMenu.value = option
}
const closeModal = (value: boolean) => {
dialogShow.value = value
}
const saveAttachments = async (files: File[]) => {
await saveAttachment(files, {} as any)
dialogShow.value = false
}
watch(activeMenu, (newVal, oldValue) => {
// Stop camera when switching to another menu
if (oldValue === 'webcam' && newVal !== 'webcam') {
// When the menu is switched when the startCamera function is called, the videoStream might not have initialized yet
// So, we need to wait for a while before stopping the camera
setTimeout(() => {
stopCamera()
}, 1000)
}
})
</script>
<template>
<NcModal
v-model:visible="dialogShow"
:show-separator="false"
size="medium"
width="50rem"
wrap-class-name="nc-modal-attachment-create"
class="!rounded-md"
@keydown.esc="dialogShow = false"
>
<div class="flex h-full flex-row">
<div style="border-top-left-radius: 1rem; border-bottom-left-radius: 1rem" class="px-2 !-full flex-grow bg-gray-100">
<NcMenu class="!h-full !bg-gray-100">
<NcMenuItem
key="local"
:class="{
'active-menu': activeMenu === 'local',
}"
@click="selectMenu('local')"
>
<div class="flex gap-2 items-center">
<GeneralIcon icon="file" />
{{ $t('title.localFiles') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="!isPublic"
key="url"
:class="{
'active-menu': activeMenu === 'url',
}"
@click="selectMenu('url')"
>
<div class="flex gap-2 items-center">
<GeneralIcon icon="link2" />
{{ $t('title.uploadViaUrl') }}
</div>
</NcMenuItem>
<NcMenuItem
key="webcam"
:class="{
'active-menu': activeMenu === 'webcam',
}"
@click="selectMenu('webcam')"
>
<div class="flex gap-2 items-center">
<GeneralIcon icon="camera" />
{{ $t('title.webcam') }}
</div>
</NcMenuItem>
</NcMenu>
</div>
<div style="height: 425px" class="w-full p-2">
<LazyCellAttachmentUploadProvidersLocal
v-show="activeMenu === 'local'"
@update:visible="closeModal"
@upload="(e) => saveAttachments(e)"
/>
<LazyCellAttachmentUploadProvidersCamera
v-if="activeMenu === 'webcam'"
@update:visible="closeModal"
@upload="(e) => saveAttachments(e)"
/>
<LazyCellAttachmentUploadProvidersUrl
v-if="activeMenu === 'url'"
@update:visible="closeModal"
@upload="(e) => saveAttachments(e)"
/>
</div>
</div>
</NcModal>
</template>
<style lang="scss">
.nc-modal-attachment-create {
.active-menu {
@apply !bg-gray-200 font-sembold text-brand-500 rounded-md;
}
}
.nc-modal-attachment-create {
.nc-modal {
@apply !p-0;
}
}
</style>
<style scoped lang="scss">
:deep(.ant-menu-inline),
:deep(.ant-menu-vertical),
:deep(.ant-menu-vertical-left) {
border-right: none !important;
}
</style>

123
packages/nc-gui/components/cell/attachment/Modal.vue

@ -92,22 +92,27 @@ const handleFileDelete = (i: number) => {
>
<template #title>
<div class="flex gap-4">
<div
<NcButton
v-if="isSharedForm || (!readOnly && isUIAllowed('dataEdit') && !isPublic)"
class="nc-attach-file group"
data-testid="attachment-expand-file-picker-button"
@click="open"
>
<MaterialSymbolsAttachFile class="transform group-hover:(text-accent scale-120)" />
{{ $t('activity.attachFile') }}
</div>
<div class="flex gap-2 items-center">
<component :is="iconMap.cellAttachment" class="w-4 h-4" />
{{ $t('activity.attachFile') }}
</div>
</NcButton>
<div class="flex items-center gap-2">
{{ $t('labels.viewingAttachmentsOf') }}
<div class="font-semibold underline">{{ column?.title }}</div>
</div>
<div v-if="selectedVisibleItems.includes(true)" class="flex flex-1 items-center gap-3 justify-end mr-[30px]">
<div
v-if="selectedVisibleItems.includes(true) && selectedVisibleItems.length > 1"
class="flex flex-1 items-center gap-3 justify-end mr-[30px]"
>
<NcButton type="primary" class="nc-attachment-download-all" @click="bulkDownloadFiles">
{{ $t('activity.bulkDownload') }}
</NcButton>
@ -127,40 +132,14 @@ const handleFileDelete = (i: number) => {
</template>
<div ref="sortableRef" :class="{ dragging }" class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-6 relative p-6">
<div v-for="(item, i) of visibleItems" :key="`${item.title}-${i}`" class="flex flex-col gap-1">
<div v-for="(item, i) of visibleItems" :key="`${item.title}-${i}`" class="flex flex-col group gap-1">
<a-card class="nc-attachment-item group">
<a-checkbox
<NcCheckbox
v-model:checked="selectedVisibleItems[i]"
class="nc-attachment-checkbox group-hover:(opacity-100)"
class="nc-attachment-checkbox absolute top-2 left-2 group-hover:(opacity-100)"
:class="{ '!opacity-100': selectedVisibleItems[i] }"
/>
<a-tooltip v-if="!readOnly">
<template #title> {{ $t('title.removeFile') }} </template>
<component
:is="iconMap.closeCircle"
v-if="isSharedForm || (isUIAllowed('dataEdit') && !isPublic)"
class="nc-attachment-remove"
@click.stop="onRemoveFileClick(item.title, i)"
/>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title> {{ $t('title.downloadFile') }} </template>
<div class="nc-attachment-download group-hover:(opacity-100)">
<component :is="iconMap.download" @click.stop="downloadFile(item)" />
</div>
</a-tooltip>
<a-tooltip v-if="isSharedForm || (!readOnly && isUIAllowed('dataEdit') && !isPublic)" placement="bottom">
<template #title> {{ $t('title.renameFile') }} </template>
<div class="nc-attachment-rename group-hover:(opacity-100) mr-[35px]">
<component :is="iconMap.rename" @click.stop="renameFile(item, i)" />
</div>
</a-tooltip>
<div
:class="[dragging ? 'cursor-move' : 'cursor-pointer']"
class="nc-attachment h-full w-full flex items-center justify-center overflow-hidden"
@ -183,9 +162,32 @@ const handleFileDelete = (i: number) => {
<IcOutlineInsertDriveFile v-else height="150" width="150" @click.stop="openAttachment(item)" />
</div>
</a-card>
<div class="truncate" :title="item.title">
{{ item.title }}
<div class="relative flex" :title="item.title">
<div class="flex-auto truncate line-height-4">
{{ item.title }}
</div>
<div class="flex-none hide-ui transition-all transition-ease-in-out !h-6 flex items-center bg-white">
<NcTooltip placement="bottom">
<template #title> {{ $t('title.downloadFile') }} </template>
<NcButton class="!text-gray-500" size="xsmall" type="text" @click="downloadFile(item)">
<component :is="iconMap.download" />
</NcButton>
</NcTooltip>
<NcTooltip v-if="!isSharedForm || (!readOnly && isUIAllowed('dataEdit') && !isPublic)" placement="bottom">
<template #title> {{ $t('title.renameFile') }} </template>
<NcButton size="xsmall" class="nc-attachment-rename !text-gray-500" type="text" @click="renameFile(item, i)">
<component :is="iconMap.rename" />
</NcButton>
</NcTooltip>
<NcTooltip v-if="!readOnly" placement="bottom">
<template #title> {{ $t('title.removeFile') }} </template>
<NcButton class="!text-red-500" size="xsmall" type="text" @click="onRemoveFileClick(item.title, i)">
<component :is="iconMap.delete" v-if="isSharedForm || (isUIAllowed('dataEdit') && !isPublic)" />
</NcButton>
</NcTooltip>
</div>
</div>
</div>
@ -217,13 +219,15 @@ const handleFileDelete = (i: number) => {
</template>
<style lang="scss">
.nc-attachment-modal {
.nc-attach-file {
@apply select-none cursor-pointer color-transition flex items-center gap-1 border-1 p-2 rounded
@apply hover:(bg-primary bg-opacity-10 text-primary ring);
@apply active:(ring-accent ring-opacity-100 bg-primary bg-opacity-20);
}
.hide-ui {
@apply h-0 w-0 overflow-hidden whitespace-nowrap;
// When the parent with class 'group' is hovered
.group:hover & {
@apply h-auto w-auto overflow-visible whitespace-normal;
}
}
.nc-attachment-modal {
.nc-attachment-item {
@apply !h-2/3 !min-h-[200px] flex items-center justify-center relative;
@ -238,35 +242,15 @@ const handleFileDelete = (i: number) => {
@supports (-moz-appearance: none) {
&:hover::after {
@apply ring shadow transform scale-103;
@apply ring shadow;
}
&:active::after {
@apply ring ring-accent ring-opacity-100 shadow transform scale-103;
@apply ring ring-accent ring-opacity-100 shadow;
}
}
}
.nc-attachment-download,
.nc-attachment-rename {
@apply bg-white absolute bottom-2 right-2;
@apply transition-opacity duration-150 ease-in opacity-0 hover:ring;
@apply cursor-pointer rounded shadow flex items-center p-1 border-1;
@apply active:(ring border-0 ring-accent);
}
.nc-attachment-checkbox {
@apply absolute top-2 left-2;
@apply transition-opacity duration-150 ease-in opacity-0;
}
.nc-attachment-remove {
@apply absolute top-2 right-2 bg-white;
@apply hover:(ring ring-red-500);
@apply cursor-pointer rounded-full border-2;
@apply active:(ring border-0 ring-red-500);
}
.ant-card-body {
@apply !p-2 w-full h-full;
}
@ -275,19 +259,10 @@ const handleFileDelete = (i: number) => {
@apply !p-0;
}
.ghost,
.ghost > * {
@apply !pointer-events-none;
}
.dragging {
.nc-attachment-item {
@apply !pointer-events-none;
}
.ant-tooltip {
@apply !hidden;
}
}
}
</style>

147
packages/nc-gui/components/cell/attachment/UploadProviders/Camera.vue

@ -0,0 +1,147 @@
<script setup lang="ts">
import { useAttachmentCell } from '../utils'
const emits = defineEmits<{
'update:visible': [value: boolean]
'upload': [fileList: File[]]
}>()
const { isLoading, startCamera: _startCamera, stopCamera: _stopCamera, videoStream, permissionGranted } = useAttachmentCell()!
const capturedImage = ref<null | File>(null)
const videoRef = ref<HTMLVideoElement | undefined>()
const canvasRef = ref<HTMLCanvasElement | undefined>()
const startCamera = async () => {
try {
await _startCamera()
if (!videoRef.value || !videoStream.value) return
videoRef.value.srcObject = videoStream.value
} catch (error) {}
}
const stopCamera = () => {
_stopCamera()
if (videoRef.value) {
videoRef.value.srcObject = null
}
}
const retakeImage = () => {
capturedImage.value = null
startCamera()
}
const captureImage = () => {
const video = videoRef.value
const canvas = canvasRef.value
if (!video || !canvas) return
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const context = canvas.getContext('2d')
if (context) {
canvas.style.display = 'block'
context.translate(canvas.width, 0)
context.scale(-1, 1)
context.drawImage(video, 0, 0, canvas.width, canvas.height)
canvas.toBlob((blob) => {
if (!blob) return
capturedImage.value = new File([blob], `${new Date().toDateString()}.png`, { type: 'image/png' })
}, 'image/png')
stopCamera()
}
}
const closeMenu = () => {
emits('update:visible', false)
}
onMounted(() => {
startCamera()
})
onBeforeUnmount(() => {
stopCamera()
})
</script>
<template>
<div class="w-full relative h-full">
<NcTooltip class="absolute top-3 right-2">
<NcButton type="text" class="!border-0" size="xsmall" @click="closeMenu">
<GeneralIcon icon="close" />
</NcButton>
<template #title> {{ $t('general.close') }} </template>
</NcTooltip>
<div v-if="!permissionGranted" class="w-full h-full flex bg-gray-50 items-center justify-center">
<div
class="flex flex-col hover:bg-white p-2 cursor-pointer rounded-md !transition-all transition-ease-in-out duration-300 gap-2 items-center justify-center"
@click="startCamera"
>
<div class="p-5 bg-white rounded-md shadow-sm">
<mdi-camera class="text-4xl text-gray-800" />
</div>
<h1 class="text-gray-800 font-semibold text-center text-xl">
{{ $t('labels.allowAccessToYourCamera') }}
</h1>
</div>
</div>
<div
v-else
:class="{
'py-8': !capturedImage,
'pt-8 pb-2': capturedImage,
}"
class="w-full gap-3 h-full flex-col flex items-center justify-between"
>
<div v-show="!capturedImage" class="w-full gap-3 h-full flex-col flex items-center justify-between">
<video ref="videoRef" class="rounded-md" style="width: 400px" autoplay></video>
<NcButton class="!rounded-full !px-0" @click="captureImage">
<mdi-camera class="text-xl" />
</NcButton>
</div>
<div v-show="capturedImage" class="flex group flex-col gap-1">
<canvas ref="canvasRef" class="rounded-md" style="width: 400px; display: none"></canvas>
<div class="relative text-[12px] font-semibold text-gray-800 flex">
<div class="flex-auto truncate line-height-4">
{{ capturedImage?.name }}
</div>
<div class="flex-none hide-ui transition-all transition-ease-in-out !h-4 flex items-center bg-white">
<NcTooltip placement="bottom">
<template #title> {{ $t('title.removeFile') }} </template>
<component :is="iconMap.delete" class="!text-red-500 cursor-pointer" @click="retakeImage" />
</NcTooltip>
</div>
</div>
<div class="flex-none text-[10px] font-semibold text-gray-500">
{{ formatBytes(capturedImage?.size, 0) }}
</div>
</div>
<div v-show="capturedImage" class="flex gap-2 pr-2 bottom-1 relative w-full items-center justify-end">
<NcButton :disabled="isLoading" type="secondary" size="small" @click="closeMenu">
{{ $t('labels.cancel') }}
</NcButton>
<NcButton :loading="isLoading" size="small" @click="emits('upload', [capturedImage] as File[])">
<template v-if="!isLoading"> {{ $t('labels.uploadImage') }} </template>
<template v-else> {{ $t('labels.uploading') }} </template>
</NcButton>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
video {
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}
</style>

190
packages/nc-gui/components/cell/attachment/UploadProviders/Local.vue

@ -0,0 +1,190 @@
<script setup lang="ts">
import { useAttachmentCell } from '../utils'
const emits = defineEmits<{
'update:visible': [value: boolean]
'upload': [fileList: File[]]
}>()
const dropZoneRef = ref<HTMLDivElement>()
const tempFiles = ref<File[]>([])
const { isLoading } = useAttachmentCell()!
const { files, open: _open } = useFileDialog({
reset: true,
})
watch(files, (newFiles) => {
if (!newFiles) return
Object.values(newFiles).forEach((file) => {
tempFiles.value.push(file)
})
})
const onDrop = (files: File[], event: DragEvent) => {
tempFiles.value.push(...files)
event.preventDefault()
event.stopPropagation()
}
const thumbnails = computedAsync(async () => {
const map = new Map()
await Promise.all(
tempFiles.value.map(async (file) => {
const thumbnail = await createThumbnail(file)
if (thumbnail) {
map.set(file, thumbnail)
}
}),
)
return map
})
const onRemoveFileClick = (file: File) => {
tempFiles.value = tempFiles.value.filter((f) => f !== file)
}
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
const clearAll = () => {
tempFiles.value = []
}
const open = () => {
_open()
}
const closeMenu = () => {
emits('update:visible', false)
}
onBeforeUnmount(() => {
tempFiles.value = []
})
</script>
<template>
<div
:class="{
'flex flex-col relative justify-center items-center': !tempFiles.length,
}"
class="w-full p-2 h-full"
>
<NcTooltip v-if="tempFiles.length === 0" class="absolute top-3 right-3">
<NcButton type="text" class="!border-0" size="xsmall" @click="closeMenu">
<GeneralIcon icon="close" />
</NcButton>
<template #title> {{ $t('general.close') }} </template>
</NcTooltip>
<div v-if="tempFiles.length > 0" class="flex w-full border-b-1 py-1 h-9.5 items-center justify-between top-0">
<NcButton type="text" size="small" @click="clearAll">
{{ $t('labels.clearAllFiles') }}
</NcButton>
<span class="text-xs">
{{ tempFiles.length }} files, total size:
{{
formatBytes(
tempFiles.reduce((acc, file) => acc + file.size, 0),
0,
)
}}
</span>
<NcButton type="text" size="small" @click="open">
<div class="flex gap-1 items-center">
<component :is="iconMap.plus" />
{{ $t('labels.addMore') }}
</div>
</NcButton>
</div>
<div
ref="dropZoneRef"
:class="{
'border-brand-500': isOverDropZone,
'border-dashed border-1': !tempFiles.length,
}"
data-testid="attachment-drop-zone"
:style="`height: ${tempFiles.length > 0 ? '324px' : '100%'}`"
class="flex flex-col items-center justify-center h-full w-full flex-grow-1 rounded-lg"
@click="tempFiles.length > 0 ? () => {} : open()"
>
<div v-if="!tempFiles.length" class="flex cursor-pointer items-center justify-center flex-col gap-2">
<template v-if="!isOverDropZone">
<component :is="iconMap.upload" class="w-5 h-5" />
<h1>
{{ $t('labels.clickTo') }}
<span class="font-semibold"> {{ $t('labels.browseFiles') }} </span>
{{ $t('general.or') }}
<span class="font-semibold"> {{ $t('labels.dragFilesHere') }} </span>
{{ $t('labels.toUpload') }}
</h1>
</template>
<template v-if="isOverDropZone">
<component :is="iconMap.upload" class="w-5 text-brand-500 h-5" />
<h1 class="text-brand-500 font-bold">{{ $t('labels.dropHere') }}</h1>
</template>
</div>
<template v-else>
<div
class="grid overflow-y-auto flex-grow-1 nc-scrollbar-md grid-cols-4 w-full h-full items-start py-2 justify-center gap-4"
>
<div v-for="file in tempFiles" :key="file.name" class="flex gap-1.5 group min-w-34 max-w-28 pb-4 flex-col relative">
<div
v-if="!thumbnails.get(file)"
style="height: 140px"
class="flex items-center justify-center rounded-md bg-gray-300"
>
<component :is="iconMap.file" class="w-16 h-16" />
</div>
<img v-else :src="thumbnails.get(file)" style="height: 140px" alt="thumbnail" class="rounded-md object-cover" />
<div class="relative text-[12px] font-semibold items-center text-gray-800 flex">
<NcTooltip class="flex-auto truncate" placement="bottom">
<template #title> {{ file.name }} </template>
{{ file.name }}
</NcTooltip>
<div class="flex-none hide-ui transition-all transition-ease-in-out !h-4 flex items-center bg-white">
<NcTooltip placement="bottom">
<template #title> {{ $t('title.removeFile') }} </template>
<component :is="iconMap.delete" class="!text-red-500 w-3 h-3 cursor-pointer" @click="onRemoveFileClick(file)" />
</NcTooltip>
</div>
</div>
<div class="flex-none text-[10px] font-semibold text-gray-500">
{{ formatBytes(file.size, 0) }}
</div>
</div>
</div>
</template>
</div>
<div v-if="tempFiles.length" class="flex gap-2 pt-1 bg-white w-full items-center justify-end">
<NcButton :disabled="isLoading" type="secondary" size="small" @click="closeMenu">
{{ $t('labels.cancel') }}
</NcButton>
<NcButton :loading="isLoading" data-testid="nc-upload-file" size="small" @click="emits('upload', tempFiles)">
<template v-if="isLoading">
{{ $t('labels.uploading') }}
</template>
<template v-else> {{ $t('general.upload') }} {{ tempFiles.length }} {{ $t('objects.files') }} </template>
</NcButton>
</div>
</div>
</template>
<style lang="scss">
.hide-ui {
@apply h-0 w-0 overflow-hidden whitespace-nowrap;
.group:hover & {
@apply h-auto w-auto overflow-visible whitespace-normal;
}
}
</style>

173
packages/nc-gui/components/cell/attachment/UploadProviders/Url.vue

@ -0,0 +1,173 @@
<script setup lang="ts">
import { useAttachmentCell } from '../utils'
const emits = defineEmits<{
'update:visible': [value: boolean]
}>()
const { openAttachment } = useAttachment()
const { uploadViaUrl, updateModelValue } = useAttachmentCell()!
const closeMenu = () => {
emits('update:visible', false)
}
const inputRef = ref<HTMLInputElement | null>(null)
const tempAttachments = ref<
{
url?: string
mimetype: string
title: string
path?: string
size: number
}[]
>([])
const onSave = async () => {
updateModelValue(tempAttachments.value)
closeMenu()
}
const url = ref('')
const isParsing = ref(false)
const deleteAttachment = (index: number) => {
tempAttachments.value.splice(index, 1)
}
const isValidUrl = ref(true)
const errorMessage = ref('')
const uploadAndParseUrl = async () => {
if (!isValidURL(url.value)) {
isValidUrl.value = false
return
}
isValidUrl.value = true
try {
isParsing.value = true
const data = await uploadViaUrl({ url: url.value }, true)
if (typeof data !== 'string' && data?.length) {
tempAttachments.value = [...data, ...tempAttachments.value]
url.value = ''
} else {
isValidUrl.value = false
errorMessage.value = data
}
} finally {
isParsing.value = false
}
await nextTick(() => {
inputRef.value?.focus()
})
}
watch(url, () => {
isValidUrl.value = true
})
</script>
<template>
<div class="py-2 px-2 h-full flex gap-2 flex-col">
<div class="flex w-full bg-white border-b-1 py-1 justify-between">
<h1 class="font-semibold">
{{ $t('title.uploadViaUrl') }}
</h1>
<NcTooltip>
<NcButton type="secondary" class="!border-0" size="xsmall" @click="closeMenu">
<GeneralIcon icon="close" />
</NcButton>
<template #title> {{ $t('general.close') }} </template>
</NcTooltip>
</div>
<div class="flex-grow bg-white">
<h1 class="text-gray-800 font-semibold">
{{ $t('labels.addFilesFromUrl') }}
</h1>
<div class="flex bg-white gap-2">
<a-input
ref="inputRef"
v-model:value="url"
type="url"
:disabled="isParsing"
class="flex-grow"
placeholder="www.google.com/hello.png"
@keydown.enter="uploadAndParseUrl"
/>
<NcButton :disabled="!isValidUrl" :loading="isParsing" size="small" class="!h-10 !px-4" @click="uploadAndParseUrl">
{{ $t('general.upload') }}
</NcButton>
</div>
<span v-if="url.length > 0 && !isValidUrl" class="text-red-500 text-[13px]">
{{ errorMessage.length > 0 ? errorMessage : $t('labels.enterValidUrl') }}
</span>
<template v-if="tempAttachments.length > 0">
<div :style="`height: ${!isValidUrl ? '208px' : '230px'}`" class="overflow-y-auto bg-white mt-1 !max-h-[250px]">
<h1 class="font-semibold capitalize sticky top-0 bg-white text-gray-800">
{{ $t('objects.files') }}
</h1>
<div
v-for="(file, index) in tempAttachments"
:key="index"
class="flex w-full items-center mt-2 h-10 px-2 py-1 border-1 rounded-md"
>
<div class="flex w-full items-center gap-2">
<GeneralIcon icon="file" />
{{ file.title }}
<NcTooltip class="hover:underline">
<NuxtLink class="flex items-center" target="_blank" @click="openAttachment(file)">
<component :is="iconMap.externalLink" class="w-3.5 h-3.5 text-gray-500" />
</NuxtLink>
<template #title> {{ $t('labels.openFile') }} </template>
</NcTooltip>
</div>
<div class="flex-grow-1"></div>
<NcTooltip>
<template #title> {{ $t('title.removeFile') }} </template>
<NcButton type="text" size="xsmall" @click="deleteAttachment(index)">
<GeneralIcon icon="close" />
</NcButton>
</NcTooltip>
</div>
</div>
</template>
</div>
<div class="flex gap-2 items-center justify-end">
<NcButton :disabled="isParsing" type="secondary" size="small" @click="closeMenu"> {{ $t('labels.cancel') }} </NcButton>
<NcButton :disabled="isParsing || tempAttachments.length === 0" size="small" @click="onSave">
{{ $t('activity.addFiles') }}</NcButton
>
</div>
</div>
</template>
<style scoped lang="scss">
.ant-input::placeholder {
@apply text-gray-500;
}
.ant-input {
@apply px-4 rounded-lg py-2 w-full border-1 focus:border-brand-500 border-gray-200 !ring-0;
}
a {
@apply !text-gray-700 !no-underline !hover:underline;
}
</style>

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

@ -49,7 +49,6 @@ const {
visibleItems,
onDrop,
isLoading,
open: _open,
FileIcon,
selectedImage,
isReadonly,
@ -118,8 +117,11 @@ watch(
},
)
const isNewAttachmentModalOpen = ref(false)
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
if (e.key === 'Enter' && !isReadonly.value) {
if (isNewAttachmentModalOpen.value) return
e.stopPropagation()
if (!modalVisible.value && !isMobileMode.value) {
modalVisible.value = true
@ -132,10 +134,14 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
const rowHeight = inject(RowHeightInj, ref())
const openAttachmentModal = () => {
isNewAttachmentModalOpen.value = true
}
const open = (e: Event) => {
e.stopPropagation()
_open()
openAttachmentModal()
}
const openAttachment = (item: any) => {
@ -207,7 +213,7 @@ const handleFileDelete = (i: number) => {
isGrid ? '22px' : '32px'
})`,
}"
class="nc-attachment-cell relative flex color-transition flex items-center w-full xs:(min-h-12 max-h-32)"
class="nc-attachment-cell relative flex color-transition gap-2 flex items-center w-full xs:(min-h-12 max-h-32)"
:class="{ 'justify-center': !active, 'justify-between': active, 'px-2': isExpandedForm }"
>
<LazyCellAttachmentCarousel />
@ -228,7 +234,7 @@ const handleFileDelete = (i: number) => {
<div
v-if="!isReadonly"
: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)"
class="group cursor-pointer py-1 flex gap-1 items-center rounded border-none shadow-sm hover:(bg-primary bg-opacity-10)"
data-testid="attachment-cell-file-picker-button"
tabindex="0"
@click="open"
@ -246,9 +252,7 @@ const handleFileDelete = (i: number) => {
v-if="active || !visibleItems.length || (isForm && visibleItems.length)"
class="flex items-center gap-1 xs:(w-full min-w-12 h-7 justify-center)"
>
<MaterialSymbolsAttachFile
class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-tiny"
/>
<MaterialSymbolsAttachFile class="text-gray-500 text-tiny" />
<div
v-if="!visibleItems.length"
data-rec="true"
@ -268,9 +272,9 @@ const handleFileDelete = (i: number) => {
:class="{
'justify-center': !isExpandedForm && !isGallery && !isKanban,
'py-1': rowHeight === 1 && !isForm && !isExpandedForm,
'py-1.5': rowHeight !== 1 || isForm || isExpandedForm,
'py-1.5 !gap-4 ': rowHeight !== 1 || isForm || isExpandedForm,
}"
class="nc-attachment-wrapper flex cursor-pointer w-full items-center flex-wrap gap-2 nc-scrollbar-thin mt-0 items-start px-[1px]"
class="nc-attachment-wrapper flex cursor-pointer w-full items-center flex-wrap gap-3 nc-scrollbar-thin mt-0 items-start px-[1px]"
:style="{
maxHeight: isForm || isExpandedForm ? undefined : `max(100%, ${isGrid ? '22px' : '32px'})`,
}"
@ -283,7 +287,7 @@ const handleFileDelete = (i: number) => {
<div v-if="isImage(item.title, item.mimetype ?? item.type)">
<div
class="nc-attachment flex items-center flex-col flex-wrap justify-center flex-auto"
:class="{ 'ml-2': active, '!w-30': isForm || isExpandedForm }"
:class="{ '!w-30': isForm || isExpandedForm }"
@click="() => onImageClick(item)"
>
<LazyCellAttachmentImage
@ -333,18 +337,16 @@ const handleFileDelete = (i: number) => {
<div
v-if="active || (isForm && visibleItems.length)"
class="xs:hidden h-6 w-5.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 group cursor-pointer flex gap-1 items-center rounded border-none p-1"
>
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<NcTooltip v-else placement="bottom" class="flex">
<template #title> {{ $t('activity.viewAttachment') }}</template>
<component
:is="iconMap.expand"
class="flex-none transform dark:(!text-white) group-hover:(!text-grey-800 scale-120) text-gray-500 text-sm"
@click.stop="onExpand"
/>
<NcButton type="text" size="xsmall" @click.stop="onExpand">
<component :is="iconMap.expand" />
</NcButton>
</NcTooltip>
</div>
</template>
@ -372,6 +374,7 @@ const handleFileDelete = (i: number) => {
</template>
</LazyGeneralDeleteModal>
</div>
<LazyCellAttachmentAttachFile v-if="isNewAttachmentModalOpen" v-model:value="isNewAttachmentModalOpen" />
</template>
<style lang="scss">

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

@ -35,6 +35,10 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
/** for image carousel */
const selectedImage = ref()
const videoStream = ref<MediaStream | null>(null)
const permissionGranted = ref(false)
const { base } = storeToRefs(useBase())
const { api, isLoading } = useApi()
@ -57,6 +61,18 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
}),
}
const startCamera = async () => {
if (!videoStream.value) {
videoStream.value = await navigator.mediaDevices.getUserMedia({ video: true })
}
permissionGranted.value = true
}
const stopCamera = () => {
videoStream.value?.getTracks().forEach((track) => track.stop())
videoStream.value = null
}
/** our currently visible items, either the locally stored or the ones from db, depending on isPublic & isForm status */
const visibleItems = computed<any[]>(() => (isPublic.value && isForm.value ? storedFiles.value : attachments.value))
@ -214,22 +230,33 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
message.error(e.message || t('msg.error.internalError'))
}
} else if (imageUrls.length) {
try {
const data = await api.storage.uploadByUrl(
{
path: [NOCO, base.value.id, meta.value?.id, column.value?.id].join('/'),
},
imageUrls,
)
newAttachments.push(...data)
} catch (e: any) {
message.error(e.message || t('msg.error.internalError'))
}
const data = uploadViaUrl(imageUrls)
if (!data) return
newAttachments.push(...data)
}
updateModelValue(JSON.stringify([...attachments.value, ...newAttachments]))
}
async function uploadViaUrl(url: AttachmentReqType | AttachmentReqType[], returnError = false) {
const imageUrl = Array.isArray(url) ? url : [url]
try {
const data = await api.storage.uploadByUrl(
{
path: [NOCO, base.value.id, meta.value?.id, column.value?.id].join('/'),
},
imageUrl,
)
return data
} catch (e: any) {
console.log(e)
if (returnError) {
return "File couldn't be uploaded. Verify URL & try again."
}
message.error("File couldn't be uploaded. Verify URL & try again.")
return null
}
}
async function renameFile(attachment: AttachmentType, idx: number) {
return new Promise<boolean>((resolve) => {
const { close } = useDialog(RenameFile, {
@ -360,10 +387,15 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
downloadFile,
updateModelValue,
selectedImage,
uploadViaUrl,
selectedVisibleItems,
storedFiles,
bulkDownloadFiles,
defaultAttachmentMeta,
startCamera,
stopCamera,
videoStream,
permissionGranted,
}
},
'useAttachmentCell',

1
packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue

@ -55,6 +55,7 @@ onMounted(() => {
<template>
<div class="flex w-full flex-col py-0.9 px-1 border-gray-200 gap-y-1">
<LazyGeneralMaintenanceAlert />
<div class="flex items-center pr-2 justify-between">
<NcDropdown v-model:visible="isMenuOpen" placement="topLeft" overlay-class-name="!min-w-64">
<div

3
packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue

@ -94,7 +94,7 @@ function openAirtableImportDialog(baseId?: string, sourceId?: string) {
}
function openTableCreateMagicDialog(sourceId?: string) {
if (!sourceId) return
if (!sourceId || !base.value?.id) return
$e('c:table:create:navdraw')
@ -102,6 +102,7 @@ function openTableCreateMagicDialog(sourceId?: string) {
const { close } = useDialog(resolveComponent('DlgTableMagic'), {
'modelValue': isOpen,
'baseId': base.value.id,
'sourceId': sourceId,
'onUpdate:modelValue': closeDialog,
})

3
packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue

@ -38,7 +38,7 @@ function openAirtableImportDialog(baseId?: string, sourceId?: string) {
}
function openQuickImportDialog(type: string) {
if (!source.value?.id) return
if (!source.value?.id || !source.value.base_id) return
$e(`a:actions:import-${type}`)
@ -47,6 +47,7 @@ function openQuickImportDialog(type: string) {
const { close } = useDialog(resolveComponent('DlgQuickImport'), {
'modelValue': isOpen,
'importType': type,
'baseId': source.value.base_id,
'sourceId': source.value.id,
'onUpdate:modelValue': closeDialog,
})

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

@ -984,7 +984,7 @@ async function openAudit(source: SourceType) {
<DlgProjectDuplicate v-if="selectedProjectToDuplicate" v-model="isDuplicateDlgOpen" :base="selectedProjectToDuplicate" />
<GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]">
<LazyDashboardSettingsErd :source-id="activeBaseId" />
<LazyDashboardSettingsErd :base-id="base?.id" :source-id="activeBaseId" />
</div>
</GeneralModal>
</template>

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

@ -5,6 +5,8 @@ import 'splitpanes/dist/splitpanes.css'
const router = useRouter()
const route = router.currentRoute
const { setLeftSidebarSize } = useGlobal()
const { isMobileMode } = storeToRefs(useConfigStore())
const {
@ -30,22 +32,32 @@ const currentSidebarSize = computed({
const { handleSidebarOpenOnMobileForNonViews } = useConfigStore()
const contentSize = computed(() => 100 - sideBarSize.value.current)
const mobileNormalizedContentSize = computed(() => {
if (isMobileMode.value) {
return isLeftSidebarOpen.value ? 0 : 100
}
return contentSize.value
return 100 - leftSidebarWidthPercent.value
})
const sidebarWidth = computed(() =>
isMobileMode.value ? viewportWidth.value : (sideBarSize.value.old * viewportWidth.value) / 100,
)
watch(currentSidebarSize, () => {
leftSidebarWidthPercent.value = currentSidebarSize.value
leftSidebarWidthPercent.value = (currentSidebarSize.value / viewportWidth.value) * 100
setLeftSidebarSize(currentSidebarSize.value)
})
const sidebarWidth = computed(() => (isMobileMode.value ? viewportWidth.value : sideBarSize.value.old))
const normalizedWidth = computed(() => {
const maxSize = remToPx(viewportWidth.value <= 1560 ? 20 : 35)
const minSize = remToPx(16)
if (sidebarWidth.value > maxSize) {
return maxSize
} else if (sidebarWidth.value < minSize) {
return minSize
} else {
return sidebarWidth.value
}
})
watch(isLeftSidebarOpen, () => {
@ -87,10 +99,15 @@ function handleMouseMove(e: MouseEvent) {
}
}
function onWindowResize() {
function onWindowResize(e?: any): void {
viewportWidth.value = window.innerWidth
onResize(currentSidebarSize.value)
leftSidebarWidthPercent.value = (currentSidebarSize.value / viewportWidth.value) * 100
// if sidebar width is greater than normalized width and this function is called from window resize event (not from template) update left sidebar width
if (e && normalizedWidth.value < sidebarWidth.value) {
onResize(leftSidebarWidthPercent.value)
}
}
onMounted(() => {
@ -138,10 +155,9 @@ function onResize(widthPercent: any) {
const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
// If the viewport width is less than 1560px, the max sidebar width should be 20rem
if (viewportWidth.value <= 1560) {
if (width > remToPx(20)) {
sideBarSize.value.old = ((20 * fontSize) / viewportWidth.value) * 100
sideBarSize.value.old = 20 * fontSize
if (isLeftSidebarOpen.value) sideBarSize.value.current = sideBarSize.value.old
return
}
@ -150,31 +166,19 @@ function onResize(widthPercent: any) {
const widthRem = width / fontSize
if (widthRem < 16) {
sideBarSize.value.old = ((16 * fontSize) / viewportWidth.value) * 100
sideBarSize.value.old = 16 * fontSize
if (isLeftSidebarOpen.value) sideBarSize.value.current = sideBarSize.value.old
return
} else if (widthRem > 35) {
sideBarSize.value.old = ((35 * fontSize) / viewportWidth.value) * 100
sideBarSize.value.old = 35 * fontSize
if (isLeftSidebarOpen.value) sideBarSize.value.current = sideBarSize.value.old
return
}
sideBarSize.value.old = widthPercent
sideBarSize.value.old = width
sideBarSize.value.current = sideBarSize.value.old
}
const normalizedWidth = computed(() => {
const maxSize = remToPx(35)
const minSize = remToPx(16)
if (sidebarWidth.value > maxSize) {
return maxSize
} else if (sidebarWidth.value < minSize) {
return minSize
} else {
return sidebarWidth.value
}
})
</script>
<template>
@ -192,7 +196,8 @@ const normalizedWidth = computed(() => {
max-size="60%"
class="nc-sidebar-splitpane !sm:max-w-140 relative !overflow-visible flex"
:style="{
width: `${mobileNormalizedSidebarSize}%`,
'width': `${mobileNormalizedSidebarSize}%`,
'min-width': `${mobileNormalizedSidebarSize}%`,
}"
>
<div
@ -215,7 +220,7 @@ const normalizedWidth = computed(() => {
:size="mobileNormalizedContentSize"
class="flex-grow"
:style="{
'min-width': `${100 - mobileNormalizedSidebarSize}%`,
'min-width': `${mobileNormalizedContentSize}%`,
}"
>
<slot name="content" />

13
packages/nc-gui/components/dashboard/settings/DataSources.vue

@ -5,6 +5,7 @@ import { ClientType } from '#imports'
interface Props {
state: string
baseId: string
reload?: boolean
}
@ -20,10 +21,9 @@ const { $api, $e } = useNuxtApp()
const basesStore = useBases()
const { loadProject } = basesStore
const { isDataSourceLimitReached } = storeToRefs(basesStore)
const { isDataSourceLimitReached, bases } = storeToRefs(basesStore)
const baseStore = useBase()
const { base } = storeToRefs(baseStore)
const base = computed(() => bases.value.get(props.baseId) ?? {})
const { isUIAllowed } = useRoles()
@ -294,7 +294,12 @@ const openedTab = ref('erd')
</div>
</template>
<div class="h-full pt-4">
<LazyDashboardSettingsErd class="h-full overflow-auto" :source-id="activeSource.id" :show-all-columns="false" />
<LazyDashboardSettingsErd
class="h-full overflow-auto"
:base-id="baseId"
:source-id="activeSource.id"
:show-all-columns="false"
/>
</div>
</a-tab-pane>
<a-tab-pane v-if="sources && activeSource === sources[0]" key="audit">

3
packages/nc-gui/components/dashboard/settings/Erd.vue

@ -2,11 +2,12 @@
const props = defineProps<{
sourceId: string
showAllColumns?: boolean
baseId?: string
}>()
</script>
<template>
<div class="w-full h-full !p-0">
<ErdView :source-id="props.sourceId" :show-all-columns="props.showAllColumns" />
<ErdView :base-id="props.baseId" :source-id="props.sourceId" :show-all-columns="props.showAllColumns" />
</div>
</template>

15
packages/nc-gui/components/dashboard/settings/UIAcl.vue

@ -11,10 +11,15 @@ const { t } = useI18n()
const { $api, $e } = useNuxtApp()
const { base } = storeToRefs(useBase())
const { base: activeBase } = storeToRefs(useBase())
const _projectId = inject(ProjectIdInj, ref())
const baseId = computed(() => _projectId.value ?? base.value?.id)
const baseId = computed(() => _projectId.value ?? activeBase.value?.id!)
const { bases } = storeToRefs(useBases())
const base = computed(() => bases.value.get(baseId.value) ?? {})
const { includeM2M } = useGlobal()
@ -222,14 +227,14 @@ const toggleSelectAll = (role: Role) => {
<div v-for="role in roles" :key="role">
<div v-if="column.name === role">
<a-tooltip>
<NcTooltip>
<template #title>
<span v-if="record.disabled[role]">
{{ $t('labels.clickToMake') }} '{{ record.title }}' {{ $t('labels.visibleForRole') }} {{ role }}
{{ $t('labels.inUI') }} dashboard</span
>
<span v-else
>{{ $t('labels.clickToHide') }}'{{ record.title }}' {{ $t('labels.forRole') }}:{{ role }}
>{{ $t('labels.clickToHide') }} '{{ record.title }}' {{ $t('labels.forRole') }}:{{ role }}
{{ $t('labels.inUI') }}</span
>
</template>
@ -239,7 +244,7 @@ const toggleSelectAll = (role: Role) => {
:class="`nc-acl-${record.title}-${role}-chkbox !ml-0.25`"
@change="onRoleCheck(record, role as Role)"
/>
</a-tooltip>
</NcTooltip>
</div>
</div>
</template>

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

@ -9,11 +9,12 @@ import importWorkerUrl from '~/workers/importWorker?worker&url'
interface Props {
modelValue: boolean
importType: 'csv' | 'json' | 'excel'
baseId: string
sourceId: string
importDataOnly?: boolean
}
const { importType, importDataOnly = false, sourceId, ...rest } = defineProps<Props>()
const { importType, importDataOnly = false, baseId, sourceId, ...rest } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
@ -33,6 +34,10 @@ const progressMsg = ref('Parsing Data ...')
const { tables } = storeToRefs(useBase())
const tablesStore = useTablesStore()
const { loadProjectTables } = tablesStore
const { baseTables } = storeToRefs(tablesStore)
const activeKey = ref('uploadTab')
const jsonEditorRef = ref()
@ -65,6 +70,7 @@ const defaultImportState = {
autoSelectFieldTypes: true,
firstRowAsHeaders: true,
shouldImportData: true,
importDataOnly: true,
},
}
const importState = reactive(defaultImportState)
@ -79,7 +85,6 @@ const IsImportTypeExcel = computed(() => importType === 'excel')
const validators = computed(() => ({
url: [fieldRequiredValidator(), importUrlValidator, isImportTypeCsv.value ? importCsvUrlValidator : importExcelUrlValidator],
maxRowsToParse: [fieldRequiredValidator()],
}))
const { validate, validateInfos } = useForm(importState, validators)
@ -147,10 +152,6 @@ const disableImportButton = computed(() => !templateEditorRef.value?.isValid ||
const disableFormatJsonButton = computed(() => !jsonEditorRef.value?.isValid)
const modalWidth = computed(() => {
if (importType === 'excel' && templateEditorModal.value) {
return 'max(90vw, 600px)'
}
return 'max(60vw, 600px)'
})
@ -160,6 +161,10 @@ async function handlePreImport() {
preImportLoading.value = true
isParsingData.value = true
if (!baseTables.value.get(baseId)) {
await loadProjectTables(baseId)
}
if (activeKey.value === 'uploadTab') {
if (isImportTypeCsv.value || (isWorkerSupport && importWorker)) {
await parseAndExtractData(importState.fileList as streamImportFileList)
@ -245,10 +250,11 @@ function formatJson() {
jsonEditorRef.value?.format()
}
function populateUniqueTableName(tn: string) {
function populateUniqueTableName(tn: string, draftTn: string[] = []) {
let c = 1
while (
tables.value.some((t: TableType) => {
draftTn.includes(tn) ||
baseTables.value.get(baseId)?.some((t: TableType) => {
const s = t.table_name.split('___')
let target = t.table_name
if (s.length > 1) target = s[1]
@ -483,10 +489,13 @@ async function parseAndExtractData(val: UploadFile[] | ArrayBuffer | string) {
if (importDataOnly) importColumns.value = templateGenerator!.getColumns()
else {
// ensure the target table name not exist in current table list
templateData.value.tables = templateData.value.tables.map((table: Record<string, any>) => ({
...table,
table_name: populateUniqueTableName(table.table_name),
}))
const draftTableNames = [] as string[]
templateData.value.tables = templateData.value.tables.map((table: Record<string, any>) => {
const table_name = populateUniqueTableName(table.table_name, draftTableNames)
draftTableNames.push(table_name)
return { ...table, table_name }
})
}
importData.value = templateGenerator!.getData()
}
@ -508,6 +517,11 @@ const onError = () => {
const onChange = () => {
isError.value = false
}
onMounted(() => {
importState.parserConfig.importDataOnly = importDataOnly
importState.parserConfig.autoSelectFieldTypes = importDataOnly
})
</script>
<template>
@ -522,7 +536,12 @@ const onChange = () => {
<div class="px-5">
<div class="prose-xl font-weight-bold my-5">{{ importMeta.header }}</div>
<div class="mt-5">
<div
class="mt-5"
:class="{
'mb-4': templateEditorModal,
}"
>
<LazyTemplateEditor
v-if="templateEditorModal"
ref="templateEditorRef"
@ -532,6 +551,7 @@ const onChange = () => {
:import-data-only="importDataOnly"
:quick-import-type="importType"
:max-rows-to-parse="importState.parserConfig.maxRowsToParse"
:base-id="baseId"
:source-id="sourceId"
:import-worker="importWorker"
class="nc-quick-import-template-editor"
@ -572,6 +592,9 @@ const onChange = () => {
<p class="ant-upload-hint">
{{ importMeta.uploadHint }}
</p>
<template #removeIcon>
<component :is="iconMap.deleteListItem" />
</template>
</a-upload-dragger>
</div>
</a-tab-pane>
@ -598,9 +621,9 @@ const onChange = () => {
</template>
<div class="pr-10 pt-5">
<a-form :model="importState" name="quick-import-url-form" layout="vertical" class="mb-0">
<a-form :model="importState" name="quick-import-url-form" layout="vertical" class="mb-0 !ml-0.5">
<a-form-item :label="importMeta.urlInputLabel" v-bind="validateInfos.url">
<a-input v-model:value="importState.url" size="large" />
<a-input v-model:value="importState.url" size="large" class="!rounded-md" />
</a-form-item>
</a-form>
</div>
@ -615,16 +638,6 @@ const onChange = () => {
<!-- Advanced Settings -->
<span class="prose-lg">{{ $t('title.advancedSettings') }}</span>
<a-form-item class="!my-2" :label="t('msg.info.footMsg')" v-bind="validateInfos.maxRowsToParse">
<a-input-number v-model:value="importState.parserConfig.maxRowsToParse" :min="1" :max="50000" />
</a-form-item>
<a-form-item v-if="!importDataOnly" class="!my-2">
<a-checkbox v-model:checked="importState.parserConfig.autoSelectFieldTypes">
<span class="caption">{{ $t('labels.autoSelectFieldTypes') }}</span>
</a-checkbox>
</a-form-item>
<a-form-item v-if="isImportTypeCsv || IsImportTypeExcel" class="!my-2">
<a-checkbox v-model:checked="importState.parserConfig.firstRowAsHeaders">
<span class="caption">{{ $t('labels.firstRowAsHeaders') }}</span>
@ -689,3 +702,12 @@ const onChange = () => {
</template>
</a-modal>
</template>
<style lang="scss" scoped>
:deep(.ant-upload-list-item-thumbnail) {
line-height: 48px;
}
:deep(.ant-upload-list-item-card-actions-btn.ant-btn-icon-only) {
@apply !h-6;
}
</style>

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

@ -2,18 +2,19 @@
import type { ComponentPublicInstance } from '@vue/runtime-core'
import { capitalize } from '@vue/runtime-core'
import type { Form as AntForm, SelectProps } from 'ant-design-vue'
import type {
CalendarType,
ColumnType,
FormType,
GalleryType,
GridType,
KanbanType,
LookupType,
MapType,
TableType,
import {
type CalendarType,
type ColumnType,
type FormType,
FormulaDataTypes,
type GalleryType,
type GridType,
type KanbanType,
type LookupType,
type MapType,
type TableType,
} from 'nocodb-sdk'
import { UITypes, ViewTypes, isSystemColumn } from 'nocodb-sdk'
import { UITypes, ViewTypes } from 'nocodb-sdk'
interface Props {
modelValue: boolean
@ -370,7 +371,11 @@ onMounted(async () => {
if (props.type === ViewTypes.CALENDAR) {
viewSelectFieldOptions.value = meta
.value!.columns!.filter((el) => el.uidt === UITypes.Date || (el.uidt === UITypes.DateTime && !isSystemColumn(el)))
.value!.columns!.filter(
(el) =>
[UITypes.DateTime, UITypes.Date, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(el.uidt) ||
(el.uidt === UITypes.Formula && (el.colOptions as any)?.parsed_tree?.dataType === FormulaDataTypes.DATE),
)
.map((field) => {
return {
value: field.id,
@ -378,6 +383,17 @@ onMounted(async () => {
uidt: field.uidt,
}
})
.sort((a, b) => {
const priority = {
[UITypes.DateTime]: 1,
[UITypes.Date]: 2,
[UITypes.Formula]: 3,
[UITypes.CreatedTime]: 4,
[UITypes.LastModifiedTime]: 5,
}
return (priority[a.uidt] || 6) - (priority[b.uidt] || 6)
})
if (viewSelectFieldOptions.value?.length) {
// take the first option

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

@ -22,13 +22,19 @@ const props = defineProps({
},
})
const { baseTables: _baseTables } = storeToRefs(useTablesStore())
const { sources, base } = storeToRefs(useBase())
const { bases } = storeToRefs(useBases())
const baseId = computed(() => props.baseId ?? base.value!.id)
const { base: activeBase } = storeToRefs(useBase())
const baseId = computed(() => props.baseId ?? activeBase.value!.id!)
const tablesStore = useTablesStore()
const { baseTables: _baseTables } = storeToRefs(tablesStore)
const baseTables = computed(() => _baseTables.value.get(baseId.value) ?? [])
const sources = computed<SourceType[]>(() => bases.value.get(baseId.value)?.sources || [])
const { metas, getMeta } = useMetas()
const tables = ref<TableType[]>([])
@ -128,6 +134,14 @@ watch(
config.showPkAndFk = config.showAllColumns
},
)
onMounted(async () => {
if (!props.baseId) return
if (!_baseTables.value.get(props.baseId)) {
await tablesStore.loadProjectTables(props.baseId)
}
})
</script>
<template>

5
packages/nc-gui/components/general/MaintenanceAlert.vue

@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template></template>
<style scoped lang="scss"></style>

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

@ -145,7 +145,7 @@ const pageSizeOptions = [
<template #overlay>
<NcMenu class="nc-pagination-menu overflow-hidden">
<NcSubMenu :key="`${localPageSize}page`" class="bg-gray-100 z-20 top-0 !sticky">
<NcSubMenu v-if="showSizeChanger" :key="`${localPageSize}page`" class="bg-gray-100 z-20 top-0 !sticky">
<template #title>
<div class="rounded-lg text-[13px] font-medium w-full">{{ localPageSize }} / page</div>
</template>

3
packages/nc-gui/components/project/ImportModal.vue

@ -36,7 +36,7 @@ function openAirtableImportDialog(baseId?: string, sourceId?: string) {
}
function openQuickImportDialog(type: 'csv' | 'excel' | 'json') {
if (!source.value.id) return
if (!source.value.id || !source.value.base_id) return
$e(`a:actions:import-${type}`)
@ -45,6 +45,7 @@ function openQuickImportDialog(type: 'csv' | 'excel' | 'json') {
const { close } = useDialog(resolveComponent('DlgQuickImport'), {
'modelValue': isOpen,
'importType': type,
'baseId': source.value.base_id,
'sourceId': source.value.id,
'onUpdate:modelValue': closeDialog,
})

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

@ -21,6 +21,8 @@ provide(ActiveViewInj, sharedView)
provide(IsPublicInj, ref(true))
provide(IsLockedInj, isLocked)
provide(ReloadAggregateHookInj, createEventHook())
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideViewGroupBy(sharedView, meta, xWhere, true)

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

@ -627,7 +627,7 @@ const handleSubmitRenameOrNewStack = async (loadMeta: boolean, stack?: any, stac
>
<div class="flex gap-2 items-center">
<component :is="iconMap.plus" class="flex-none w-4 h-4" />
{{ $t('activity.addNewRecord') }}
{{ $t('activity.newRecord') }}
</div>
</NcMenuItem>
<NcMenuItem

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

@ -38,8 +38,6 @@ const alignLeft = computed(() => props.alignLeft ?? false)
const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const count = computed(() => vPaginationData.value?.totalRows ?? Infinity)
const page = computed({
@ -99,7 +97,7 @@ const tempPageVal = ref(page.value)
class="flex items-center"
:class="{
'flex-1': !alignLeft,
'sticky left-0': isGroupBy,
'left-0 sticky': alignLeft,
}"
>
<slot name="add-record" />
@ -114,11 +112,9 @@ const tempPageVal = ref(page.value)
<div
v-if="!hidePagination"
class="transition-all duration-350"
class="transition-all ml-2 sticky left-0 duration-350"
:class="{
'-ml-17': isLeftSidebarOpen && !alignLeft,
'ml-8': alignLeft,
'sticky': isGroupBy,
'left-[159px]': isGroupBy && $slots['add-record'],
'left-[32px]': isGroupBy && !$slots['add-record'],
}"

35
packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue

@ -1,10 +1,11 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import type { ColumnType } from 'nocodb-sdk'
import { type ColumnType, UITypes } from 'nocodb-sdk'
const emit = defineEmits(['expandRecord', 'newRecord'])
const {
calDataType,
selectedDate,
selectedTime,
formattedData,
@ -577,6 +578,7 @@ const calculateNewRow = (event: MouseEvent, skipChangeCheck?: boolean) => {
const onResize = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit') || !container.value || !resizeRecord.value) return
if (resizeRecord.value.rowMeta.range?.is_readonly) return
const { top, bottom } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
@ -664,6 +666,8 @@ const onResizeEnd = () => {
const onResizeStart = (direction: 'right' | 'left', _event: MouseEvent, record: Row) => {
if (!isUIAllowed('dataEdit')) return
if (record.rowMeta.range?.is_readonly) return
resizeDirection.value = direction
resizeRecord.value = record
document.addEventListener('mousemove', onResize)
@ -722,6 +726,8 @@ const dragStart = (event: MouseEvent, record: Row) => {
// We use a timeout to determine if the user is dragging or clicking on the record
dragTimeout.value = setTimeout(() => {
if (!isUIAllowed('dataEdit')) return
if (record.rowMeta.range?.is_readonly) return
isDragging.value = true
while (!target.classList.contains('draggable-record')) {
target = target.parentElement as HTMLElement
@ -771,6 +777,8 @@ const dropEvent = (event: DragEvent) => {
isWithoutDates: boolean
} = JSON.parse(data)
if (record.rowMeta.range?.is_readonly) return
const fromCol = record.rowMeta.range?.fk_from_col
const toCol = record.rowMeta.range?.fk_to_col
@ -964,11 +972,11 @@ watch(
<template #overlay>
<NcMenu class="w-64">
<NcMenuItem> Select date field to add </NcMenuItem>
<NcMenuItem
v-for="(range, calIndex) in calendarRange"
:key="calIndex"
class="text-gray-800 font-semibold text-sm"
@click="
<template v-for="(range, calIndex) in calendarRange" :key="calIndex">
<NcMenuItem
v-if="!range.is_readonly"
class="text-gray-800 font-semibold text-sm"
@click="
() => {
let record = {
row: {
@ -986,17 +994,18 @@ watch(
emit('newRecord', record)
}
"
>
<div class="flex items-center gap-1">
<LazySmartsheetHeaderCellIcon :column-meta="range.fk_from_col" />
<span class="ml-1">{{ range.fk_from_col!.title! }}</span>
</div>
</NcMenuItem>
>
<div class="flex items-center gap-1">
<LazySmartsheetHeaderCellIcon :column-meta="range.fk_from_col" />
<span class="ml-1">{{ range.fk_from_col!.title! }}</span>
</div>
</NcMenuItem>
</template>
</NcMenu>
</template>
</NcDropdown>
<NcButton
v-else-if="!isPublic && isUIAllowed('dataEdit')"
v-else-if="!isPublic && isUIAllowed('dataEdit') && [UITypes.DateTime, UITypes.Date].includes(calDataType)"
:class="{
'!block': hour.isSame(selectedTime),
'!hidden': !hour.isSame(selectedTime),

16
packages/nc-gui/components/smartsheet/calendar/MonthView.vue

@ -151,7 +151,6 @@ const recordsToDisplay = computed<{
overflowCount: number
}
} = {}
if (!calendarRange.value) return []
const recordsToDisplay: Array<Row> = []
@ -555,6 +554,8 @@ const onResizeEnd = () => {
const onResizeStart = (direction: 'right' | 'left', event: MouseEvent, record: Row) => {
if (!isUIAllowed('dataEdit') || draggingId.value) return
if (record.rowMeta.range?.is_readonly) return
// selectedDate.value = null
resizeInProgress.value = true
resizeDirection.value = direction
@ -567,6 +568,7 @@ const onResizeStart = (direction: 'right' | 'left', event: MouseEvent, record: R
const stopDrag = (event: MouseEvent) => {
clearTimeout(dragTimeout.value)
if (!isUIAllowed('dataEdit') || !dragRecord.value || !isDragging.value) return
if (dragRecord.value.rowMeta.range?.is_readonly) return
event.preventDefault()
dragElement.value!.style.boxShadow = 'none'
@ -603,6 +605,7 @@ const dragStart = (event: MouseEvent, record: Row) => {
dragTimeout.value = setTimeout(() => {
if (!isUIAllowed('dataEdit')) return
if (record.rowMeta.range?.is_readonly) return
isDragging.value = true
while (!target.classList.contains('draggable-record')) {
@ -651,6 +654,8 @@ const dropEvent = (event: DragEvent) => {
isWithoutDates: boolean
} = JSON.parse(data)
if (record.rowMeta.range?.is_readonly) return
dragRecord.value = record
const { newRow, updateProperty } = calculateNewRow(event, isWithoutDates)
@ -756,10 +761,9 @@ const addRecord = (date: dayjs.Dayjs) => {
<div v-if="isUIAllowed('dataEdit')" class="flex justify-between p-1">
<span
:class="{
block: !isDateSelected(day),
hidden: isDateSelected(day),
'block group-hover:hidden': !isDateSelected(day) && [UITypes.DateTime, UITypes.Date].includes(calDataType),
'hidden': isDateSelected(day) && [UITypes.DateTime, UITypes.Date].includes(calDataType),
}"
class="group-hover:hidden"
></span>
<NcDropdown v-if="calendarRange.length > 1" auto-close>
@ -801,7 +805,7 @@ const addRecord = (date: dayjs.Dayjs) => {
</template>
</NcDropdown>
<NcButton
v-else
v-else-if="[UITypes.DateTime, UITypes.Date].includes(calDataType)"
:class="{
'!block': isDateSelected(day),
'!hidden': !isDateSelected(day),
@ -875,7 +879,7 @@ const addRecord = (date: dayjs.Dayjs) => {
:selected="dragRecord?.rowMeta?.id === record.rowMeta.id || resizeRecord?.rowMeta?.id === record.rowMeta.id"
@resize-start="onResizeStart"
>
<template v-if="calDataType === UITypes.DateTime" #time>
<template v-if="[UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime].includes(calDataType)" #time>
<span class="text-xs font-medium text-gray-400">
{{ dayjs(record.row[record.rowMeta.range?.fk_from_col!.title!]).format('h:mma').slice(0, -1) }}
</span>

11
packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue

@ -10,6 +10,7 @@ const {
formattedData,
formattedSideBarData,
calendarRange,
selectedDate,
displayField,
viewMetaProperties,
selectedTime,
@ -605,6 +606,8 @@ const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[],
const onResize = (event: MouseEvent) => {
if (!isUIAllowed('dataEdit') || !container.value || !resizeRecord.value || !scrollContainer.value) return
if (resizeRecord.value.rowMeta.range?.is_readonly) return
const { width, left, top, bottom } = container.value.getBoundingClientRect()
const { scrollHeight } = container.value
@ -693,6 +696,9 @@ const onResizeEnd = () => {
const onResizeStart = (direction: 'right' | 'left', event: MouseEvent, record: Row) => {
if (!isUIAllowed('dataEdit')) return
if (record.rowMeta.range?.is_readonly) return
resizeInProgress.value = true
resizeDirection.value = direction
resizeRecord.value = record
@ -836,6 +842,8 @@ const dragStart = (event: MouseEvent, record: Row) => {
dragTimeout.value = setTimeout(() => {
if (!isUIAllowed('dataEdit')) return
if (record.rowMeta.range?.is_readonly) return
isDragging.value = true
while (!target.classList.contains('draggable-record')) {
target = target.parentElement as HTMLElement
@ -879,6 +887,8 @@ const dropEvent = (event: DragEvent) => {
record: Row
} = JSON.parse(data)
if (record.rowMeta.range?.is_readonly) return
dragRecord.value = record
const { newRow, updatedProperty } = calculateNewRow(event, true)
@ -1009,6 +1019,7 @@ watch(
@dblclick="addRecord(hour)"
@click="
() => {
selectedDate = hour
selectedTime = hour
dragRecord = undefined
}

10
packages/nc-gui/components/smartsheet/calendar/index.vue

@ -147,7 +147,10 @@ reloadViewDataHook?.on(async (params: void | { shouldShowLoading?: boolean }) =>
@new-record="newRecord"
/>
<LazySmartsheetCalendarWeekViewDateTimeField
v-else-if="activeCalendarView === 'week' && calDataType === UITypes.DateTime"
v-else-if="
activeCalendarView === 'week' &&
[UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime, UITypes.Formula].includes(calDataType)
"
@expand-record="expandRecord"
@new-record="newRecord"
/>
@ -157,7 +160,10 @@ reloadViewDataHook?.on(async (params: void | { shouldShowLoading?: boolean }) =>
@new-record="newRecord"
/>
<LazySmartsheetCalendarDayViewDateTimeField
v-else-if="activeCalendarView === 'day' && calDataType === UITypes.DateTime"
v-else-if="
activeCalendarView === 'day' &&
[UITypes.DateTime, UITypes.LastModifiedTime, UITypes.CreatedTime, UITypes.Formula].includes(calDataType)
"
@expand-record="expandRecord"
@new-record="newRecord"
/>

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

@ -186,6 +186,18 @@ function scrollToComment(commentId: string) {
}
}
function scrollToAudit(auditId?: string) {
if (!auditId) return
const auditEl = commentsWrapperEl.value?.querySelector(`.nc-audit-item.${auditId}`)
if (auditEl) {
auditEl.scrollIntoView({
behavior: 'smooth',
block: 'center',
})
}
}
watch(commentsWrapperEl, () => {
setTimeout(() => {
nextTick(() => {
@ -261,6 +273,17 @@ function handleResetHoverEffect() {
hoveredCommentId.value = null
}
watch(
() => audits.value.length,
(auditCount) => {
nextTick(() => {
setTimeout(() => {
scrollToAudit(audits.value[auditCount - 1]?.id)
}, 100)
})
},
)
</script>
<template>
@ -530,7 +553,7 @@ function handleResetHoverEffect() {
</div>
</template>
<div v-for="audit of audits" :key="audit.id" class="nc-audit-item">
<div v-for="audit of audits" :key="audit.id" :class="`${audit.id}`" class="nc-audit-item">
<div class="group gap-3 overflow-hidden px-3 py-2 hover:bg-gray-100">
<div class="flex items-start justify-between">
<div class="flex items-start gap-3 flex-1 w-full">

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

@ -315,7 +315,9 @@ const reloadHook = createEventHook()
reloadHook.on(() => {
reloadParentRowHook?.trigger({ shouldShowLoading: false })
if (isNew.value) return
_loadRow(null, true)
_loadRow(undefined, true)
loadAudits(rowId.value, false)
})
provide(ReloadRowDataHookInj, reloadHook)

13
packages/nc-gui/components/smartsheet/grid/Aggregation.vue

@ -0,0 +1,13 @@
<script setup lang="ts">
defineProps<{
group: any
maxDepth: any
scrollLeft: number
}>()
</script>
<template>
<div></div>
</template>
<style scoped lang="scss"></style>

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

@ -1,10 +1,11 @@
<script lang="ts" setup>
import tinycolor from 'tinycolor2'
import { UITypes, dateFormats, parseStringDateTime, timeFormats } from 'nocodb-sdk'
import { CommonAggregations, UITypes, dateFormats, parseStringDateTime, timeFormats } from 'nocodb-sdk'
import Table from './Table.vue'
import GroupBy from './GroupBy.vue'
import GroupByTable from './GroupByTable.vue'
import GroupByLabel from './GroupByLabel.vue'
import type { Group } from '~/lib/types'
const props = defineProps<{
group: Group
@ -13,7 +14,13 @@ const props = defineProps<{
loadGroupData: (group: Group, force?: boolean, params?: any) => Promise<void>
loadGroupPage: (group: Group, p: number) => Promise<void>
groupWrapperChangePage: (page: number, groupWrapper?: Group) => Promise<void>
loadGroupAggregation: (
group: Group,
fields?: Array<{
field: string
type: string
}>,
) => Promise<void>
redistributeRows?: (group?: Group) => void
viewWidth?: number
@ -33,12 +40,16 @@ const vGroup = useVModel(props, 'group', emits)
const meta = inject(MetaInj, ref())
const fields = inject(FieldsInj, ref())
const scrollLeft = toRef(props, 'scrollLeft')
const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
const { gridViewCols } = useViewColumnsOrThrow()
const reloadAggregate = inject(ReloadAggregateHookInj, createEventHook())
const displayField = computed(() => {
return meta.value?.columns?.find((c) => c.pv)
})
@ -53,6 +64,32 @@ const viewDisplayField = computed(() => {
const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
reloadAggregate?.on(async (_fields) => {
if (!fields.value?.length) return
if (!_fields || !_fields?.fields.length) {
await props.loadGroupAggregation(vGroup.value)
}
if (_fields?.fields) {
const fieldAggregateMapping = _fields.fields.reduce((acc, field) => {
const f = fields.value.find((f) => f.title === field.title)
if (!f?.id) return acc
acc[f.id] = field.aggregation ?? gridViewCols.value[f.id].aggregation ?? CommonAggregations.None
return acc
}, {} as Record<string, string>)
await props.loadGroupAggregation(
vGroup.value,
Object.entries(fieldAggregateMapping).map(([field, type]) => ({
field,
type,
})),
)
}
})
const _loadGroupData = async (group: Group, force?: boolean, params?: any) => {
isViewDataLoading.value = true
isPaginationLoading.value = true
@ -109,10 +146,12 @@ const findAndLoadSubGroup = (key: any) => {
const reloadViewDataHandler = (params: void | { shouldShowLoading?: boolean | undefined; offset?: number | undefined }) => {
if (vGroup.value.nested) {
props.loadGroups({ ...(params?.offset !== undefined ? { offset: params.offset } : {}) }, vGroup.value)
props.loadGroupAggregation(vGroup.value)
} else {
_loadGroupData(vGroup.value, true, {
...(params?.offset !== undefined ? { offset: params.offset } : {}),
})
props.loadGroupAggregation(vGroup.value)
}
}
@ -219,6 +258,7 @@ const shouldRenderCell = (column) =>
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LongText,
UITypes.LastModifiedBy,
].includes(column?.uidt)
@ -263,11 +303,10 @@ const computedWidth = computed(() => {
const getSubGroupWidth = (depth: number) => {
switch (depth) {
case 3:
return `${baseValue - 26}px`
return `${baseValue - 18}px`
case 2:
return `${baseValue - 17}px`
return `${baseValue - 9}px`
case 1:
return `${baseValue - 8}px`
default:
return `${baseValue}px`
}
@ -276,7 +315,7 @@ const computedWidth = computed(() => {
if (_depth === 0) {
if (tempScrollLeft < 29) {
// The equation is calculated on trial and error basis
return `${baseValue + tempScrollLeft - (53 / 29) * tempScrollLeft}px`
return `${baseValue + tempScrollLeft - (0.02 * (tempScrollLeft * tempScrollLeft) + 1.07 * tempScrollLeft)}px`
}
return getSubGroupWidth(maxDepth)
}
@ -284,15 +323,15 @@ const computedWidth = computed(() => {
if (_depth === 1) {
if (tempScrollLeft < 30) {
// The equation is calculated on trial and error basis
return `${baseValue + tempScrollLeft - 9 - (23 / 15) * tempScrollLeft}px`
return `${baseValue + tempScrollLeft - 9 - (23 / 20) * tempScrollLeft}px`
}
return getSubGroupWidth(maxDepth)
}
if (_depth === 2) {
if (tempScrollLeft < 15) {
if (tempScrollLeft <= 14) {
// The equation is calculated on trial and error basis
return `${baseValue + tempScrollLeft - 18 - (19 / 15) * tempScrollLeft}px`
return `${baseValue + tempScrollLeft - 18 - tempScrollLeft}px`
}
return getSubGroupWidth(maxDepth)
}
@ -368,21 +407,26 @@ const bgColor = computed(() => {
<template #header>
<div
:class="{
'!rounded-b-none': activeGroups.includes(grp.key),
'border-b-1': _depth === (maxDepth ?? 1) - 1 && activeGroups.includes(grp.key),
'!rounded-b-none': activeGroups.includes(grp.key.toString()),
'!border-b-1': _depth === (maxDepth ?? 1) - 1 && activeGroups.includes(grp.key.toString()),
}"
class="flex !sticky w-full items-center rounded-b-lg group select-none transition-all !rounded-t-[8px] !h-10"
class="flex !sticky w-full items-center rounded-b-lg select-none transition-all !rounded-t-[8px] !h-10"
>
<div
:style="`width:${computedWidth};`"
class="!sticky flex justify-between !h-10 border-r-1 pr-2 border-gray-300 overflow-clip items-center !left-2"
:class="{
'!rounded-bl-[8px]': !activeGroups.includes(grp.key.toString()),
}"
:style="`width:${computedWidth};background: ${bgColor};`"
class="!sticky flex z-10 justify-between !h-9.8 border-r-1 !rounded-tl-[8px] group pr-2 border-gray-300 overflow-clip items-center !left-0"
>
<div class="flex items-center">
<NcButton class="!border-0 !shadow-none !bg-transparent !hover:bg-transparent" type="secondary" size="small">
<GeneralIcon
icon="chevronDown"
class="transition-all"
:style="`${activeGroups.includes(grp.key) ? 'transform: rotate(360deg)' : 'transform: rotate(270deg)'}`"
:style="`${
activeGroups.includes(grp.key.toString()) ? 'transform: rotate(360deg)' : 'transform: rotate(270deg)'
}`"
/>
</NcButton>
@ -448,7 +492,10 @@ const bgColor = computed(() => {
</a-tag>
</div>
</div>
<div class="flex items-center">
<div
:style="`background: linear-gradient(to right, hsla(0, 0%, 97%, 0), ${bgColor} 18%);`"
class="flex !h-10 absolute right-0 pl-8 pr-2 items-center"
>
<div class="text-xs group-hover:hidden text-gray-500 nc-group-row-count">
<span>
{{ $t('datatype.Count') }}
@ -463,7 +510,7 @@ const bgColor = computed(() => {
<template #overlay>
<NcMenu>
<NcMenuItem v-if="activeGroups.includes(grp.key)" @click="collapseGroup(grp.key)">
<NcMenuItem v-if="activeGroups.includes(grp.key.toString())" @click="collapseGroup(grp.key)">
<GeneralIcon icon="minimize" />
Collapse group
</NcMenuItem>
@ -484,6 +531,12 @@ const bgColor = computed(() => {
</NcDropdown>
</div>
</div>
<SmartsheetGridAggregation
:scroll-left="props.scrollLeft || _scrollLeft"
:max-depth="maxDepth"
:group="grp"
:depth="_depth"
/>
</div>
</template>
<GroupByTable
@ -497,13 +550,11 @@ const bgColor = computed(() => {
:group-wrapper-change-page="groupWrapperChangePage"
:row-height="rowHeight"
:redistribute-rows="redistributeRows"
:expand-form="expandForm"
: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
@ -513,8 +564,8 @@ const bgColor = computed(() => {
:load-group-page="loadGroupPage"
:group-wrapper-change-page="groupWrapperChangePage"
:row-height="rowHeight"
:load-group-aggregation="loadGroupAggregation"
:redistribute-rows="redistributeRows"
:expand-form="expandForm"
:view-width="viewWidth"
:depth="_depth + 1"
:max-depth="maxDepth"
@ -526,27 +577,28 @@ const bgColor = computed(() => {
</div>
</div>
</div>
<LazySmartsheetPagination
<LazySmartsheetGridPaginationV2
v-if="vGroup.root"
v-model:pagination-data="vGroup.paginationData"
align-count-on-right
:scroll-left="_scrollLeft"
custom-label="groups"
show-api-timing
:depth="maxDepth"
:change-page="(p: number) => groupWrapperChangePage(p, vGroup)"
:style="`${props.depth && props.depth > 0 ? 'border-radius: 0 0 8px 8px !important;' : ''}`"
></LazySmartsheetPagination>
/>
<LazySmartsheetPagination
v-else
v-model:pagination-data="vGroup.paginationData"
align-count-on-right
custom-label="groups"
align-left
show-api-timing
:change-page="(p: number) => groupWrapperChangePage(p, vGroup)"
:hide-sidebars="true"
:style="`${
props.depth && props.depth > 0
? 'border-radius: 0 0 8px 8px !important; background: transparent; border-top: 0px; height: 24px'
? 'border-radius: 0 0 8px 8px !important; background: transparent; border-top: 0px; height: 24px; padding-bottom: 8px;'
: ''
}`"
:fixed-size="undefined"

183
packages/nc-gui/components/smartsheet/grid/GroupByTable.vue

@ -1,6 +1,7 @@
<script lang="ts" setup>
import { UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import { type ColumnType, UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import Table from './Table.vue'
import { NavigateDir } from '~/lib/enums'
const props = defineProps<{
group: Group
@ -16,7 +17,6 @@ const props = defineProps<{
maxDepth?: number
rowHeight?: number
expandForm?: (row: Row, state?: Record<string, any>, fromToolbar?: boolean) => void
paginationFixedSize?: number
paginationHideSidebars?: boolean
@ -30,12 +30,69 @@ const emits = defineEmits(['update:paginationData'])
const vGroup = useVModel(props, 'group', emits)
const { t } = useI18n()
const router = useRouter()
const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
const isPublic = inject(IsPublicInj, ref(false))
const skipRowRemovalOnCancel = ref(false)
const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const { eventBus } = useSmartsheetStoreOrThrow()
const routeQuery = computed(() => route.value.query as Record<string, string>)
const route = router.currentRoute
const expandedFormDlg = ref(false)
const expandedFormRow = ref<Row>()
const expandedFormRowState = ref<Record<string, any>>()
const expandedFormOnRowIdDlg = computed({
get() {
return !!routeQuery.value.rowId
},
set(val) {
if (!val) {
router.push({
query: {
...routeQuery.value,
rowId: undefined,
},
})
expandedFormRow.value = {}
expandedFormRowState.value = {}
}
},
})
function expandForm(row: Row, state?: Record<string, any>, fromToolbar = false) {
const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
expandedFormRowState.value = state
if (rowId && !isPublic.value) {
router.push({
query: {
...routeQuery.value,
rowId,
},
})
} else {
expandedFormRow.value = row
expandedFormDlg.value = true
skipRowRemovalOnCancel.value = !fromToolbar
}
}
const addRowExpandOnClose = (row: Row) => {
eventBus.emit(SmartsheetStoreEvents.CLEAR_NEW_ROW, row)
}
function addEmptyRow(group: Group, addAfter?: number, metaValue = meta.value) {
if (group.nested || !group.rows) return
@ -126,6 +183,91 @@ const pagination = computed(() => {
}
})
// get current expanded row index
function getExpandedRowIndex() {
return formattedData.value.findIndex(
(row: Row) => routeQuery.value.rowId === extractPkFromRow(row.row, meta.value?.columns as ColumnType[]),
)
}
const navigateToSiblingRow = async (dir: NavigateDir) => {
// debugger
const expandedRowIndex = getExpandedRowIndex()
// calculate next row index based on direction
let siblingRowIndex = expandedRowIndex + (dir === NavigateDir.NEXT ? 1 : -1)
// if unsaved row skip it
while (formattedData.value[siblingRowIndex]?.rowMeta?.new) {
siblingRowIndex = siblingRowIndex + (dir === NavigateDir.NEXT ? 1 : -1)
}
const currentPage = vGroup.value.paginationData?.page || 1
// if next row index is less than 0, go to previous page and point to last element
if (siblingRowIndex < 0) {
// if first page, do nothing
if (currentPage === 1) return message.info(t('msg.info.noMoreRecords'))
await props.loadGroupPage(vGroup.value, currentPage - 1)
siblingRowIndex = formattedData.value.length - 1
// if next row index is greater than total rows in current view
// then load next page of formattedData and set next row index to 0
} else if (siblingRowIndex >= formattedData.value.length) {
if (vGroup.value.paginationData?.isLastPage) return message.info(t('msg.info.noMoreRecords'))
await props.loadGroupPage(vGroup.value, currentPage + 1)
siblingRowIndex = 0
}
// extract the row id of the sibling row
const rowId = extractPkFromRow(formattedData.value[siblingRowIndex].row, meta.value?.columns as ColumnType[])
if (rowId) {
await router.push({
query: {
...routeQuery.value,
rowId,
},
})
}
}
const goToNextRow = async () => {
const currentIndex = getExpandedRowIndex()
/* when last index of current page is reached we should move to next page */
if (!vGroup.value.paginationData?.isLastPage && currentIndex === vGroup.value.paginationData?.pageSize) {
const nextPage = vGroup.value.paginationData?.page ? vGroup.value.paginationData?.page + 1 : 1
await props.loadGroupPage(vGroup.value, nextPage)
}
navigateToSiblingRow(NavigateDir.NEXT)
}
const goToPreviousRow = async () => {
const currentIndex = getExpandedRowIndex()
/* when first index of current page is reached and then clicked back
previos page should be loaded
*/
if (!vGroup.value.paginationData?.isFirstPage && currentIndex === 1) {
const nextPage = vGroup.value.paginationData?.page ? vGroup.value.paginationData?.page - 1 : 1
await props.loadGroupPage(vGroup.value, nextPage)
}
navigateToSiblingRow(NavigateDir.PREV)
}
const isLastRow = computed(() => {
const currentIndex = getExpandedRowIndex()
return vGroup.value.paginationData?.isLastPage && currentIndex === formattedData.value.length - 1
})
const isFirstRow = computed(() => {
const currentIndex = getExpandedRowIndex()
return vGroup.value.paginationData?.isFirstPage && currentIndex === 0
})
async function deleteSelectedRowsWrapper() {
if (!deleteSelectedRows) return
@ -133,6 +275,12 @@ async function deleteSelectedRowsWrapper() {
// reload table data
await reloadTableData({ shouldShowLoading: true })
}
eventBus.on((event) => {
if (event === SmartsheetStoreEvents.GROUP_BY_RELOAD || event === SmartsheetStoreEvents.DATA_RELOAD) {
reloadViewDataHook?.trigger()
}
})
</script>
<template>
@ -145,7 +293,7 @@ async function deleteSelectedRowsWrapper() {
:load-data="async () => {}"
:change-page="(p: number) => props.loadGroupPage(vGroup, p)"
:call-add-empty-row="(addAfter?: number) => addEmptyRow(vGroup, addAfter)"
:expand-form="props.expandForm"
:expand-form="expandForm"
:row-height="rowHeight"
:delete-row="deleteRow"
:delete-selected-rows="deleteSelectedRowsWrapper"
@ -158,6 +306,35 @@ async function deleteSelectedRowsWrapper() {
:disable-skeleton="true"
:disable-virtual-y="true"
/>
<Suspense>
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
:load-row="!isPublic"
:row="expandedFormRow"
:state="expandedFormRowState"
:meta="meta"
:view="view"
@update:model-value="addRowExpandOnClose(expandedFormRow)"
/>
</Suspense>
<SmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg && meta?.id"
v-model="expandedFormOnRowIdDlg"
:row="expandedFormRow ?? { row: {}, oldRow: {}, rowMeta: {} }"
:meta="meta"
:load-row="!isPublic"
:state="expandedFormRowState"
:row-id="routeQuery.rowId"
:view="view"
show-next-prev-icons
:first-row="isFirstRow"
:last-row="isLastRow"
:expand-form="expandForm"
@next="goToNextRow"
@prev="goToPreviousRow"
/>
</template>
<style scoped lang="scss"></style>

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

@ -6,6 +6,9 @@ const props = defineProps<{
scrollLeft?: number
paginationData: PaginatedType
changePage: (page: number) => void
showSizeChanger?: boolean
customLabel?: string
depth?: number
}>()
const emits = defineEmits(['update:paginationData'])
@ -14,7 +17,9 @@ const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
const isLocked = inject(IsLockedInj, ref(false))
const { changePage } = props
const { changePage, customLabel } = props
const showSizeChanger = toRef(props, 'showSizeChanger')
const vPaginationData = useVModel(props, 'paginationData', emits)
@ -81,6 +86,22 @@ const size = computed({
},
})
const getAddnlMargin = (depth: number, ignoreCondition = false) => {
if (!ignoreCondition ? (scrollLeft.value ?? 0) < 30 : true) {
switch (depth) {
case 3:
return 26
case 2:
return 17
case 1:
return 8
default:
return 0
}
}
return 0
}
const renderAltOrOptlKey = () => {
return isMac() ? '⌥' : 'ALT'
}
@ -107,6 +128,7 @@ const renderAltOrOptlKey = () => {
'min-width': displayFieldComputed?.width,
'max-width': displayFieldComputed?.width,
'width': displayFieldComputed?.width,
'margin-left': `${getAddnlMargin(depth ?? 0)}px`,
}"
>
<div class="flex relative justify-between gap-2 w-full">
@ -114,13 +136,15 @@ const renderAltOrOptlKey = () => {
<a-skeleton :active="true" :title="true" :paragraph="false" class="w-16 max-w-16" />
</div>
<NcTooltip v-else class="flex sticky items-center h-full">
<template #title> {{ count }} {{ count !== 1 ? $t('objects.records') : $t('objects.record') }} </template>
<template #title>
{{ count }} {{ customLabel ? customLabel : count !== 1 ? $t('objects.records') : $t('objects.record') }}
</template>
<span
data-testid="grid-pagination"
class="text-gray-500 text-ellipsis overflow-hidden pl-1 truncate nc-grid-row-count caption text-xs text-nowrap"
>
{{ Intl.NumberFormat('en', { notation: 'compact' }).format(count) }}
{{ count !== 1 ? $t('objects.records') : $t('objects.record') }}
{{ customLabel ? customLabel : count !== 1 ? $t('objects.records') : $t('objects.record') }}
</span>
</NcTooltip>
@ -198,6 +222,13 @@ const renderAltOrOptlKey = () => {
</div>
<template v-for="({ field, width, column, value }, index) in visibleFieldsComputed" :key="index">
<div
v-if="index === 0 && scrollLeft > 30"
:style="`width: ${getAddnlMargin(depth ?? 0, true)}px;min-width: ${getAddnlMargin(
depth ?? 0,
true,
)}px;max-width: ${getAddnlMargin(depth ?? 0, true)}px`"
></div>
<NcDropdown
v-if="field && column?.id"
:disabled="[UITypes.SpecificDBType, UITypes.ForeignKey].includes(column?.uidt!) || isLocked"
@ -278,6 +309,7 @@ const renderAltOrOptlKey = () => {
v-if="count !== Infinity"
v-model:current="page"
v-model:page-size="size"
:show-size-changer="showSizeChanger"
class="xs:(mr-2)"
:total="+count"
entity-name="grid"
@ -285,7 +317,6 @@ const renderAltOrOptlKey = () => {
:next-page-tooltip="`${renderAltOrOptlKey()}+→`"
:first-page-tooltip="`${renderAltOrOptlKey()}+↓`"
:last-page-tooltip="`${renderAltOrOptlKey()}+↑`"
:show-size-changer="true"
/>
</div>
</div>

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

@ -1525,7 +1525,11 @@ watch(
}
isViewDataLoading.value = true
try {
await Promise.allSettled([loadData?.(), loadViewAggregate()])
if (isGroupBy.value) {
await loadData?.()
} else {
await Promise.allSettled([loadData?.(), loadViewAggregate()])
}
calculateSlices()
} catch (e) {
if (!axios.isCancel(e)) {
@ -2457,6 +2461,7 @@ onKeyStroke('ArrowDown', onDown)
v-else-if="paginationDataRef"
v-model:pagination-data="paginationDataRef"
:change-page="changePage"
:show-size-changer="!isGroupBy"
:scroll-left="scrollLeft"
/>
</div>

21
packages/nc-gui/components/smartsheet/grid/index.vue

@ -146,8 +146,17 @@ const toggleOptimisedQuery = () => {
}
}
const { rootGroup, groupBy, isGroupBy, loadGroups, loadGroupData, loadGroupPage, groupWrapperChangePage, redistributeRows } =
useViewGroupByOrThrow()
const {
rootGroup,
groupBy,
isGroupBy,
loadGroups,
loadGroupData,
loadGroupPage,
groupWrapperChangePage,
redistributeRows,
loadGroupAggregation,
} = useViewGroupByOrThrow()
const sidebarStore = useSidebarStore()
@ -251,8 +260,8 @@ onMounted(() => {
:delete-selected-rows="deleteSelectedRows"
:delete-range-of-rows="deleteRangeOfRows"
:bulk-update-rows="bulkUpdateRows"
:remove-row-if-new="removeRowIfNew"
:expand-form="expandForm"
:remove-row-if-new="removeRowIfNew"
:row-height="rowHeight"
@toggle-optimised-query="toggleOptimisedQuery"
@bulk-update-dlg="bulkUpdateDlg = true"
@ -266,12 +275,12 @@ onMounted(() => {
:load-group-page="loadGroupPage"
:group-wrapper-change-page="groupWrapperChangePage"
:row-height="rowHeight"
:load-group-aggregation="loadGroupAggregation"
:max-depth="groupBy.length"
:redistribute-rows="redistributeRows"
:expand-form="expandForm"
:view-width="viewWidth"
/>
<Suspense>
<Suspense v-if="!isGroupBy">
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
@ -284,7 +293,7 @@ onMounted(() => {
/>
</Suspense>
<SmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg && meta?.id"
v-if="expandedFormOnRowIdDlg && meta?.id && !isGroupBy"
v-model="expandedFormOnRowIdDlg"
:row="expandedFormRow ?? { row: {}, oldRow: {}, rowMeta: {} }"
:meta="meta"

39
packages/nc-gui/components/smartsheet/toolbar/Calendar/Range.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { type CalendarRangeType, UITypes, isSystemColumn } from 'nocodb-sdk'
import { type CalendarRangeType, FormulaDataTypes, UITypes } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
const meta = inject(MetaInj, ref())
@ -30,6 +30,32 @@ const hideWeekends = computed({
},
})
const dateFieldOptions = computed<SelectProps['options']>(() => {
return (
meta.value?.columns
?.filter(
(c) =>
[UITypes.DateTime, UITypes.Date, UITypes.CreatedTime, UITypes.LastModifiedTime].includes(c.uidt) ||
(c.uidt === UITypes.Formula && (c.colOptions as any)?.parsed_tree?.dataType === FormulaDataTypes.DATE),
)
.map((c) => ({
label: c.title,
value: c.id,
uidt: c.uidt,
})) ?? []
).sort((a, b) => {
const priority = {
[UITypes.DateTime]: 1,
[UITypes.Date]: 2,
[UITypes.Formula]: 3,
[UITypes.CreatedTime]: 4,
[UITypes.LastModifiedTime]: 5,
}
return (priority[a.uidt] || 6) - (priority[b.uidt] || 6)
})
})
watch(
() => activeView.value?.id,
async (newVal, oldVal) => {
@ -103,17 +129,6 @@ const saveCalendarRanges = async () => {
}
}
const dateFieldOptions = computed<SelectProps['options']>(() => {
return (
meta.value?.columns
?.filter((c) => c.uidt === UITypes.Date || (c.uidt === UITypes.DateTime && !isSystemColumn(c)))
.map((c) => ({
label: c.title,
value: c.id,
uidt: c.uidt,
})) ?? []
)
})
/*
const removeRange = async (id: number) => {
_calendar_ranges.value = _calendar_ranges.value.filter((_, i) => i !== id)

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

@ -24,7 +24,7 @@ const localValue = computed({
set: (val) => emit('update:modelValue', val),
})
const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const { showSystemFields, metaColumnById, fieldsMap } = useViewColumnsOrThrow()
const options = computed<SelectProps['options']>(() =>
(
@ -65,10 +65,11 @@ const options = computed<SelectProps['options']>(() =>
}
})
)
// sort and keep system columns at the end
// sort by view column order and keep system columns at the end
?.sort((field1, field2) => {
let orderVal1 = 0
let orderVal2 = 0
let sortByOrder = 0
if (isSystemColumn(field1)) {
orderVal1 = 1
@ -77,7 +78,16 @@ const options = computed<SelectProps['options']>(() =>
orderVal2 = 1
}
return orderVal1 - orderVal2
if (
field1?.id &&
field2?.id &&
fieldsMap.value[field1.id]?.order !== undefined &&
fieldsMap.value[field2.id]?.order !== undefined
) {
sortByOrder = fieldsMap.value[field1.id].order - fieldsMap.value[field2.id].order
}
return orderVal1 - orderVal2 || sortByOrder
})
?.map((c: ColumnType) => ({
value: c.id,

35
packages/nc-gui/components/smartsheet/toolbar/FieldListWithSearch.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk'
import { type ColumnType, isSystemColumn } from 'nocodb-sdk'
const props = defineProps<{
// As we need to focus search box when the parent is opened
@ -13,10 +13,41 @@ const props = defineProps<{
const emits = defineEmits<{ selected: [ColumnType] }>()
const { isParentOpen, toolbarMenu, searchInputPlaceholder, selectedOptionId, options, showSelectedOption } = toRefs(props)
const { isParentOpen, toolbarMenu, searchInputPlaceholder, selectedOptionId, showSelectedOption } = toRefs(props)
const { fieldsMap } = useViewColumnsOrThrow()
const searchQuery = ref('')
const options = computed(() =>
(props.options || [])
.map((c) => c)
.sort((field1, field2) => {
// sort by view column order and keep system columns at the end
let orderVal1 = 0
let orderVal2 = 0
let sortByOrder = 0
if (isSystemColumn(field1)) {
orderVal1 = 1
}
if (isSystemColumn(field2)) {
orderVal2 = 1
}
if (
field1?.id &&
field2?.id &&
fieldsMap.value[field1.id]?.order !== undefined &&
fieldsMap.value[field2.id]?.order !== undefined
) {
sortByOrder = fieldsMap.value[field1.id].order - fieldsMap.value[field2.id].order
}
return orderVal1 - orderVal2 || sortByOrder
}),
)
const filteredOptions = computed(
() => options.value?.filter((c: ColumnType) => c.title?.toLowerCase().includes(searchQuery.value.toLowerCase())) ?? [],
)

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

@ -13,10 +13,16 @@ const { fieldsToGroupBy, groupByLimit } = useViewGroupByOrThrow()
const { $e } = useNuxtApp()
const _groupBy = ref<{ fk_column_id?: string; sort: string; order: number }[]>([])
interface Group {
fk_column_id?: string
sort: string
order: number
}
const _groupBy = ref<Group[]>([])
const groupBy = computed<{ fk_column_id?: string; sort: string; order: number }[]>(() => {
const tempGroupBy: { fk_column_id?: string; sort: string; order: number }[] = []
const groupBy = computed<Group[]>(() => {
const tempGroupBy: Group[] = []
Object.values(gridViewCols.value).forEach((col) => {
if (col.group_by) {
tempGroupBy.push({
@ -124,11 +130,13 @@ const addFieldToGroupBy = (column: ColumnType) => {
showCreateGroupBy.value = false
}
const removeFieldFromGroupBy = async (index: string | number) => {
const removeFieldFromGroupBy = async (group: Group) => {
if (groupedByColumnIds.value.length === 0) {
open.value = false
return
}
const index = _groupBy.value.findIndex((g) => g.fk_column_id === group.fk_column_id)
_groupBy.value.splice(+index, 1)
await saveGroupBy()
}
@ -210,7 +218,7 @@ const onMove = async (event: { moved: { newIndex: number; oldIndex: number } })
/>
<div
v-else
class="flex flex-col bg-white overflow-auto nc-group-by-list menu-filter-dropdown w-100 p-6"
class="flex flex-col bg-white overflow-auto nc-group-by-list menu-filter-dropdown w-100 p-4"
data-testid="nc-group-by-menu"
>
<div class="max-h-100" @click.stop>
@ -280,7 +288,7 @@ const onMove = async (event: { moved: { newIndex: number; oldIndex: number } })
class="nc-group-by-item-remove-btn !border-l-transparent !rounded-l-none min-w-40"
size="small"
type="secondary"
@click.stop="removeFieldFromGroupBy(i)"
@click.stop="removeFieldFromGroupBy(group)"
>
<component :is="iconMap.deleteListItem" />
</NcButton>
@ -297,17 +305,15 @@ const onMove = async (event: { moved: { newIndex: number; oldIndex: number } })
>
<NcButton
v-e="['c:group-by:add']"
class="nc-add-group-by-btn mt-5"
style="width: fit-content"
size="small"
type="text"
size="small"
style="width: fit-content"
class="nc-add-group-by-btn mt-2 !text-brand-500"
@click.stop="showCreateGroupBy = true"
>
<div class="flex gap-1 items-center">
<div class="flex">
{{ $t('activity.addSubGroup') }}
</div>
<GeneralIcon icon="plus" />
{{ $t('activity.addSubGroup') }}
</div>
</NcButton>
<template #overlay>

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

@ -41,7 +41,7 @@ const views = computed(() => viewsByTable.value.get(table.value.id!))
const isViewIdCopied = ref(false)
const currentBaseId = computed(() => table.value?.source_id)
const currentSourceId = computed(() => table.value?.source_id)
const onRenameMenuClick = () => {
emits('rename')
@ -316,14 +316,15 @@ const onDelete = async () => {
}}
</NcMenuItem>
</template>
<template v-if="currentBaseId">
<template v-if="table?.base_id && currentSourceId">
<LazyDlgQuickImport
v-for="tp in quickImportDialogTypes"
:key="tp"
v-model="quickImportDialogs[tp].value"
:import-data-only="true"
:import-type="tp"
:source-id="currentBaseId"
:base-id="table.base_id"
:source-id="currentSourceId"
/>
</template>
</NcMenu>

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

@ -43,7 +43,9 @@ useBase()
const meta = inject(MetaInj, ref())
const currentBaseId = computed(() => meta.value?.source_id)
const currentBaseId = computed(() => meta.value?.base_id)
const currentSourceId = computed(() => meta.value?.source_id)
/*
const Icon = computed(() => {
@ -172,13 +174,14 @@ useMenuCloseOnEsc(open)
</template>
</a-dropdown>
<template v-if="currentBaseId">
<template v-if="currentSourceId && currentBaseId">
<LazyDlgQuickImport
v-for="tp in quickImportDialogTypes"
:key="tp"
v-model="quickImportDialogs[tp].value"
:import-type="tp"
:source-id="currentBaseId"
:base-id="currentBaseId"
:source-id="currentSourceId"
:import-data-only="true"
/>
</template>

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

@ -1,20 +1,19 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, getDateFormat, getDateTimeFormat, isSystemColumn, isVirtualCol, parseStringDate } from 'nocodb-sdk'
import type { ColumnType, OracleUi, TableType } from 'nocodb-sdk'
import {
SqlUiFactory,
UITypes,
getDateFormat,
getDateTimeFormat,
isSystemColumn,
isVirtualCol,
parseStringDate,
} from 'nocodb-sdk'
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'
import { srcDestMappingColumns, tableColumns } from './utils'
const { quickImportType, baseTemplate, importData, importColumns, importDataOnly, maxRowsToParse, sourceId, importWorker } =
defineProps<Props>()
const emit = defineEmits(['import', 'error', 'change'])
dayjs.extend(utc)
const { t } = useI18n()
interface Props {
quickImportType: 'csv' | 'excel' | 'json'
baseTemplate: Record<string, any>
@ -22,6 +21,7 @@ interface Props {
importColumns: any[]
importDataOnly: boolean
maxRowsToParse: number
baseId: string
sourceId: string
importWorker: Worker
}
@ -31,6 +31,24 @@ interface Option {
value: string
}
const {
quickImportType,
baseTemplate,
importData,
importColumns,
importDataOnly,
maxRowsToParse,
baseId,
sourceId,
importWorker,
} = defineProps<Props>()
const emit = defineEmits(['import', 'error', 'change'])
dayjs.extend(utc)
const { t } = useI18n()
const meta = inject(MetaInj, ref())
const columns = computed(() => meta.value?.columns || [])
@ -43,13 +61,33 @@ const { $api } = useNuxtApp()
const { addTab } = useTabs()
const baseStrore = useBase()
const { loadTables } = baseStrore
const { sqlUis, base } = storeToRefs(baseStrore)
const { openTable } = useTablesStore()
const { baseTables } = storeToRefs(useTablesStore())
const basesStore = useBases()
const { bases } = storeToRefs(basesStore)
const { base: activeBase } = storeToRefs(useBase())
const base = computed(() => bases.value.get(baseId) || activeBase.value)
const tablesStore = useTablesStore()
const { openTable, loadProjectTables } = tablesStore
const { baseTables } = storeToRefs(tablesStore)
const sqlUi = ref(sqlUis.value[sourceId] || Object.values(sqlUis.value)[0])
const sqlUis = computed(() => {
const temp: Record<string, any> = {}
for (const source of base.value.sources ?? []) {
if (source.id) {
temp[source.id] = SqlUiFactory.create({ client: source.type }) as Exclude<
ReturnType<(typeof SqlUiFactory)['create']>,
typeof OracleUi
>
}
}
return temp
})
const sqlUi = computed(() => sqlUis.value[sourceId] || Object.values(sqlUis.value)[0])
const hasSelectColumn = ref<boolean[]>([])
@ -63,7 +101,7 @@ const isImporting = ref(false)
const importingTips = ref<Record<string, string>>({})
const checkAllRecord = ref<boolean[]>([])
const checkAllRecord = ref<Record<string, boolean>>({})
const formError = ref()
@ -130,14 +168,20 @@ watch(
let res = true
if (importDataOnly) {
for (const tn of Object.keys(srcDestMapping.value)) {
let flag = false
if (!atLeastOneEnabledValidation(tn)) {
res = false
}
for (const record of srcDestMapping.value[tn]) {
if (!fieldsValidation(record, tn)) {
return false
res = false
flag = true
break
}
}
if (flag) {
break
}
}
} else {
for (const [_, o] of Object.entries(validateInfos)) {
@ -220,20 +264,13 @@ function deleteTable(tableIdx: number) {
function deleteTableColumn(tableIdx: number, columnKey: number) {
const columnIdx = data.tables[tableIdx].columns.findIndex((c: ColumnType & { key: number }) => c.key === columnKey)
data.tables[tableIdx].columns.splice(columnIdx, 1)
}
function addNewColumnRow(tableIdx: number, uidt: string) {
data.tables[tableIdx].columns.push({
key: data.tables[tableIdx].columns.length,
title: `title${data.tables[tableIdx].columns.length + 1}`,
column_name: `title${data.tables[tableIdx].columns.length + 1}`,
uidt,
})
let key = 0
nextTick(() => {
const input = inputRefs.value[data.tables[tableIdx].columns.length - 1]
input.focus()
input.select()
data.tables[tableIdx].columns.forEach((_c: ColumnType & { key: number }, i: number) => {
if (data.tables[tableIdx].columns[i].key !== undefined) {
data.tables[tableIdx].columns[i].key = key
key++
}
})
}
@ -560,7 +597,7 @@ async function importTemplate() {
)
}
// reload table list
await loadTables()
await loadProjectTables(base.value.id, true)
addTab({
...tab,
@ -725,7 +762,7 @@ watch(modelRef, async () => {
</template>
<template #extra>
<a-tooltip bottom>
<NcTooltip bottom class="inline-block">
<template #title>
<span>{{ $t('activity.deleteTable') }}</span>
</template>
@ -735,7 +772,7 @@ watch(modelRef, async () => {
class="text-lg mr-8"
@click.stop="deleteTable(tableIdx)"
/>
</a-tooltip>
</NcTooltip>
</template>
<a-table
@ -763,10 +800,10 @@ watch(modelRef, async () => {
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'source_column'">
<NcTooltip class="truncate"
><template #title>{{ record.srcCn }}</template
>{{ record.srcCn }}</NcTooltip
>
<NcTooltip class="truncate inline-block">
<template #title>{{ record.srcCn }}</template>
{{ record.srcCn }}
</NcTooltip>
</template>
<template v-else-if="column.key === 'destination_column'">
@ -777,10 +814,13 @@ watch(modelRef, async () => {
:filter-option="filterOption"
dropdown-class-name="nc-dropdown-filter-field"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-current" />
</template>
<a-select-option v-for="(col, i) of columns" :key="i" :value="col.title">
<div class="flex items-center">
<component :is="getUIDTIcon(col.uidt)" />
<span class="ml-2">{{ col.title }}</span>
<div class="flex items-center gap-2">
<component :is="getUIDTIcon(col.uidt)" class="w-3.5 h-3.5" />
<span>{{ col.title }}</span>
</div>
</a-select-option>
</a-select>
@ -811,13 +851,13 @@ watch(modelRef, async () => {
<a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx">
<template #header>
<a-form-item v-bind="validateInfos[`tables.${tableIdx}.table_name`]" no-style>
<div class="flex flex-col w-full">
<div class="flex flex-col w-full mr-2">
<a-input
v-model:value="table.table_name"
class="font-weight-bold text-lg"
class="font-weight-bold text-lg !rounded-md"
size="large"
hide-details
:bordered="false"
:bordered="true"
@click.stop
@blur="handleEditableTnChange(tableIdx)"
@keydown.enter="handleEditableTnChange(tableIdx)"
@ -831,17 +871,17 @@ watch(modelRef, async () => {
</template>
<template #extra>
<a-tooltip bottom>
<NcTooltip bottom class="inline-block mr-8">
<template #title>
<span>{{ $t('activity.deleteTable') }}</span>
</template>
<component
:is="iconMap.delete"
:is="iconMap.deleteListItem"
v-if="data.tables.length > 1"
class="text-lg mr-8"
class="text-lg"
@click.stop="deleteTable(tableIdx)"
/>
</a-tooltip>
</NcTooltip>
</template>
<a-table
v-if="table.columns && table.columns.length"
@ -878,117 +918,71 @@ watch(modelRef, async () => {
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'column_name'">
<a-form-item v-bind="validateInfos[`tables.${tableIdx}.columns.${record.key}.${column.key}`]">
<a-input :ref="(el: HTMLInputElement) => (inputRefs[record.key] = el)" v-model:value="record.title" />
<a-input
:ref="(el: HTMLInputElement) => (inputRefs[record.key] = el)"
v-model:value="record.title"
class="!rounded-md"
/>
</a-form-item>
</template>
<template v-else-if="column.key === 'uidt'">
<a-form-item v-bind="validateInfos[`tables.${tableIdx}.columns.${record.key}.${column.key}`]">
<a-select
v-model:value="record.uidt"
class="w-52"
show-search
:filter-option="filterOption"
dropdown-class-name="nc-dropdown-template-uidt"
@change="handleUIDTChange(record, table)"
>
<a-select-option v-for="(option, i) of uiTypeOptions" :key="i" :value="option.value">
<a-tooltip placement="right">
<template v-if="isSelectDisabled(option.label, table.columns[record.key]?._disableSelect)" #title>
{{
$t('msg.tooLargeFieldEntity', {
entity: option.label,
})
}}
</template>
{{ option.label }}
</a-tooltip>
</a-select-option>
</a-select>
</a-form-item>
</template>
<template v-else-if="column.key === 'dtxp'">
<a-form-item v-if="isSelect(record)">
<a-input v-model:value="record.dtxp" />
<NcTooltip :disabled="importDataOnly">
<template #title>
{{ $t('tooltip.useFieldEditMenuToConfigFieldType') }}
</template>
<a-select
v-model:value="record.uidt"
class="w-52"
show-search
:filter-option="filterOption"
dropdown-class-name="nc-dropdown-template-uidt"
:disabled="!importDataOnly"
@change="handleUIDTChange(record, table)"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-current" />
</template>
<a-select-option v-for="(option, i) of uiTypeOptions" :key="i" :value="option.value">
<div class="flex items-center gap-2">
<component :is="getUIDTIcon(UITypes[option.value])" class="h-3.5 w-3.5" />
<NcTooltip placement="right" :disabled="!importDataOnly" show-on-truncate-only>
<template v-if="isSelectDisabled(option.label, table.columns[record.key]?._disableSelect)" #title>
{{
$t('msg.tooLargeFieldEntity', {
entity: option.label,
})
}}
</template>
{{ option.label }}
</NcTooltip>
</div>
</a-select-option>
</a-select>
</NcTooltip>
</a-form-item>
</template>
<template v-if="column.key === 'action'">
<a-tooltip v-if="record.key === 0">
<template #title>
<span>{{ $t('general.primaryValue') }}</span>
</template>
<div class="flex items-center float-right mr-4">
<mdi-key-star class="text-lg" />
</div>
</a-tooltip>
<a-tooltip v-else>
<NcTooltip class="inline-block">
<template #title>
<span>{{ $t('activity.column.delete') }}</span>
</template>
<a-button type="text" @click="deleteTableColumn(tableIdx, record.key)">
<div class="flex items-center">
<component :is="iconMap.delete" class="text-lg" />
</div>
</a-button>
</a-tooltip>
<NcButton
type="text"
size="small"
:disabled="table.columns.length === 1"
@click="deleteTableColumn(tableIdx, record.key)"
>
<component :is="iconMap.deleteListItem" />
</NcButton>
</NcTooltip>
</template>
</template>
</a-table>
<div class="mt-5 flex gap-2 justify-center">
<a-tooltip bottom>
<template #title>
<span>{{ $t('activity.column.addNumber') }}</span>
</template>
<a-button class="group" @click="addNewColumnRow(tableIdx, 'Number')">
<div class="flex items-center">
<component :is="iconMap.number" class="group-hover:!text-accent flex text-lg" />
</div>
</a-button>
</a-tooltip>
<a-tooltip bottom>
<template #title>
<span>{{ $t('activity.column.addSingleLineText') }}</span>
</template>
<a-button class="group" @click="addNewColumnRow(tableIdx, 'SingleLineText')">
<div class="flex items-center">
<component :is="iconMap.text" class="group-hover:!text-accent text-lg" />
</div>
</a-button>
</a-tooltip>
<a-tooltip bottom>
<template #title>
<span>{{ $t('activity.column.addLongText') }}</span>
</template>
<a-button class="group" @click="addNewColumnRow(tableIdx, 'LongText')">
<div class="flex items-center">
<component :is="iconMap.longText" class="group-hover:!text-accent text-lg" />
</div>
</a-button>
</a-tooltip>
<a-tooltip bottom>
<template #title>
<span>{{ $t('activity.column.addOther') }}</span>
</template>
<a-button class="group" @click="addNewColumnRow(tableIdx, 'SingleLineText')">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" class="group-hover:!text-accent text-lg" />
</div>
</a-button>
</a-tooltip>
</div>
</a-collapse-panel>
</a-collapse>
</a-form>
@ -1013,4 +1007,11 @@ watch(modelRef, async () => {
}
}
}
:deep(.ant-collapse-header) {
@apply !items-center;
& > div {
@apply flex;
}
}
</style>

11
packages/nc-gui/components/template/utils.ts

@ -13,14 +13,15 @@ export const tableColumns: (Omit<ColumnGroupType<any>, 'children'> & { dataIndex
key: 'uidt',
width: 250,
},
{
name: 'Select Option',
key: 'dtxp',
},
// {
// name: 'Select Option',
// key: 'dtxp',
// },
{
name: 'Action',
key: 'action',
align: 'right',
align: 'center',
width: 40,
},
]

4
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -87,8 +87,8 @@ watch(value, (next) => {
<template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }">
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex items-center w-full">
<div class="nc-cell-field chips flex items-center flex-1" :class="{ 'max-w-[calc(100%_-_16px)]': !isUnderLookup }">
<div class="nc-cell-field flex items-center w-full">
<div class="chips flex items-center flex-1" :class="{ 'max-w-[calc(100%_-_16px)]': !isUnderLookup }">
<template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip
:item="value"

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

@ -127,7 +127,7 @@ watch(
<template>
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex items-center gap-1 w-full chips-wrapper min-h-4">
<div class="nc-cell-field flex items-center gap-1 w-full chips-wrapper min-h-4">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<VirtualCellComponentsItemChip

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

@ -126,7 +126,7 @@ watch(
<template>
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex items-center gap-1 w-full chips-wrapper min-h-4">
<div class="nc-cell-field flex items-center gap-1 w-full chips-wrapper min-h-4">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<VirtualCellComponentsItemChip

4
packages/nc-gui/components/virtual-cell/OneToOne.vue

@ -84,8 +84,8 @@ watch(
<template>
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex w-full chips-wrapper items-center min-h-4" :class="{ active }">
<div class="nc-cell-field chips flex items-center flex-1 max-w-[calc(100%_-_16px)]">
<div class="nc-cell-field flex w-full chips-wrapper items-center min-h-4" :class="{ active }">
<div class="chips flex items-center flex-1 max-w-[calc(100%_-_16px)]">
<template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip
:item="value"

49
packages/nc-gui/components/virtual-cell/components/ItemChip.vue

@ -27,6 +27,8 @@ const isForm = inject(IsFormInj)!
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false))
const { open } = useExpandedFormDetached()
function openExpandedForm() {
@ -40,6 +42,7 @@ function openExpandedForm() {
meta: relatedTableMeta.value,
rowId,
useMetaFields: true,
loadRow: !isPublic.value,
})
}
}
@ -58,7 +61,7 @@ export default {
:class="{ active, 'border-1 py-1 px-2': isAttachment(column) }"
@click="openExpandedForm"
>
<div class="text-ellipsis overflow-hidden">
<div class="text-ellipsis overflow-hidden pointer-events-none">
<span class="name">
<!-- Render virtual cell -->
<div v-if="isVirtualCol(column)">
@ -112,5 +115,49 @@ export default {
white-space: nowrap;
word-break: keep-all;
}
:deep(.nc-action-icon) {
@apply invisible;
}
:deep(.nc-cell) {
&.nc-cell-longtext {
.long-text-wrapper {
@apply min-h-1;
.nc-readonly-rich-text-wrapper {
@apply !min-h-1;
}
.nc-rich-text {
@apply pl-0;
.tiptap.ProseMirror {
@apply -ml-1 min-h-1;
}
}
}
}
&.nc-cell-checkbox {
@apply children:pl-0;
& > div {
@apply !h-auto;
}
}
&.nc-cell-singleselect .nc-cell-field > div {
@apply flex items-center;
}
&.nc-cell-multiselect .nc-cell-field > div {
@apply h-5;
}
&.nc-cell-email,
&.nc-cell-phonenumber {
@apply flex items-center;
}
&.nc-cell-email,
&.nc-cell-phonenumber,
&.nc-cell-url {
.nc-cell-field-link {
@apply py-0;
}
}
}
}
</style>

16
packages/nc-gui/components/workspace/AuditLogs.vue

@ -476,7 +476,7 @@ useEventListener(tableWrapper, 'scroll', () => {
</div>
</div>
</template>
<div v-if="selectedAudit" class="flex flex-col gap-4">
<div v-if="selectedAudit" class="nc-expanded-audit flex flex-col gap-4">
<div class="bg-gray-50 rounded-lg border-1 border-gray-200">
<div class="flex">
<div class="w-1/2 border-r border-gray-200 flex flex-col gap-2 px-4 py-3">
@ -548,7 +548,9 @@ useEventListener(tableWrapper, 'scroll', () => {
</div>
<div class="flex flex-col gap-2">
<div class="cell-header">{{ $t('labels.description') }}</div>
<div class="text-small leading-[18px] text-gray-600">{{ selectedAudit?.description }}</div>
<div>
<pre class="!text-small !leading-[18px] !text-gray-600 mb-0">{{ selectedAudit?.description }}</pre>
</div>
</div>
</div>
</NcModal>
@ -557,13 +559,9 @@ useEventListener(tableWrapper, 'scroll', () => {
</template>
<style lang="scss" scoped>
.nc-audit-table pre {
display: table;
table-layout: fixed;
width: 100%;
white-space: break-spaces;
font-size: unset;
font-family: unset;
.nc-expanded-audit pre {
font-family: Manrope, 'Inter', 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif;
}
:deep(.nc-menu-item-inner) {

65
packages/nc-gui/composables/useCalendarViewStore.ts

@ -1,5 +1,16 @@
import type { ComputedRef, Ref } from 'vue'
import type { Api, CalendarRangeType, CalendarType, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import {
type Api,
type CalendarRangeType,
type CalendarType,
type ColumnType,
FormulaDataTypes,
type PaginatedType,
type TableType,
type ViewType,
isSystemColumn,
isVirtualCol,
} from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import dayjs from 'dayjs'
@ -108,6 +119,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
fk_from_col: ColumnType
fk_to_col?: ColumnType | null
id: string
is_readonly: boolean
}>
>([])
@ -442,19 +454,46 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const calMeta = typeof res.meta === 'string' ? JSON.parse(res.meta) : res.meta
activeCalendarView.value = calMeta?.active_view
if (!activeCalendarView.value) activeCalendarView.value = 'month'
calendarRange.value = res?.calendar_range?.map(
(
range: CalendarRangeType & {
id?: string
calendarRange.value = res?.calendar_range
?.map(
(
range: CalendarRangeType & {
id?: string
},
) => {
const fromCol = meta.value?.columns?.find((col) => col.id === range.fk_from_column_id)
const toCol = range.fk_to_column_id ? meta.value?.columns?.find((col) => col.id === range.fk_to_column_id) : null
if (fromCol?.uidt === UITypes.Formula || toCol?.uidt === UITypes.Formula) {
// Check if fromCol Formula return type is Date
const isFromColDate =
fromCol?.uidt === UITypes.Formula &&
(fromCol?.colOptions as any)?.parsed_tree?.dataType === FormulaDataTypes.DATE
// Check if toCol Formula return type is Date
const isToColDate =
toCol?.uidt === UITypes.Formula && (toCol?.colOptions as any)?.parsed_tree?.dataType === FormulaDataTypes.DATE
if (!isFromColDate) {
message.error(`Please update the Formula column ${fromCol?.title} to return a date`)
return null
}
if (toCol && !isToColDate) {
message.error(`Please update the Formula column ${toCol?.title} to return a date`)
return null
}
}
return {
id: range?.id,
fk_from_col: fromCol,
fk_to_col: toCol,
is_readonly: [fromCol, toCol].some((col) => isSystemColumn(col) || isVirtualCol(col)),
}
},
) => {
return {
id: range?.id,
fk_from_col: meta.value?.columns?.find((col) => col.id === range.fk_from_column_id),
fk_to_col: range.fk_to_column_id ? meta.value?.columns?.find((col) => col.id === range.fk_to_column_id) : null,
}
},
) as any
)
.filter(Boolean) as any
} catch (e: unknown) {
message.error(
`Error loading calendar meta ${await extractSdkResponseErrorMsg(

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

@ -218,7 +218,7 @@ export function useData(args: {
// query: { ignoreWebhook: !saved }
// }
)
await reloadAggregate?.trigger({ field: [property] })
await reloadAggregate?.trigger({ fields: [{ title: property }] })
if (!undo) {
addUndo({
@ -360,7 +360,7 @@ export function useData(args: {
}
await $api.dbTableRow.bulkUpdate(NOCO, metaValue?.base_id as string, metaValue?.id as string, updateArray)
await reloadAggregate?.trigger({ field: props })
await reloadAggregate?.trigger({ fields: props.map((p) => ({ title: p })) })
if (!undo) {
addUndo({

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

@ -173,7 +173,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
}
}
const loadAudits = async (_rowId?: string) => {
const loadAudits = async (_rowId?: string, showLoading: boolean = true) => {
if (!isUIAllowed('auditListRow') || isEeUI || (!row.value && !_rowId)) return
const rowId = _rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
@ -181,7 +181,9 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
if (!rowId) return
try {
isAuditLoading.value = true
if (showLoading) {
isAuditLoading.value = true
}
const res =
(
await $api.utils.auditList({

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

@ -160,6 +160,10 @@ export function useGlobalActions(state: State): Actions {
state.gridViewPageSize.value = pageSize
}
const setLeftSidebarSize = (size: number) => {
state.leftSidebarSize.value = size
}
return {
signIn,
signOut,
@ -171,5 +175,6 @@ export function useGlobalActions(state: State): Actions {
ncNavigateTo,
getMainUrl,
setGridViewPageSize,
setLeftSidebarSize,
}
}

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

@ -1,6 +1,7 @@
import { useStorage } from '@vueuse/core'
import type { JwtPayload } from 'jwt-decode'
import type { AppInfo, State, StoredState } from './types'
import { INITIAL_LEFT_SIDEBAR_WIDTH } from '~/lib/constants'
export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
/** get the preferred languages of a user, according to browser settings */
@ -57,6 +58,7 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
isMobileMode: null,
lastOpenedWorkspaceId: null,
gridViewPageSize: 25,
leftSidebarSize: INITIAL_LEFT_SIDEBAR_WIDTH,
}
/** saves a reactive state, any change to these values will write/delete to localStorage */

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

@ -53,6 +53,7 @@ export interface StoredState {
isMobileMode: boolean | null
lastOpenedWorkspaceId: string | null
gridViewPageSize: number
leftSidebarSize: number
}
export type State = ToRefs<Omit<StoredState, 'token'>> & {
@ -89,6 +90,7 @@ export interface Actions {
getBaseUrl: (workspaceId: string) => string | undefined
getMainUrl: (workspaceId: string) => string | undefined
setGridViewPageSize: (pageSize: number) => void
setLeftSidebarSize: (size: number) => void
}
export type ReadonlyState = Readonly<Pick<State, 'token' | 'user'>> & Omit<State, 'token' | 'user'>

2
packages/nc-gui/composables/useMultiSelect/index.ts

@ -1172,7 +1172,7 @@ export function useMultiSelect(
for (const col of cols) {
if (!col.title || !isPasteable(row, col)) {
if ((isBt(col) || isOo(pasteCol) || isMm(col)) && !isInfoShown) {
if ((isBt(col) || isOo(col) || isMm(col)) && !isInfoShown) {
message.info(t('msg.info.groupPasteIsNotSupportedOnLinksColumn'))
isInfoShown = true
}

15
packages/nc-gui/composables/useServerConfig.ts

@ -0,0 +1,15 @@
const useServerConfig = () => {
const getConfig = async () => {}
const checkMaintenance = async () => {}
const dismissMaintenance = () => {}
return {
getConfig,
checkMaintenance,
dismissMaintenance,
}
}
export default useServerConfig

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

@ -233,6 +233,35 @@ export function useSharedView() {
)
}
const fetchBulkAggregatedData = async (param: {
aggregation?: Array<{
field: string
type: string
}>
aggregateFilterList: Array<{
where: string
alias: string
}>
filtersArr?: FilterType[]
where?: string
}) => {
if (!sharedView.value) return {}
return await $api.public.dataTableBulkAggregate(
sharedView.value.uuid!,
{
...param,
aggregateFilterList: JSON.stringify(param.aggregateFilterList),
filterArrJson: JSON.stringify(param.filtersArr ?? nestedFilters.value),
} as any,
{
headers: {
'xc-password': password.value,
},
},
)
}
const fetchSharedViewActiveDate = async (param: {
from_date: string
to_date: string
@ -318,6 +347,7 @@ export function useSharedView() {
fetchSharedCalendarViewData,
fetchSharedViewGroupedData,
fetchAggregatedData,
fetchBulkAggregatedData,
paginationData,
sorts,
exportFile,

26
packages/nc-gui/composables/useViewAggregate.ts

@ -21,7 +21,7 @@ const [useProvideViewAggregate, useViewAggregate] = useInjectionState(
const aggregations = ref({}) as Ref<Record<string, any>>
const reloadAggregate = inject(ReloadAggregateHookInj, createEventHook())
const reloadAggregate = inject(ReloadAggregateHookInj)
const visibleFieldsComputed = computed(() => {
const fie = fields.value.map((field, index) => ({ field, index })).filter((f) => f.index !== 0)
@ -99,26 +99,28 @@ const [useProvideViewAggregate, useViewAggregate] = useInjectionState(
}
const updateAggregate = async (fieldId: string, agg: string) => {
loadViewAggregate([
{
field: fieldId,
type: agg,
},
])
await reloadAggregate?.trigger({
fields: [
{
title: fields.value.find((f) => f.id === fieldId)?.title ?? '',
aggregation: agg,
},
],
})
await updateGridViewColumn(fieldId, { aggregation: agg })
}
reloadAggregate?.on(async (_fields) => {
if (!_fields || !_fields.field?.length) {
if (!_fields || !_fields?.fields.length) {
await loadViewAggregate()
}
if (_fields?.field) {
const fieldAggregateMapping = _fields.field.reduce((acc, field) => {
const f = fields.value.find((f) => f.title === field)
if (_fields?.fields) {
const fieldAggregateMapping = _fields.fields.reduce((acc, field) => {
const f = fields.value.find((f) => f.title === field.title)
if (!f?.id) return acc
acc[f.id] = gridViewCols.value[f.id].aggregation ?? CommonAggregations.None
acc[f.id] = field.aggregation ?? gridViewCols.value[f.id].aggregation ?? CommonAggregations.None
return acc
}, {} as Record<string, string>)

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

@ -21,6 +21,15 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
const fields = ref<Field[]>()
const fieldsMap = computed(() =>
(fields.value || []).reduce<Record<string, any>>((acc, curr) => {
if (curr.fk_column_id) {
acc[curr.fk_column_id] = curr
}
return acc
}, {}),
)
const filterQuery = ref('')
const { $api, $e } = useNuxtApp()
@ -420,6 +429,7 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
return {
fields,
fieldsMap,
loadViewColumns,
filteredFieldList,
filterQuery,

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

@ -1,7 +1,16 @@
import type { ColumnType, LinkToAnotherRecordType, LookupType, SelectOptionsType, TableType, ViewType } from 'nocodb-sdk'
import {
type ColumnType,
CommonAggregations,
type LinkToAnotherRecordType,
type LookupType,
type SelectOptionsType,
type TableType,
type ViewType,
} from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { message } from 'ant-design-vue'
import type { Group } from '../lib/types'
const excludedGroupingUidt = [UITypes.Attachment, UITypes.QrCode, UITypes.Barcode]
@ -20,7 +29,7 @@ const [useProvideViewGroupBy, useViewGroupBy] = useInjectionState(
const { base } = storeToRefs(useBase())
const { sharedView, fetchSharedViewData } = useSharedView()
const { sharedView, fetchSharedViewData, fetchBulkAggregatedData } = useSharedView()
const { gridViewCols } = useViewColumnsOrThrow()
@ -82,6 +91,7 @@ const [useProvideViewGroupBy, useViewGroupBy] = useInjectionState(
count: 0,
column: {} as any,
nestedIn: [],
aggregations: {},
paginationData: { page: 1, pageSize: groupByGroupLimit.value },
nested: true,
children: [],
@ -303,6 +313,7 @@ const [useProvideViewGroupBy, useViewGroupBy] = useInjectionState(
column_uidt: groupby.column.uidt,
},
],
aggregations: curr.aggregations ?? {},
paginationData: {
page: 1,
pageSize: group!.nestedIn.length < groupBy.value.length - 1 ? groupByGroupLimit.value : groupByRecordLimit.value,
@ -325,6 +336,7 @@ const [useProvideViewGroupBy, useViewGroupBy] = useInjectionState(
totalRows: temp.count,
}
temp.color = keyExists.color
// update group
Object.assign(keyExists, temp)
continue
@ -350,6 +362,29 @@ const [useProvideViewGroupBy, useViewGroupBy] = useInjectionState(
if (expectedPage < group.paginationData.page!) {
await groupWrapperChangePage(expectedPage, group)
}
if (appInfo.value.ee) {
const aggregationParams = (group.children ?? []).map((child) => ({
where: calculateNestedWhere(child.nestedIn, where?.value),
alias: child.key,
}))
const aggResponse = !isPublic
? await api.dbDataTableBulkAggregate.dbDataTableBulkAggregate(meta.value!.id, {
viewId: view.value!.id,
aggregateFilterList: aggregationParams,
})
: await fetchBulkAggregatedData({
aggregateFilterList: aggregationParams,
})
Object.entries(aggResponse).forEach(([key, value]) => {
const child = (group?.children ?? []).find((c) => c.key.toString() === key.toString())
if (child) {
Object.assign(child.aggregations, value)
}
})
}
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -390,6 +425,47 @@ const [useProvideViewGroupBy, useViewGroupBy] = useInjectionState(
}
}
async function loadGroupAggregation(
group: Group,
fields?: Array<{
field: string
type: string
}>,
) {
try {
if (!meta?.value?.id || !view.value?.id || !view.value?.fk_model_id || !appInfo.value.ee) return
const filteredFields = fields?.filter((x) => x.type !== CommonAggregations.None)
if (filteredFields && !filteredFields?.length) return
const aggregationParams = (group.children ?? []).map((child) => ({
where: calculateNestedWhere(child.nestedIn, where?.value),
alias: child.key,
}))
const response = !isPublic
? await api.dbDataTableBulkAggregate.dbDataTableBulkAggregate(meta.value!.id, {
viewId: view.value!.id,
aggregateFilterList: aggregationParams,
...(filteredFields ? { aggregation: filteredFields } : {}),
})
: await fetchBulkAggregatedData({
aggregateFilterList: aggregationParams,
...(filteredFields ? { aggregation: filteredFields } : {}),
})
Object.entries(response).forEach(([key, value]) => {
const child = (group.children ?? []).find((c) => c.key.toString() === key.toString())
if (child) {
Object.assign(child.aggregations, value)
}
})
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const loadGroupPage = async (group: Group, p: number) => {
if (!group.paginationData) {
group.paginationData = { page: 1, pageSize: groupByRecordLimit.value }
@ -474,11 +550,14 @@ const [useProvideViewGroupBy, useViewGroupBy] = useInjectionState(
if (group.nested) {
const child = group.children?.find((g) => {
if (!groupBy.value[nestLevel].column.title) return undefined
return (
g.key ===
valueToTitle(row.row[groupBy.value[nestLevel].column.title!], groupBy.value[nestLevel].column, group.displayValueProp)
)
})
console.log(child)
if (child) {
return findGroupForRow(row, child, nestLevel + 1)
}
@ -501,15 +580,20 @@ const [useProvideViewGroupBy, useViewGroupBy] = useInjectionState(
}
if (group) {
group.rows?.splice(group!.rows.indexOf(row), 1)
console.log('removed Bunch')
modifyCount(group, -1)
}
}
} else {
if (group) {
group.rows?.splice(group!.rows.indexOf(row), 1)
console.log('removed Bunch22')
modifyCount(group, -1)
} else {
rootGroup.value.rows?.splice(rootGroup.value.rows!.indexOf(row), 1)
}
if (properGroup.group?.children) loadGroups({}, properGroup.group)
// if (properGroup.group?.children) loadGroups({}, properGroup.group)
}
})
} else {
@ -572,6 +656,7 @@ const [useProvideViewGroupBy, useViewGroupBy] = useInjectionState(
loadGroups,
loadGroupData,
loadGroupPage,
loadGroupAggregation,
groupWrapperChangePage,
redistributeRows,
}

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

@ -31,9 +31,17 @@ export const ReloadViewDataHookInj: InjectionKey<EventHook<{ shouldShowLoading?:
export const ReloadViewMetaHookInj: InjectionKey<EventHook<boolean | void>> = Symbol('reload-view-meta-injection')
export const ReloadRowDataHookInj: InjectionKey<EventHook<{ shouldShowLoading?: boolean; offset?: number } | void>> =
Symbol('reload-row-data-injection')
export const ReloadAggregateHookInj: InjectionKey<EventHook<{ field: string[] } | undefined>> = Symbol(
'reload-aggregate-data-injection',
)
export const ReloadAggregateHookInj: InjectionKey<
EventHook<
| {
fields: {
title: string
aggregation?: string
}[]
}
| undefined
>
> = Symbol('reload-aggregate-data-injection')
export const OpenNewRecordFormHookInj: InjectionKey<EventHook<void>> = Symbol('open-new-record-form-injection')
export const FieldsInj: InjectionKey<Ref<ColumnType[]>> = Symbol('fields-injection')
export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injection')

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

@ -142,9 +142,11 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
r: +this.config.firstRowAsHeaders,
})
const cellProps = ws[cellId] || {}
column.uidt = excelTypeToUidt[cellProps.t] || UITypes.SingleLineText
column.uidt = this.config.importDataOnly
? excelTypeToUidt[cellProps.t] || UITypes.SingleLineText
: UITypes.SingleLineText
if (column.uidt === UITypes.SingleLineText) {
if (column.uidt === UITypes.SingleLineText && this.config.importDataOnly) {
// check for long text
if (isMultiLineTextType(rows, col)) {
column.uidt = UITypes.LongText
@ -238,7 +240,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
for (const row of rows.slice(1)) {
const rowData: Record<string, any> = {}
for (let i = 0; i < table.columns.length; i++) {
if (!this.config.autoSelectFieldTypes) {
if (!this.config.autoSelectFieldTypes || !this.config.importDataOnly) {
// take raw data instead of data parsed by xlsx
const cellId = this.xlsx.utils.encode_cell({
c: range.s.c + i,
@ -257,7 +259,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
const cellObj = ws[cellId]
rowData[table.columns[i].column_name] =
(cellObj && cellObj.w && cellObj.w.replace(/[^\d.]+/g, '')) || row[i]
(cellObj && typeof cellObj?.w === 'string' && cellObj.w.replace(/[^\d.]+/g, '')) || row[i]
} else if (table.columns[i].uidt === UITypes.SingleSelect || table.columns[i].uidt === UITypes.MultiSelect) {
rowData[table.columns[i].column_name] = (row[i] || '').toString().trim() || null
} else if (table.columns[i].uidt === UITypes.Date) {

31
packages/nc-gui/lang/ar.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "ليس فارغاً"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "Created By",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "إعلام عبر",
"projName": "اسم المشروع",
@ -941,7 +956,7 @@
"addFilter": "إضافة تصفية",
"share": "مشاركة",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "تعطيل قاعدة مشتركة",
@ -1067,12 +1082,12 @@
"showJunctionTableNames": "Show Junction Table Names"
},
"kanban": {
"collapseStack": "Collapse Stack",
"collapseStack": "Collapse stack",
"collapseAll": "Collapse all",
"expandAll": "Expand all",
"renameStack": "Rename stack",
"deleteStack": "Delete Stack",
"stackedBy": "Stacked By",
"deleteStack": "Delete stack",
"stackedBy": "Stacked by",
"chooseGroupingField": "Choose a Grouping Field",
"addOrEditStack": "Add / Edit Stack"
},
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

31
packages/nc-gui/lang/bn_IN.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "নল নয"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "Created By",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "এর মযম অবহিত করন",
"projName": "পরকলর নম",
@ -941,7 +956,7 @@
"addFilter": "ফির যত করন",
"share": "শর",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "ভগ করস অকষম করন",
@ -1067,12 +1082,12 @@
"showJunctionTableNames": "Show Junction Table Names"
},
"kanban": {
"collapseStack": "Collapse Stack",
"collapseStack": "Collapse stack",
"collapseAll": "Collapse all",
"expandAll": "Expand all",
"renameStack": "Rename stack",
"deleteStack": "Delete Stack",
"stackedBy": "Stacked By",
"deleteStack": "Delete stack",
"stackedBy": "Stacked by",
"chooseGroupingField": "Choose a Grouping Field",
"addOrEditStack": "Add / Edit Stack"
},
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

23
packages/nc-gui/lang/cs.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "není null"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Kopírovat URL záznamu",
@ -648,7 +664,6 @@
"createdBy": "Vytvořil/a",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Vytvořeno",
"notifyVia": "Upozornit pomocí",
"projName": "Název projektu",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Vytvoření osobních API tokenů pro použití při automatizaci nebo v externích aplikacích.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

25
packages/nc-gui/lang/da.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "er ikke null."
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "Oprettet af",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "Bemærk Via.",
"projName": "Projekt navn",
@ -941,7 +956,7 @@
"addFilter": "Tilføj filter",
"share": "Del",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "Deaktiver delt base",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

41
packages/nc-gui/lang/de.json

@ -30,25 +30,25 @@
"scatter_plot": "Streudiagramm",
"bubble_chart": "Blasendiagramm",
"radar_chart": "Netzdiagramm",
"polar_area_chart": "Polar Area Chart",
"polar_area_chart": "Polargebiet-Kreisdiagramm",
"radial_bar_chart": "Radiales Balkendiagramm",
"heatmap_chart": "Heatmap-Diagramm",
"treemap_chart": "Baumdiagramm",
"box_plot_chart": "Box Plot Chart",
"candlestick_chart": "Candlestick Chart"
"box_plot_chart": "Boxplot-Diagramm",
"candlestick_chart": "Candlestick Diagramm"
}
},
"aggregation": {
"sum": "Sum",
"count": "Count",
"sum": "Summe",
"count": "Anzahl",
"min": "Min",
"max": "Max",
"avg": "Avg",
"avg": "Durchschn.",
"median": "Median",
"std_dev": "Std dev",
"histogram": "Histogram",
"range": "Range",
"percent_empty": "Empty",
"std_dev": "Std. Abweichung",
"histogram": "Histogramm",
"range": "Bereich",
"percent_empty": "Leer",
"percent_filled": "Filled",
"percent_unique": "Unique",
"count_unique": "Unique",
@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Tag",
@ -384,6 +385,9 @@
"isNotNull": "ist nicht Null"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -648,7 +664,6 @@
"createdBy": "Erstellt von",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Erstellt am",
"notifyVia": "Benachrichtigen mit",
"projName": "Projektname",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Ungültige Uhrzeit",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Ungültige Telefonnummer",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Personalisierte API Tokens zur Verwendung in Automatisierungen oder externen Apps erstellen.",
"selectFieldToSort": "Feld zum Sortieren auswählen",
"selectFieldToGroup": "Feld zum Gruppieren auswählen",

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

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "is not null"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -479,7 +483,7 @@
"quickImportExcel": "Quick Import - Excel",
"quickImportJSON": "Quick Import - JSON",
"jsonEditor": "JSON Editor",
"comingSoon": "Coming Soon",
"comingSoon": "Coming soon",
"advancedSettings": "Advanced Settings",
"codeSnippet": "Code Snippet",
"keyboardShortcut": "Keyboard Shortcuts",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Change",
"allowDataWrite": "Allow Data Write/Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "Created By",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "Notify Via",
"projName": "Base name",
@ -941,7 +956,7 @@
"addFilter": "Add filter",
"share": "Share",
"groupBy": "Group By",
"addSubGroup": "New subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "Disable shared base",
@ -1067,12 +1082,12 @@
"showJunctionTableNames": "Show Junction Table Names"
},
"kanban": {
"collapseStack": "Collapse Stack",
"collapseStack": "Collapse stack",
"collapseAll": "Collapse all",
"expandAll": "Expand all",
"renameStack": "Rename stack",
"deleteStack": "Delete Stack",
"stackedBy": "Stacked By",
"deleteStack": "Delete stack",
"stackedBy": "Stacked by",
"chooseGroupingField": "Choose a Grouping Field",
"addOrEditStack": "Add / Edit Stack"
},
@ -1134,7 +1149,8 @@
"clientCA": "Select CA file",
"changeIconColour": "Change icon colour",
"preFillFormInfo": "To get a prefilled link, make sure you’ve filled the necessary fields in the form view builder.",
"surveyFormInfo": "Form mode with one field per page"
"surveyFormInfo": "Form mode with one field per page",
"useFieldEditMenuToConfigFieldType": "Use field edit menu for type conversions after file is imported"
},
"placeholder": {
"selectSlackChannels": "Select Slack channels",
@ -1211,7 +1227,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1301,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

21
packages/nc-gui/lang/es.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Día",
@ -384,6 +385,9 @@
"isNotNull": "no es nulo"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Soltar aquí",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -648,7 +664,6 @@
"createdBy": "Creado por",
"viewingAttachmentsOf": "Viendo adjuntos de",
"readOnly": "Sólo lectura",
"dropHere": "Soltar aquí",
"createdOn": "Creado el",
"notifyVia": "Notificar a través de",
"projName": "Nombre del proyecto",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Hora no válida",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Tamaño de página modificado",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No hay registros vinculados",
"noLinkedRecords": "No linked records",
"recordsLinked": "registros vinculados",
"acceptOnlyValid": "Sólo acepta",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Cree tokens de API personales para utilizarlos en la automatización o en aplicaciones externas.",
"selectFieldToSort": "Seleccionar campo para ordenar",
"selectFieldToGroup": "Seleccione el campo al grupo",

31
packages/nc-gui/lang/eu.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "is not null"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "Sortzailea:",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "Jakinarazi nazazu honekin:",
"projName": "Proiektuaren izena",
@ -941,7 +956,7 @@
"addFilter": "Add Filter",
"share": "Share",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "Disable shared base",
@ -1067,12 +1082,12 @@
"showJunctionTableNames": "Show Junction Table Names"
},
"kanban": {
"collapseStack": "Collapse Stack",
"collapseStack": "Collapse stack",
"collapseAll": "Collapse all",
"expandAll": "Expand all",
"renameStack": "Rename stack",
"deleteStack": "Delete Stack",
"stackedBy": "Stacked By",
"deleteStack": "Delete stack",
"stackedBy": "Stacked by",
"chooseGroupingField": "Choose a Grouping Field",
"addOrEditStack": "Add / Edit Stack"
},
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

25
packages/nc-gui/lang/fa.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "تهی نیست"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "ایجاد شده توسط",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "اعلان از طریق",
"projName": "نام پروژه",
@ -941,7 +956,7 @@
"addFilter": "افزودن فیلتر",
"share": "اشتراک گذاری",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "غیرفعال کردن پایگاه مشترک",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

25
packages/nc-gui/lang/fi.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "ei ole nolla"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "Luonut",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "Ilmoittaa kautta",
"projName": "Projektin nimi",
@ -941,7 +956,7 @@
"addFilter": "Lisää suodatin",
"share": "Jaa",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "Poista jaettu pohja",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

123
packages/nc-gui/lang/fr.json

@ -39,58 +39,58 @@
}
},
"aggregation": {
"sum": "Sum",
"count": "Count",
"sum": "Somme",
"count": "Compte",
"min": "Min",
"max": "Max",
"avg": "Avg",
"median": "Median",
"std_dev": "Std dev",
"histogram": "Histogram",
"range": "Range",
"percent_empty": "Empty",
"percent_filled": "Filled",
"avg": "Moyenne",
"median": "Médiane",
"std_dev": "Ecart-type",
"histogram": "Histogramme",
"range": "Portée",
"percent_empty": "Vide",
"percent_filled": "Rempli",
"percent_unique": "Unique",
"count_unique": "Unique",
"count_empty": "Empty",
"count_filled": "Filled",
"earliest_date": "Min date",
"latest_date": "Max date",
"date_range": "Range",
"month_range": "Range",
"checked": "Checked",
"unchecked": "Unchecked",
"percent_checked": "Checked",
"percent_unchecked": "Unchecked",
"attachment_size": "Size",
"none": "None"
"count_empty": "Vide",
"count_filled": "Rempli",
"earliest_date": "Date mini",
"latest_date": "Date max",
"date_range": "Portée",
"month_range": "Portée",
"checked": "Coché",
"unchecked": "Décoché",
"percent_checked": "Coché",
"percent_unchecked": "Décoché",
"attachment_size": "Taille",
"none": "Aucun"
},
"aggregation_type": {
"sum": "Sum",
"count": "Count",
"min": "Min",
"max": "Max",
"avg": "Average",
"median": "Median",
"std_dev": "Standard Deviation",
"histogram": "Histogram",
"range": "Range",
"percent_empty": "Percent Empty",
"percent_filled": "Percent Filled",
"percent_unique": "Percent Unique",
"sum": "Somme",
"count": "Compte",
"min": "Minimum",
"max": "Maximum",
"avg": "Moyenne",
"median": "Médiane",
"std_dev": "Écart type",
"histogram": "Histogramme",
"range": "Portée",
"percent_empty": "Pourcentage de vide",
"percent_filled": "Pourcentage de renseignés",
"percent_unique": "Pourcentage de valeurs uniques",
"count_unique": "Unique",
"count_empty": "Empty",
"count_filled": "Filled",
"earliest_date": "Earliest Date",
"latest_date": "Latest Date",
"date_range": "Date Range",
"month_range": "Month Range",
"checked": "Checked",
"unchecked": "Unchecked",
"percent_checked": "Percent Checked",
"percent_unchecked": "Percent Unchecked",
"attachment_size": "Attachment Size",
"none": "None"
"count_empty": "Vide",
"count_filled": "Rempli",
"earliest_date": "Date la plus ancienne",
"latest_date": "Date la plus récente",
"date_range": "Portée en jours",
"month_range": "Portée des mois",
"checked": "Coché",
"unchecked": "Décoché",
"percent_checked": "Pourcentage coché",
"percent_unchecked": "Pourcentage non coché",
"attachment_size": "Taille des pièces jointes",
"none": "Aucun"
},
"general": {
"role": "Rôle",
@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Propriétaire",
"member": "Membre",
"day": "Jour",
@ -384,6 +385,9 @@
"isNotNull": "est non null"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Renommer la Base",
"renameWorkspace": "Renommer l'espace de travail",
"renamingWorkspace": "Renommer l'espace de travail",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Déposer ici",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -648,7 +664,6 @@
"createdBy": "Créé par",
"viewingAttachmentsOf": "Visualisation des pièces jointes de",
"readOnly": "Lecture seule",
"dropHere": "Déposer ici",
"createdOn": "Créé le",
"notifyVia": "Notifier via",
"projName": "Nom du projet",
@ -1102,8 +1117,8 @@
"schemaChangeDisabled": "Schema editing is disabled for this data source.",
"typeNotAllowed": "This datatype is not allowed.",
"dataWriteOptionDisabled": "Data editing can only be disabled when 'Schema editing' is also disabled.",
"allowMetaWrite": "This option allows modification of database schema, including adding, altering, or deleting tables and columns. Use with caution, as changes may impact the structural integrity of your database.",
"allowDataWrite": "This option allows create, update, or delete of records within database tables. Ideal for administrative users need to change data directly.",
"allowMetaWrite": "Cette option permet de modifier le schéma de la base de données, notamment d'ajouter, de modifier ou de supprimer des tables et des colonnes. Utilisez-la avec précaution, car les modifications peuvent avoir un impact sur l'intégrité de la structure de votre base de données.",
"allowDataWrite": "Cette option permet de créer, de mettre à jour ou de supprimer des enregistrements dans les tables de la base de données. Elle est idéale pour les administrateurs qui ont besoin de modifier des données directement en base.",
"reachedSourceLimit": "Limité à une seule source de données pour le moment",
"saveChanges": "Sauvegarder les modifications",
"xcDB": "Créer un nouveau projet",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Le champ de type Lookup n'est pas supporté pour les relations de non appartenance",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "Vous n'avez aucun lien défini pour ajouter un champ Lookup",
"linkColumnClearNotSupportedYet": "Vous n'avez aucun lien défini pour ajouter un champ {type}",
"recordCouldNotBeFound": "L'enregistrement n'a pas pu être trouvé",
"invalidPhoneNumber": "Numéro de téléphone invalide",
"pageSizeChanged": "Taille de la page modifiée",
@ -1251,7 +1266,7 @@
"selectOption": {
"cantBeNull": "Les options de sélection ne peuvent pas être nulles",
"multiSelectCantHaveCommas": "Les champs de type MultiSelect ne peuvent pas contenir de virgules (',')",
"cantHaveDuplicates": "Select options can't have duplicates",
"cantHaveDuplicates": "Les options de sélection ne peuvent pas être dupliquées",
"createNewOptionNamed": "Créer une nouvelle option nommée"
},
"plsEnterANumber": "Veuillez saisir un nombre",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "Aucun enregistrement lié",
"noLinkedRecords": "Aucun enregistrement lié",
"recordsLinked": "enregistrements liés",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Créer des jetons d'API personnels à utiliser dans une automatisation ou des applications externes.",
"selectFieldToSort": "Sélectionner le champ de tri",
"selectFieldToGroup": "Sélectionner le champ de regroupement",
@ -1327,7 +1342,7 @@
}
},
"info": {
"schemaReadOnly": "Schema alterations are disabled for this source",
"schemaReadOnly": "Les modifications de schéma sont désactivées pour cette source",
"enterWorkspaceName": "Entrez le nom de l'espace de travail",
"enterBaseName": "Entrez le nom de base",
"idpPaste": "Collez cette URL dans la console de vos fournisseurs d'identité",
@ -1494,7 +1509,7 @@
"goToNext": "Aller au suivant",
"thankYou": "Merci !",
"submittedFormData": "Vous avez soumis les données du formulaire avec succès.",
"editingSystemKeyNotSupported": "Editing system key not supported",
"editingSystemKeyNotSupported": "Modification de la clé primaire non prise en charge",
"notAvailableAtTheMoment": "Non disponible pour le moment",
"groupPasteIsNotSupportedOnLinksColumn": "Group paste operation is not supported on Links/LinkToAnotherRecord column",
"groupClearIsNotSupportedOnLinksColumn": "Group clear operation is not supported on Links/LinkToAnotherRecord column",
@ -1550,7 +1565,7 @@
"invalidEmails": "E-mails non valides",
"invalidEmail": "Email non valide"
},
"invalidXml": "Invalid XML",
"invalidXml": "XML invalide",
"invalidURL": "URL invalide",
"invalidEmail": "Email invalide",
"internalError": "Une erreur interne est survenue",

23
packages/nc-gui/lang/he.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "בעלים",
"member": "חבר",
"day": "יום",
@ -384,6 +385,9 @@
"isNotNull": "לא NULL"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "שינוי שם בסיס",
"renameWorkspace": "שינוי שם סביבת עבודה",
"renamingWorkspace": "משנים שם סביבת עבודה",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "שחרר כאן",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "שם הכותרת",
"icon": "סמל",
"max": "מקסימום",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "מזהה:",
"copiedRecordURL": "כתובת URL של הרשומה הועתקה",
"copyRecordURL": "העתק כתובת URL של הרשומה",
@ -648,7 +664,6 @@
"createdBy": "נוצר על ידי",
"viewingAttachmentsOf": "צפייה בקבצים מצורפים של",
"readOnly": "קריאה בלבד",
"dropHere": "שחרר כאן",
"createdOn": "נוצר בתאריך",
"notifyVia": "הודע דרך",
"projName": "שם הפרוייקט",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "שאילתה מקורטת מופעלת",
"lookupNonBtWarning": "שדה החיפוש לא נתמך לאינטרקציה מחוץ ליחס 'נמצא ב'",
"invalidTime": "זמן לא חוקי",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "הרשומה לא נמצאה",
"invalidPhoneNumber": "מספר טלפון לא חוקי",
"pageSizeChanged": "גודל העמוד השתנה",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "לא נקשרו רשומות",
"noLinkedRecords": "אין רשומות מקושרות",
"recordsLinked": "רשומות מקושרות",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "צור אסימון API אישי כדי להשתמש באוטומציה או ביישומי חיצוניים.",
"selectFieldToSort": "בחר שדה למיון",
"selectFieldToGroup": "בחר שדה לקיבוץ",

31
packages/nc-gui/lang/hi.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "निररथक नह"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "Created By",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "Notify Via",
"projName": "परिजनम",
@ -941,7 +956,7 @@
"addFilter": "फिटर ज",
"share": "शयर करन",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "स आधर अकषम कर",
@ -1067,12 +1082,12 @@
"showJunctionTableNames": "Show Junction Table Names"
},
"kanban": {
"collapseStack": "Collapse Stack",
"collapseStack": "Collapse stack",
"collapseAll": "Collapse all",
"expandAll": "Expand all",
"renameStack": "Rename stack",
"deleteStack": "Delete Stack",
"stackedBy": "Stacked By",
"deleteStack": "Delete stack",
"stackedBy": "Stacked by",
"chooseGroupingField": "Choose a Grouping Field",
"addOrEditStack": "Add / Edit Stack"
},
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

25
packages/nc-gui/lang/hr.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "nije ništavan"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "Kreirao/Kreirala",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "Obavijestiti putem",
"projName": "Naziv projekta",
@ -941,7 +956,7 @@
"addFilter": "Dodajte filtar",
"share": "Udio",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "Onemogući zajedničku bazu",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

83
packages/nc-gui/lang/hu.json

@ -271,6 +271,7 @@
"ipAddress": "IP cím"
},
"objects": {
"files": "files",
"owner": "Tulajdonos",
"member": "Tag",
"day": "Nap",
@ -384,6 +385,9 @@
"isNotNull": "nem null"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Bázis átnevezése",
"renameWorkspace": "Munkaterület átnevezése",
"renamingWorkspace": "Munkaterület átnevezése",
@ -444,9 +448,9 @@
"locked": "Locked",
"personal": "Personal",
"appStore": "App Store",
"teamAndAuth": "Team & Auth",
"rolesUserMgmt": "Roles & Users Management",
"userMgmt": "Users Management",
"teamAndAuth": "Csapat és bejelentkezés",
"rolesUserMgmt": "Szerepkörök és felhasználók kezelése",
"userMgmt": "Felhasználók kezelése",
"apiTokens": "API Tokens",
"apiTokenMgmt": "API Tokens Management",
"rolesMgmt": "Roles Management",
@ -464,7 +468,7 @@
"headCreateProject": "Bázis létrehozása | NocoDB",
"headLogin": "Log In | NocoDB",
"resetPassword": "Reset your password",
"teamAndSettings": "Team & Settings",
"teamAndSettings": "Csapat és beállítások",
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable",
"generateToken": "Generate Token",
@ -472,12 +476,12 @@
"helpCenter": "Help Center",
"noLabels": "No Labels",
"swaggerDocumentation": "Swagger Documentation",
"quickImportFrom": "Quick Import From",
"quickImport": "Quick Import",
"quickImportAirtable": "Quick Import - Airtable",
"quickImportCSV": "Quick Import - CSV",
"quickImportExcel": "Quick Import - Excel",
"quickImportJSON": "Quick Import - JSON",
"quickImportFrom": "Gyors importálás ebből",
"quickImport": "Gyors importálás",
"quickImportAirtable": "Gyors importálás - Airtable",
"quickImportCSV": "Gyors importálás - CSV",
"quickImportExcel": "Gyors importálás - Excel",
"quickImportJSON": "Gyors importálás - JSON",
"jsonEditor": "JSON Editor",
"comingSoon": "Coming Soon",
"advancedSettings": "Advanced Settings",
@ -491,7 +495,7 @@
"accountSettings": "Account Settings",
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
"userManagement": "User Management",
"userManagement": "Felhasználó kezelése",
"accountManagement": "Account management",
"licence": "Licence",
"allowAllMimeTypes": "Allow All Mime Types",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -537,8 +553,8 @@
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"signOutUser": "Felhasználó kijelentkeztetése",
"signOutUsers": "Felhasználók kijelentkeztetése",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
@ -552,7 +568,7 @@
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"userOptions": "Felhasználó beállításai",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"childView": "Child View",
@ -638,7 +654,7 @@
"clickToCopyTableID": "Click to copy Table ID",
"clickToCopyViewID": "Click to copy View ID",
"viewMode": "View Mode",
"searchUsers": "Search Users",
"searchUsers": "Felhasználók keresése",
"superAdmin": "Super Admin",
"allTables": "All Tables",
"members": "Members",
@ -648,7 +664,6 @@
"createdBy": "Created By",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "Notify Via",
"projName": "Bázis neve",
@ -680,7 +695,7 @@
"columnType": "Field Type",
"roleName": "Role Name",
"roleDescription": "Role Description",
"databaseType": "Type in Database",
"databaseType": "Típus az adatbázisban",
"lengthValue": "Length/ value",
"dbType": "Adatbázis típusa",
"servername": "servername / hostAddr",
@ -849,12 +864,12 @@
"renameBase": "Bázis átnevezése",
"renameWorkspace": "Munkaterület átnevezése",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"manageUsers": "Felhasználók kezelése",
"newWorkspace": "Új munkaterület",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToBase": "Meghívás bázisba",
"inviteToWorkspace": "Meghívása a munkaterületbe",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
@ -876,13 +891,13 @@
"attachmentDrop": "Click or drop a file into cell",
"addFiles": "Fájl(ok) hozzáadása",
"hideInUI": "Hide in UI",
"addBase": "Add Base",
"addBase": "Bázis hozáádaása",
"addParameter": "Add Parameter",
"submitAnotherForm": "Submit Another Form",
"dragAndDropFieldsHereToAdd": "Drag and drop fields here to add",
"editSource": "Edit Data Source",
"enterText": "Enter text",
"okEditBase": "Ok & Edit Base",
"okEditBase": "Ok és bázis szerkesztése",
"showInUI": "Show in UI",
"outOfSync": "Out of sync",
"newSource": "New Data Source",
@ -941,22 +956,22 @@
"addFilter": "Add filter",
"share": "Share",
"groupBy": "Group By",
"addSubGroup": "New subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"label": "Bázis megosztása",
"disable": "Disable shared base",
"enable": "Anyone with the link",
"link": "Shared base link"
},
"invite": "Invite",
"inviteMore": "Invite more",
"inviteTeam": "Invite Team",
"inviteUser": "Invite User",
"inviteTeam": "Csapat meghívása",
"inviteUser": "Felhasználó meghívása",
"inviteToken": "Invite Token",
"linkedRecords": "Hivatkozott rekordok",
"addNewLink": "Add New Link",
"newUser": "New User",
"editUser": "Edit user",
"newUser": "Új felhasználó",
"editUser": "Felhasználó szerkesztése",
"deleteUser": "Felhasználó eltávolítása a bázisról",
"resendInvite": "Resend Invite E-mail",
"copyInviteURL": "Copy Invite URL",
@ -1035,7 +1050,7 @@
"editConnJson": "Edit connection JSON",
"sponsorUs": "Sponsor Us",
"sendEmail": "SEND EMAIL",
"addUserToProject": "Add user to base",
"addUserToProject": "Felhasználó hozzáadása a bázishoz",
"getApiSnippet": "Get API Snippet",
"clearCell": "Clear cell",
"addFilterGroup": "Add filter group",
@ -1106,7 +1121,7 @@
"allowDataWrite": "Enable this option to allow updating, deleting, or inserting data within the database tables. Ideal for administrative users who need to manage data directly.",
"reachedSourceLimit": "Limited to 10 data sources per base",
"saveChanges": "Save changes",
"xcDB": "Create a new base",
"xcDB": "Új bázis létrehozása",
"extDB": "Supports MySQL, PostgreSQL, SQL Server & SQLite",
"apiRest": "Accessible via REST APIs",
"apiGQL": "Accessible via GraphQL APIs",
@ -1138,14 +1153,14 @@
},
"placeholder": {
"selectSlackChannels": "Select Slack channels",
"selectTeamsChannels": "Select Microsoft Teams channels",
"selectTeamsChannels": "Mixrosoft Teams csatorna kiválasztása",
"selectDiscordChannels": "Select Discord channels",
"selectMattermostChannels": "Select Mattermost channels",
"webhookTitle": "Webhook Title",
"barcodeColumn": "Select a field for the Barcode value",
"notFoundContent": "No valid field Type can be found.",
"selectBarcodeFormat": "Select a Barcode format",
"projName": "Enter Base Name",
"projName": "Bázis neve",
"selectGroupField": "Select a Grouping Field",
"selectGroupFieldNotFound": "No Single Select Field can be found. Please create one first.",
"selectCoverImageField": "Select a cover image field",
@ -1186,7 +1201,7 @@
"createTableLabel": "From scratch or import or connect to external database",
"noTokenCreated": "No API Tokens created",
"noTokenCreatedLabel": "Looks like you haven’t generated any API tokens yet.",
"inviteYourTeam": "Invite your team",
"inviteYourTeam": "Csapat meghívása",
"inviteYourTeamLabel": "Fast track your projects by collaborating on them with your team!",
"searchOptions": "Search options"
},
@ -1329,7 +1344,7 @@
"info": {
"schemaReadOnly": "Schema alterations are disabled for this source",
"enterWorkspaceName": "Munkaterület nevének megadása",
"enterBaseName": "Enter base name",
"enterBaseName": "Bázis neve",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
@ -1398,7 +1413,7 @@
"editorDesc": "Can edit records but cannot change structure of database/fields.",
"commenterDesc": "Can view and comment the records but cannot edit anything",
"viewerDesc": "Can view the records but cannot edit anything",
"addUser": "Add new user",
"addUser": "Új felhasználó hozzáadása",
"staticRoleInfo": "System defined roles can't be edited",
"exportZip": "Export base meta to zip file and download.",
"importZip": "Import base meta zip file and restart.",

25
packages/nc-gui/lang/id.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "bukan null."
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "Dibuat oleh",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "Beri tahu VIA.",
"projName": "Nama Proyek",
@ -941,7 +956,7 @@
"addFilter": "Tambahkan Filter",
"share": "Membagikan",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "Nonaktifkan basis bersama",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

21
packages/nc-gui/lang/it.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "non è nullo"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Rilascia qui",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -648,7 +664,6 @@
"createdBy": "Creato da",
"viewingAttachmentsOf": "Vedi allegati di",
"readOnly": "Sola lettura",
"dropHere": "Rilascia qui",
"createdOn": "Creato il",
"notifyVia": "Notifica tramite",
"projName": "Nome del progetto",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "La query ottimizzata è abilitata",
"lookupNonBtWarning": "Il campo Lookup non è supportato per la relazione non-appartenente",
"invalidTime": "Ora non valida",
"linkColumnClearNotSupportedYet": "Non ci sono collegamenti supportati per Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Riga non trovata",
"invalidPhoneNumber": "Numero di telefono non valido",
"pageSizeChanged": "Dimensione pagina modificata",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "Nessun record collegato",
"noLinkedRecords": "No linked records",
"recordsLinked": "record collegati",
"acceptOnlyValid": "Accetta solo",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Creare token API personali da utilizzare nell'automazione o in applicazioni esterne.",
"selectFieldToSort": "Selezionare il campo da ordinare",
"selectFieldToGroup": "Selezionare il campo da raggruppare",

25
packages/nc-gui/lang/ja.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "がnullでない"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "クリエイティッド バイ",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "次で通知する",
"projName": "プロジェクト名",
@ -941,7 +956,7 @@
"addFilter": "フィルターを追加",
"share": "共有",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "テーブルの共有を無効にする",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

25
packages/nc-gui/lang/ko.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "null 값이 아님"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "이곳에 끌어다 놓기",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "헤더 이름",
"icon": "아이콘",
"max": "최대",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "ID:",
"copiedRecordURL": "레코드 URL을 복사했습니다.",
"copyRecordURL": "레코드 URL 복사",
@ -648,7 +664,6 @@
"createdBy": "생성자",
"viewingAttachmentsOf": "첨부 파일 보기",
"readOnly": "읽기 전용 (readonly)",
"dropHere": "이곳에 끌어다 놓기",
"createdOn": "생성일",
"notifyVia": "공지",
"projName": "프로젝트 이름",
@ -941,7 +956,7 @@
"addFilter": "필터 추가",
"share": "공유",
"groupBy": "분류 기준",
"addSubGroup": "하위 그룹 추가",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "공유 베이스 비활성화",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "최적화된 쿼리가 활성화되었습니다",
"lookupNonBtWarning": "Lookup 필드는 Belongs To 관계에서만 사용할 수 있습니다.",
"invalidTime": "유효하지 않은 시간형식입니다.",
"linkColumnClearNotSupportedYet": "링크 열의 내용을 지우는 것은 아직 지원되지 않습니다.",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "레코드를 찾을 수 없습니다.",
"invalidPhoneNumber": "유효하지 않은 전화번호입니다.",
"pageSizeChanged": "페이지 크기가 변경되었습니다.",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "레코드가 연결되지 않았습니다.",
"noLinkedRecords": "No linked records",
"recordsLinked": "레코드가 연결되었습니다.",
"acceptOnlyValid": "유효한 값만 허용",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "새 API 토큰 생성",
"selectFieldToSort": "정렬할 필드 선택",
"selectFieldToGroup": "Select Field to Group",

23
packages/nc-gui/lang/lv.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "nav null"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -648,7 +664,6 @@
"createdBy": "Izveidoja",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "Paziņot izmantojot",
"projName": "Projekta nosaukums",
@ -941,7 +956,7 @@
"addFilter": "Pievienot filtru",
"share": "Koplietot",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "Atspējot koplietošanu",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

25
packages/nc-gui/lang/nl.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "is niet NULL"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "Gemaakt door",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "Melding via",
"projName": "Projectnaam",
@ -941,7 +956,7 @@
"addFilter": "Voeg filter toe",
"share": "Deel",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "Schakel de gedeelde database uit",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

31
packages/nc-gui/lang/no.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Day",
@ -384,6 +385,9 @@
"isNotNull": "er ikke null"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "Created By",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "Varsle Via.",
"projName": "Prosjektnavn",
@ -941,7 +956,7 @@
"addFilter": "Legg til filter",
"share": "Dele",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "Deaktiver delt base",
@ -1067,12 +1082,12 @@
"showJunctionTableNames": "Show Junction Table Names"
},
"kanban": {
"collapseStack": "Collapse Stack",
"collapseStack": "Collapse stack",
"collapseAll": "Collapse all",
"expandAll": "Expand all",
"renameStack": "Rename stack",
"deleteStack": "Delete Stack",
"stackedBy": "Stacked By",
"deleteStack": "Delete stack",
"stackedBy": "Stacked by",
"chooseGroupingField": "Choose a Grouping Field",
"addOrEditStack": "Add / Edit Stack"
},
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

47
packages/nc-gui/lang/pl.json

@ -72,7 +72,7 @@
"max": "Max",
"avg": "Średni",
"median": "Mediana",
"std_dev": "Standard Deviation",
"std_dev": "Odchylenie standardowe",
"histogram": "Histogram",
"range": "Zakres",
"percent_empty": "Procent pustych",
@ -84,12 +84,12 @@
"earliest_date": "Najwcześniejsza data",
"latest_date": "Ostatnia data",
"date_range": "Zakres dat",
"month_range": "Month Range",
"month_range": "Zakres Miesięczny",
"checked": "Zaznaczono",
"unchecked": "Niezaznaczono",
"percent_checked": "Percent Checked",
"percent_unchecked": "Percent Unchecked",
"attachment_size": "Attachment Size",
"percent_checked": "Procent wybranych",
"percent_unchecked": "Procent niewybranych",
"attachment_size": "Rozmiar załącznika",
"none": "Brak"
},
"general": {
@ -268,9 +268,10 @@
"colour": "Kolor",
"use": "Użyj",
"stack": "Stos",
"ipAddress": "IP Address"
"ipAddress": "Adres IP"
},
"objects": {
"files": "files",
"owner": "Właściciel",
"member": "Członek",
"day": "Dzień",
@ -384,6 +385,9 @@
"isNotNull": "Nie jest null"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Zmień nazwę tabeli",
"renameWorkspace": "Zmień nazwę przestrzeni roboczej",
"renamingWorkspace": "Zmiana nazwy przestrzeni roboczej",
@ -513,8 +517,20 @@
"looksLikeThisStackIsEmpty": "Wygląda na to, że ten stos nie ma żadnych rekordów"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Upuść tutaj",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Nie zalecane",
"allowMetaWrite": "Allow Schema Edit",
"allowMetaWrite": "Zezwól na modyfikację schematu",
"allowDataWrite": "Allow Data Edit",
"selectView": "Wybierz widok",
"connectionDetails": "Szczegóły połączenia",
@ -648,7 +664,6 @@
"createdBy": "Stworzony przez",
"viewingAttachmentsOf": "Przeglądanie załączników",
"readOnly": "Tylko do odczytu",
"dropHere": "Upuść tutaj",
"createdOn": "Utworzony",
"notifyVia": "Powiadom przez",
"projName": "Nazwa projektu",
@ -840,7 +855,7 @@
"relationType": "Typ relacji",
"showThousandsSeparator": "Pokaż separator tysięcy",
"signUpForFree": "Załóż darmowe konto",
"coverImageField": "Cover image field",
"coverImageField": "Obraz okładki",
"fitImage": "Dopasuj zdjęcie",
"coverImageArea": "Obraz okładki"
},
@ -1084,7 +1099,7 @@
},
"toggleMobileMode": "Przełącz tryb mobilny",
"startCommenting": "Rozpocznij komentowanie",
"noCommentsYet": "No comments yet!",
"noCommentsYet": "Na razie brak komentarzy!",
"clearForm": "Wyczyść formularz",
"addFieldFromFormView": "Dodaj pole",
"selectAllFields": "Zaznacz wszystkie pola",
@ -1099,9 +1114,9 @@
"group": "Grupa"
},
"tooltip": {
"schemaChangeDisabled": "Schema editing is disabled for this data source.",
"typeNotAllowed": "This datatype is not allowed.",
"dataWriteOptionDisabled": "Data editing can only be disabled when 'Schema editing' is also disabled.",
"schemaChangeDisabled": "Modyfikacja schematu jest wyłączona dla tego źródła danych.",
"typeNotAllowed": "Ten typ danych jest niedozwolony.",
"dataWriteOptionDisabled": "Modyfikacja danych może być wyłączona tylko wtedy, gdy 'modyfikacja schematu' jest wyłączona",
"allowMetaWrite": "This option allows modification of database schema, including adding, altering, or deleting tables and columns. Use with caution, as changes may impact the structural integrity of your database.",
"allowDataWrite": "This option allows create, update, or delete of records within database tables. Ideal for administrative users need to change data directly.",
"reachedSourceLimit": "Ograniczenie do jednego źródła danych w tej chwili",
@ -1148,7 +1163,7 @@
"projName": "Wprowadź nazwę projektu.",
"selectGroupField": "Wybierz pole grupowania",
"selectGroupFieldNotFound": "Nie można znaleźć pola jednokrotnego wyboru. Najpierw je utwórz.",
"selectCoverImageField": "Select a cover image field",
"selectCoverImageField": "Wybierz pole obrazu okładki",
"selectGeoField": "Wybierz pole danych geograficznych",
"notSelected": "-nie wybrano-",
"selectGeoFieldNotFound": "Nie można znaleźć pola danych geograficznych. Najpierw je utwórz.",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Zoptymalizowane zapytanie jest włączone",
"lookupNonBtWarning": "Pole wyszukiwania nie jest obsługiwane dla relacji innych niż 'Należy do'",
"invalidTime": "Nieprawidłowy Czas",
"linkColumnClearNotSupportedYet": "Nie masz jeszcze żadnych obsługiwanych linków do pola Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Nie można znaleźć rekordu",
"invalidPhoneNumber": "Nieprawidłowy numer telefonu",
"pageSizeChanged": "Zmieniono rozmiar strony",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "Żadne rekordy nie są powiązane",
"noLinkedRecords": "Brak powiązanych rekordów",
"recordsLinked": "rekordy powiązane",
"acceptOnlyValid": "Akceptuje tylko",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Utwórz osobiste tokeny API do użytku w automatyzacji lub zewnętrznych aplikacjach.",
"selectFieldToSort": "Wybierz pole do sortowania",
"selectFieldToGroup": "Wybierz pole do grupowania",

23
packages/nc-gui/lang/pt.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Dia",
@ -384,6 +385,9 @@
"isNotNull": "não é nulo"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -648,7 +664,6 @@
"createdBy": "Criado por",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Criado em",
"notifyVia": "Notificar via.",
"projName": "Nome do Projeto",
@ -941,7 +956,7 @@
"addFilter": "Adicionar filtro",
"share": "Compartilhado",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "Desativar a base compartilhada",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

25
packages/nc-gui/lang/pt_BR.json

@ -271,6 +271,7 @@
"ipAddress": "IP Address"
},
"objects": {
"files": "files",
"owner": "Owner",
"member": "Member",
"day": "Dia",
@ -384,6 +385,9 @@
"isNotNull": "não é nulo"
},
"title": {
"webcam": "Webcam",
"uploadViaUrl": "Upload via URL",
"localFiles": "Local Files",
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
@ -513,6 +517,18 @@
"looksLikeThisStackIsEmpty": "Looks like this stack does not have any records"
},
"labels": {
"toUpload": "to upload",
"dragFilesHere": "drag files here",
"browseFiles": "browse files",
"clickTo": "Click to",
"allowAccessToYourCamera": "Please allow access to your camera",
"openFile": "Open file",
"enterValidUrl": "Enter a valid URL to upload files",
"addFilesFromUrl": "Add files from URL",
"uploading": "Uploading",
"dropHere": "Drop here",
"addMore": "Add more",
"clearAllFiles": "Clear all files",
"notRecommended": "Not recommended",
"allowMetaWrite": "Allow Schema Edit",
"allowDataWrite": "Allow Data Edit",
@ -605,7 +621,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"enableRichText": "Enable rich text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -648,7 +664,6 @@
"createdBy": "Criado por",
"viewingAttachmentsOf": "Viewing Attachments of",
"readOnly": "Readonly",
"dropHere": "Drop here",
"createdOn": "Created On",
"notifyVia": "Notificar por",
"projName": "Nome do Projeto",
@ -941,7 +956,7 @@
"addFilter": "Adicionar filtro",
"share": "Compartilhar",
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"addSubGroup": "New Subgroup",
"shareBase": {
"label": "Share Base",
"disable": "Desativar a base compartilhada",
@ -1211,7 +1226,7 @@
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"linkColumnClearNotSupportedYet": "You don't have any supported links for {type}",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed",
@ -1285,7 +1300,7 @@
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"acceptOnlyValid": "Accepts only",
"acceptOnlyValid": "Accept only valid {type}",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",

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

Loading…
Cancel
Save