Browse Source

Merge pull request #8517 from nocodb/develop

pull/8518/head 0.207.2
github-actions[bot] 2 months ago committed by GitHub
parent
commit
03d79fc0b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      package.json
  2. 8
      packages/nc-gui/assets/style.scss
  3. 13
      packages/nc-gui/components/cell/DatePicker.vue
  4. 13
      packages/nc-gui/components/cell/DateTimePicker.vue
  5. 13
      packages/nc-gui/components/cell/TimePicker.vue
  6. 13
      packages/nc-gui/components/cell/YearPicker.vue
  7. 3
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  8. 657
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  9. 107
      packages/nc-gui/components/dashboard/settings/Modal.vue
  10. 2
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  11. 53
      packages/nc-gui/components/dlg/ProjectAudit.vue
  12. 95
      packages/nc-gui/components/nc/DateWeekSelector.vue
  13. 16
      packages/nc-gui/components/nc/MonthYearSelector.vue
  14. 2
      packages/nc-gui/components/nc/Select.vue
  15. 6
      packages/nc-gui/components/project/AllTables.vue
  16. 4
      packages/nc-gui/components/project/View.vue
  17. 19
      packages/nc-gui/components/smartsheet/Form.vue
  18. 13
      packages/nc-gui/components/smartsheet/Toolbar.vue
  19. 4
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  20. 4
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  21. 10
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  22. 4
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  23. 8
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  24. 159
      packages/nc-gui/components/smartsheet/calendar/YearView/Month.vue
  25. 37
      packages/nc-gui/components/smartsheet/calendar/YearView/index.vue
  26. 7
      packages/nc-gui/components/smartsheet/calendar/index.vue
  27. 66
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  28. 4
      packages/nc-gui/components/smartsheet/form/field-settings.vue
  29. 2
      packages/nc-gui/components/smartsheet/grid/Table.vue
  30. 2
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  31. 15
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.ts
  32. 23
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Mode.vue
  33. 13
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Range.vue
  34. 29
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  35. 13
      packages/nc-gui/components/virtual-cell/Lookup.vue
  36. 4
      packages/nc-gui/composables/useCalendarViewStore.ts
  37. 14
      packages/nc-gui/composables/useCommandPalette/index.ts
  38. 21
      packages/nc-gui/composables/useFormViewStore.ts
  39. 33
      packages/nc-gui/composables/useSharedFormViewStore.ts
  40. 2
      packages/nc-gui/composables/useUndoRedo.ts
  41. 5
      packages/nc-gui/lang/en.json
  42. 2
      packages/nc-gui/lang/es.json
  43. 210
      packages/nc-gui/lang/fr.json
  44. 4
      packages/nc-gui/lang/it.json
  45. 4
      packages/nc-gui/lang/ko.json
  46. 162
      packages/nc-gui/lang/pl.json
  47. 24
      packages/nc-gui/lang/ru.json
  48. 4
      packages/nc-gui/lang/zh-Hans.json
  49. 38
      packages/nc-gui/package.json
  50. 1
      packages/nc-gui/utils/browserUtils.ts
  51. 31
      packages/nc-gui/utils/formValidations.ts
  52. 15
      packages/nc-gui/windi.config.ts
  53. 133
      packages/noco-docs/docs/020.getting-started/050.self-hosted/020.environment-variables.md
  54. 11
      packages/noco-docs/docs/070.fields/040.field-types/050.custom-types/010.attachment.md
  55. 8
      packages/noco-docs/docs/100.data-sources/010.data-source-overview.md
  56. 29
      packages/noco-docs/docs/100.data-sources/020.connect-to-data-source.md
  57. 18
      packages/noco-docs/docs/100.data-sources/030.sync-with-data-source.md
  58. 69
      packages/noco-docs/docs/100.data-sources/040.actions-on-data-sources.md
  59. BIN
      packages/noco-docs/static/img/v2/data-source/data-source-1.png
  60. BIN
      packages/noco-docs/static/img/v2/data-source/data-source-2.png
  61. BIN
      packages/noco-docs/static/img/v2/data-source/data-source-audit.png
  62. BIN
      packages/noco-docs/static/img/v2/data-source/data-source-edit.png
  63. BIN
      packages/noco-docs/static/img/v2/data-source/data-source-erd.png
  64. BIN
      packages/noco-docs/static/img/v2/data-source/data-source-hide.png
  65. BIN
      packages/noco-docs/static/img/v2/data-source/data-source-meta-sync-1.png
  66. BIN
      packages/noco-docs/static/img/v2/data-source/data-source-meta-sync-2.png
  67. BIN
      packages/noco-docs/static/img/v2/data-source/data-source-remove.png
  68. BIN
      packages/noco-docs/static/img/v2/data-source/data-source-uiacl.png
  69. BIN
      packages/noco-docs/static/img/v2/data-source/ds-connect-1.png
  70. BIN
      packages/noco-docs/static/img/v2/data-source/ds-connect-2.png
  71. BIN
      packages/noco-docs/static/img/v2/data-source/ds-connect-3.png
  72. BIN
      packages/noco-docs/static/img/v2/data-source/ds-connect-4.png
  73. 6
      packages/nocodb-sdk/package.json
  74. 11
      packages/nocodb-sdk/src/lib/formulaHelpers.ts
  75. 45
      packages/nocodb/Dockerfile
  76. 26
      packages/nocodb/Dockerfile.local
  77. 22
      packages/nocodb/docker/litestream.yml
  78. 35
      packages/nocodb/docker/start-litestream.sh
  79. 2
      packages/nocodb/docker/start-local.sh
  80. 96
      packages/nocodb/litestream/Dockerfile
  81. 32
      packages/nocodb/package.json
  82. 6
      packages/nocodb/src/db/BaseModelSqlv2.ts
  83. 1
      packages/nocodb/src/interface/Jobs.ts
  84. 76
      packages/nocodb/src/models/Source.ts
  85. 28
      packages/nocodb/src/modules/jobs/jobs/health-check.processor.ts
  86. 37
      packages/nocodb/src/modules/jobs/redis/jobs.service.ts
  87. 136
      packages/nocodb/src/services/command-palette.service.ts
  88. 1
      packages/nocodb/src/services/org-users.service.ts
  89. 1
      packages/nocodb/src/utils/acl.ts
  90. 6
      packages/nocodb/src/utils/emailUtils.ts
  91. 1
      packages/nocodb/src/version-upgrader/ncProjectConfigUpgrader.ts
  92. 3479
      pnpm-lock.yaml
  93. 4
      scripts/pkg-executable/package.json
  94. 1
      scripts/upgradeNcGui.js
  95. 4
      tests/playwright/pages/Dashboard/Calendar/CalendarWeekDateTime.ts
  96. 4
      tests/playwright/pages/Dashboard/ProjectView/Metadata.ts
  97. 3
      tests/playwright/pages/Dashboard/ProjectView/index.ts
  98. 30
      tests/playwright/pages/Dashboard/Settings/DataSources.ts
  99. 4
      tests/playwright/pages/Dashboard/TreeView.ts
  100. 6
      tests/playwright/tests/db/features/erd.spec.ts
  101. Some files were not shown because too many files have changed in this diff Show More

5
package.json

@ -46,7 +46,7 @@
},
"pnpm": {
"overrides": {
"vue": "3.3.13",
"vue": "latest",
"typescript": "latest",
"ajv@<6.12.3": ">=6.12.3",
"node.extend@<1.1.7": ">=1.1.7",
@ -56,7 +56,8 @@
"axios@>=0.8.1 <0.28.0": ">=0.28.0",
"ip@<1.1.9": ">=1.1.9",
"ip@=2.0.0": ">=2.0.1",
"xml2js@<0.5.0": ">=0.5.0"
"xml2js@<0.5.0": ">=0.5.0",
"ufo": ">=1.5.3"
}
}
}

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

@ -19,6 +19,10 @@ body {
--navbar-bg: #fafafa;
--navbar-border: #e0e0e0;
--nc-grid-bg: #fdfdfd;
--ant-primary-color-hover: #5c85ff !important;
--ant-primary-color-active: #3366ff !important;
--ant-primary-color-outline: rgba(51, 102, 255, 0.24) !important;
}
::-moz-selection {
@ -810,6 +814,6 @@ svg.nc-virtual-cell-icon {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
}
.text-nowrap{
.text-nowrap {
text-wrap: nowrap;
}
}

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

@ -205,7 +205,18 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
// To prevent event listener on non active cell
if (!active.value) return
if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey || !isGrid.value || isExpandedForm.value || isEditColumn.value) return
if (
e.altKey ||
e.ctrlKey ||
e.shiftKey ||
e.metaKey ||
!isGrid.value ||
isExpandedForm.value ||
isEditColumn.value ||
isExpandedFormOpen()
) {
return
}
switch (e.key) {
case ';':

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

@ -269,7 +269,18 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
// To prevent event listener on non active cell
if (!active.value) return
if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey || !isGrid.value || isExpandedForm.value || isEditColumn.value) return
if (
e.altKey ||
e.ctrlKey ||
e.shiftKey ||
e.metaKey ||
!isGrid.value ||
isExpandedForm.value ||
isEditColumn.value ||
isExpandedFormOpen()
) {
return
}
switch (e.key) {
case ';':

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

@ -185,7 +185,18 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
// To prevent event listener on non active cell
if (!active.value) return
if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey || !isGrid.value || isExpandedForm.value || isEditColumn.value) return
if (
e.altKey ||
e.ctrlKey ||
e.shiftKey ||
e.metaKey ||
!isGrid.value ||
isExpandedForm.value ||
isEditColumn.value ||
isExpandedFormOpen()
) {
return
}
switch (e.key) {
case ';':

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

@ -176,7 +176,18 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
// To prevent event listener on non active cell
if (!active.value) return
if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey || !isGrid.value || isExpandedForm.value || isEditColumn.value) return
if (
e.altKey ||
e.ctrlKey ||
e.shiftKey ||
e.metaKey ||
!isGrid.value ||
isExpandedForm.value ||
isEditColumn.value ||
isExpandedFormOpen()
) {
return
}
switch (e.key) {
case ';':

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

@ -331,6 +331,7 @@ const deleteTable = () => {
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: baseRole })"
:data-testid="`sidebar-table-rename-${table.title}`"
class="nc-table-rename"
@click="openRenameTableDialog(table, base.sources[sourceIndex].id)"
>
<div v-e="['c:table:rename']" class="flex gap-2 items-center">
@ -358,7 +359,7 @@ const deleteTable = () => {
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: baseRole })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50"
class="!text-red-500 !hover:bg-red-50 nc-table-delete"
@click="deleteTable"
>
<div v-e="['c:table:delete']" class="flex gap-2 items-center">

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

@ -18,8 +18,6 @@ const vReload = useVModel(props, 'reload', emits)
const { $api, $e } = useNuxtApp()
const { t } = useI18n()
const basesStore = useBases()
const { loadProject } = basesStore
const { isDataSourceLimitReached } = storeToRefs(basesStore)
@ -27,6 +25,8 @@ const { isDataSourceLimitReached } = storeToRefs(basesStore)
const baseStore = useBase()
const { base } = storeToRefs(baseStore)
const { isUIAllowed } = useRoles()
const { projectPageTab } = storeToRefs(useConfigStore())
const { refreshCommandPalette } = useCommandPalette()
@ -42,6 +42,44 @@ const isReloading = ref(false)
const isDeleteBaseModalOpen = ref(false)
const toBeDeletedBase = ref<SourceType | undefined>()
async function updateIfSourceOrderIsNullOrDuplicate() {
const sourceOrderSet = new Set()
let hasNullOrDuplicates = false
// Check if sources.value contains null or duplicate order
for (const source of sources.value) {
if (source.order === null || sourceOrderSet.has(source.order)) {
hasNullOrDuplicates = true
break
}
sourceOrderSet.add(source.order)
}
if (!hasNullOrDuplicates) return
// update the local state
sources.value = sources.value.map((source, i) => {
return {
...source,
order: i + 1,
}
})
try {
await Promise.all(
sources.value.map(async (source) => {
await $api.source.update(source.base_id as string, source.id as string, {
id: source.id,
base_id: source.base_id,
order: source.order,
})
}),
)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
async function loadBases(changed?: boolean) {
try {
if (changed) refreshCommandPalette()
@ -53,6 +91,7 @@ async function loadBases(changed?: boolean) {
if (baseList.list && baseList.list.length) {
sources.value = baseList.list
}
await updateIfSourceOrderIsNullOrDuplicate()
} catch (e) {
console.error(e)
} finally {
@ -90,7 +129,7 @@ const deleteBase = async () => {
refreshCommandPalette()
}
}
const toggleBase = async (source: BaseType, state: boolean) => {
const toggleBase = async (source: SourceType, state: boolean) => {
try {
if (!state && sources.value.filter((src) => src.enabled).length < 2) {
message.info('There should be at least one enabled source!')
@ -116,20 +155,26 @@ const moveBase = async (e: any) => {
// sources list is mutated so we have to get the new index and mirror it to backend
const source = sources.value[e.newIndex]
if (source) {
if (!source.order) {
// empty update call to reorder sources (migration)
await $api.source.update(source.base_id as string, source.id as string, {
id: source.id,
base_id: source.base_id,
})
message.info(t('info.basesMigrated'))
let nextOrder: number
// set new order value based on the new order of the items
if (sources.value.length - 1 === e.newIndex) {
// If moving to the end, set nextOrder greater than the maximum order in the list
nextOrder = Math.max(...sources.value.map((item) => item?.order ?? 0)) + 1
} else {
await $api.source.update(source.base_id as string, source.id as string, {
id: source.id,
base_id: source.base_id,
order: e.newIndex + 1,
})
nextOrder =
(parseFloat(String(sources.value[e.newIndex - 1]?.order ?? 0)) +
parseFloat(String(sources.value[e.newIndex + 1]?.order ?? 0))) /
2
}
const _nextOrder = !isNaN(Number(nextOrder)) ? nextOrder : e.oldIndex
await $api.source.update(source.base_id as string, source.id as string, {
id: source.id,
base_id: source.base_id,
order: _nextOrder,
})
}
await loadProject(base.value.id as string, true)
await loadBases()
@ -210,400 +255,239 @@ const isNewBaseModalOpen = computed({
},
})
const isErdModalOpen = computed({
get: () => {
return [DataSourcesSubTab.ERD].includes(vState.value as any)
},
set: (val) => {
if (!val) {
vState.value = ''
}
},
})
const isMetaDataModal = computed({
get: () => {
return [DataSourcesSubTab.Metadata].includes(vState.value as any)
},
set: (val) => {
if (!val) {
vState.value = ''
}
},
})
const isUIAclModalOpen = computed({
get: () => {
return [DataSourcesSubTab.UIAcl].includes(vState.value as any)
},
set: (val) => {
if (!val) {
vState.value = ''
}
},
})
const isBaseAuditModalOpen = computed({
get: () => {
return [DataSourcesSubTab.Audit].includes(vState.value as any)
},
set: (val) => {
if (!val) {
vState.value = ''
}
},
})
const isEditBaseModalOpen = computed({
get: () => {
return [DataSourcesSubTab.Edit].includes(vState.value as any)
},
set: (val) => {
if (!val) {
vState.value = ''
}
},
})
const activeSource = ref<SourceType>(null)
const openedTab = ref('erd')
</script>
<template>
<div class="flex flex-row w-full h-full nc-data-sources-view">
<div class="flex flex-col w-full overflow-auto">
<div class="flex flex-row w-full justify-end mt-6.5 mb-2">
<NcButton
v-if="!isDataSourceLimitReached"
size="large"
class="z-10 !px-2"
type="primary"
@click="vState = DataSourcesSubTab.New"
>
<div class="flex flex-row items-center w-full gap-x-1">
<component :is="iconMap.plus" />
<div class="flex">{{ $t('activity.newSource') }}</div>
</div>
</NcButton>
</div>
<div
class="overflow-y-auto nc-scrollbar-md"
:style="{
maxHeight: 'calc(100vh - 200px)',
}"
<div class="flex flex-col h-full">
<div class="px-4 py-2 flex justify-between">
<a-breadcrumb separator=">" class="w-full cursor-pointer font-weight-bold">
<a-breadcrumb-item @click="activeSource = null">
<a class="!no-underline">Data Sources</a>
</a-breadcrumb-item>
<a-breadcrumb-item v-if="activeSource">
<span class="capitalize">{{ activeSource.alias || 'Default Source' }}</span>
</a-breadcrumb-item>
</a-breadcrumb>
<NcButton
v-if="!isDataSourceLimitReached && !activeSource && isUIAllowed('sourceCreate')"
size="large"
class="z-10 !px-2"
type="primary"
@click="vState = DataSourcesSubTab.New"
>
<div class="ds-table-head">
<div class="ds-table-row">
<div class="ds-table-col ds-table-enabled cursor-pointer">{{ $t('general.visibility') }}</div>
<div class="ds-table-col ds-table-name">{{ $t('general.name') }}</div>
<div class="ds-table-col ds-table-type">{{ $t('general.type') }}</div>
<div class="ds-table-col ds-table-actions -ml-13">{{ $t('labels.actions') }}</div>
<div class="ds-table-col ds-table-crud"></div>
</div>
<div class="flex flex-row items-center w-full gap-x-1">
<component :is="iconMap.plus" />
<div class="flex">{{ $t('activity.newSource') }}</div>
</div>
<div class="ds-table-body">
<Draggable :list="sources" item-key="id" handle=".ds-table-handle" @end="moveBase">
<template #header>
<div v-if="sources[0]" class="ds-table-row border-gray-200">
<div class="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1">
<div v-if="sources.length > 2" class="ds-table-handle" />
<a-tooltip>
<template #title>
<template v-if="sources[0].enabled">{{ $t('activity.hideInUI') }}</template>
<template v-else>{{ $t('activity.showInUI') }}</template>
</template>
<a-switch
:checked="sources[0].enabled ? true : false"
class="cursor-pointer"
size="small"
@change="toggleBase(sources[0], $event)"
/>
</a-tooltip>
</div>
</div>
<div class="ds-table-col ds-table-name font-medium">
<div class="flex items-center gap-1">
<!-- <GeneralBaseLogo :base-type="sources[0].type" /> -->
{{ $t('general.default') }}
</div>
</div>
</NcButton>
</div>
<div data-testid="nc-settings-datasources" class="flex flex-row w-full nc-data-sources-view flex-grow min-h-0">
<template v-if="activeSource">
<NcTabs v-model:activeKey="openedTab" class="nc-source-tab w-full">
<a-tab-pane key="erd">
<template #tab>
<div class="tab" data-testid="nc-erd-tab">
<div>{{ $t('title.erdView') }}</div>
</div>
</template>
<div class="h-full pt-4">
<LazyDashboardSettingsErd class="h-full overflow-auto" :source-id="activeSource.id" :show-all-columns="false" />
</div>
</a-tab-pane>
<a-tab-pane v-if="sources && activeSource === sources[0]" key="audit">
<template #tab>
<div class="tab" data-testid="nc-audit-tab">
<div>{{ $t('title.auditLogs') }}</div>
</div>
</template>
<div class="p-4 h-full overflow-auto">
<LazyDashboardSettingsBaseAudit :source-id="activeSource.id" />
</div>
</a-tab-pane>
<a-tab-pane v-if="!activeSource.is_meta && !activeSource.is_local" key="audit">
<template #tab>
<div class="tab" data-testid="nc-connection-tab">
<div>{{ $t('labels.connectionDetails') }}</div>
</div>
</template>
<div class="p-6 mt-4 h-full overflow-auto">
<LazyDashboardSettingsDataSourcesEditBase
class="w-600px"
:source-id="activeSource.id"
@source-updated="loadBases(true)"
@close="activeSource = null"
/>
</div>
</a-tab-pane>
<div class="ds-table-col ds-table-type">
<div class="flex items-center gap-1">-</div>
</div>
<a-tab-pane key="acl">
<template #tab>
<div class="tab" data-testid="nc-acl-tab">
<div>{{ $t('labels.uiAcl') }}</div>
</div>
</template>
<div class="ds-table-col ds-table-actions">
<div class="flex items-center gap-2">
<NcTooltip v-if="!sources[0].is_meta && !sources[0].is_local">
<template #title>
{{ $t('tooltip.metaSync') }}
</template>
<NcButton
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-meta-sync"
size="small"
@click="baseAction(sources[0].id, DataSourcesSubTab.Metadata)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="sync" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('title.relations') }}
</template>
<NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-erd"
@click="baseAction(sources[0].id, DataSourcesSubTab.ERD)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('labels.uiAcl') }}
</template>
<NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-ui-acl"
@click="baseAction(sources[0].id, DataSourcesSubTab.UIAcl)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('title.audit') }}
</template>
<NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-audit"
@click="baseAction(sources[0].id, DataSourcesSubTab.Audit)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="book" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
</div>
</div>
<div class="ds-table-col ds-table-crud">
<NcButton
v-if="!sources[0].is_meta && !sources[0].is_local"
size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.Edit)"
>
<GeneralIcon icon="edit" class="text-gray-600" />
</NcButton>
</div>
<div class="pt-4 h-full overflow-auto">
<LazyDashboardSettingsUIAcl :source-id="activeSource.id" />
</div>
</a-tab-pane>
<a-tab-pane v-if="!activeSource.is_meta && !activeSource.is_local" key="meta-sync">
<template #tab>
<div class="tab" data-testid="nc-meta-sync-tab">
<div>{{ $t('labels.metaSync') }}</div>
</div>
</template>
<template #item="{ element: source, index }">
<div v-if="index !== 0" class="ds-table-row border-gray-200">
<div class="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1">
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<a-tooltip>
<template #title>
<template v-if="source.enabled">{{ $t('activity.hideInUI') }}</template>
<template v-else>{{ $t('activity.showInUI') }}</template>
</template>
<a-switch
:checked="source.enabled ? true : false"
class="cursor-pointer"
size="small"
@change="toggleBase(source, $event)"
/>
</a-tooltip>
<div class="pt-4 h-full overflow-auto">
<LazyDashboardSettingsMetadata :source-id="activeSource.id" @source-synced="loadBases(true)" />
</div>
</a-tab-pane>
</NcTabs>
</template>
<div v-else class="flex flex-col w-full overflow-auto mt-1">
<div
class="overflow-y-auto nc-scrollbar-md"
:style="{
maxHeight: 'calc(100vh - 200px)',
}"
>
<div class="ds-table-head">
<div class="ds-table-row">
<div class="ds-table-col ds-table-enabled cursor-pointer">{{ $t('general.visibility') }}</div>
<div class="ds-table-col ds-table-name">{{ $t('general.name') }}</div>
<div class="ds-table-col ds-table-type">{{ $t('general.type') }}</div>
<div class="ds-table-col ds-table-actions">{{ $t('labels.actions') }}</div>
</div>
</div>
<div class="ds-table-body">
<Draggable :list="sources" item-key="id" handle=".ds-table-handle" @end="moveBase">
<template #header>
<div v-if="sources[0]" class="ds-table-row border-gray-200 cursor-pointer" @click="activeSource = sources[0]">
<div class="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1" @click.stop>
<div v-if="sources.length > 2" class="ds-table-handle" />
<a-tooltip>
<template #title>
<template v-if="sources[0].enabled">{{ $t('activity.hideInUI') }}</template>
<template v-else>{{ $t('activity.showInUI') }}</template>
</template>
<a-switch
:checked="sources[0].enabled ? true : false"
class="cursor-pointer"
size="small"
@change="toggleBase(sources[0], $event)"
/>
</a-tooltip>
</div>
</div>
<div class="ds-table-col ds-table-name font-medium">
<div class="flex items-center gap-1">
<!-- <GeneralBaseLogo :base-type="sources[0].type" /> -->
{{ $t('general.default') }}
</div>
</div>
</div>
<div class="ds-table-col ds-table-name font-medium w-full">
<div v-if="source.is_meta || source.is_local">-</div>
<span v-else class="truncate">
{{ source.is_meta || source.is_local ? $t('general.base') : source.alias }}
</span>
</div>
<div class="ds-table-col ds-table-type">
<div class="flex items-center gap-2">
<GeneralBaseLogo :source-type="source.type" />
<span class="text-gray-700 capitalize">{{ source.type }}</span>
<div class="ds-table-col ds-table-type">
<div class="flex items-center gap-1">-</div>
</div>
<div class="ds-table-col ds-table-actions">
<NcButton
v-if="!sources[0].is_meta && !sources[0].is_local"
size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text"
@click.stop="baseAction(sources[0].id, DataSourcesSubTab.Edit)"
>
<GeneralIcon icon="edit" class="text-gray-600" />
</NcButton>
</div>
</div>
</template>
<template #item="{ element: source, index }">
<div v-if="index !== 0" class="ds-table-row border-gray-200 cursor-pointer" @click="activeSource = source">
<div class="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1" @click.stop>
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<a-tooltip>
<template #title>
<template v-if="source.enabled">{{ $t('activity.hideInUI') }}</template>
<template v-else>{{ $t('activity.showInUI') }}</template>
</template>
<a-switch
:checked="source.enabled ? true : false"
class="cursor-pointer"
size="small"
@change="toggleBase(source, $event)"
/>
</a-tooltip>
</div>
</div>
<div class="ds-table-col ds-table-name font-medium w-full">
<div v-if="source.is_meta || source.is_local" class="h-8 w-1">-</div>
<span v-else class="truncate">
{{ source.is_meta || source.is_local ? $t('general.base') : source.alias }}
</span>
</div>
<div class="ds-table-col ds-table-actions">
<div class="flex items-center gap-2">
<NcTooltip>
<template #title>
{{ $t('title.relations') }}
</template>
<NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-erd"
@click="baseAction(source.id, DataSourcesSubTab.ERD)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('labels.uiAcl') }}
</template>
<NcButton
size="small"
type="text"
class="nc-action-btn cursor-pointer outline-0"
data-testid="nc-data-sources-view-ui-acl"
@click="baseAction(source.id, DataSourcesSubTab.UIAcl)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip v-if="!isEeUI">
<template #title>
{{ $t('title.audit') }}
</template>
<NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-audit"
@click="baseAction(source.id, DataSourcesSubTab.Audit)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="book" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<div class="ds-table-col ds-table-type">
<div class="flex items-center gap-2">
<GeneralBaseLogo :source-type="source.type" />
<span class="text-gray-700 capitalize">{{ source.type }}</span>
</div>
</div>
<div class="ds-table-col justify-end gap-x-1 ds-table-actions">
<NcTooltip>
<template #title>
{{ $t('tooltip.metaSync') }}
{{ $t('general.remove') }}
</template>
<NcButton
v-if="!source.is_meta && !source.is_local"
size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text"
data-testid="nc-data-sources-view-meta-sync"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(source.id, DataSourcesSubTab.Metadata)"
@click.stop="openDeleteBase(source)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="sync" class="group-hover:text-accent" />
</div>
<GeneralIcon icon="delete" class="text-red-500" />
</NcButton>
</NcTooltip>
</div>
</div>
<div class="ds-table-col ds-table-crud justify-end gap-x-1">
<NcTooltip>
<template #title>
{{ $t('general.edit') }}
</template>
<NcButton
v-if="!source.is_meta && !source.is_local"
size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text"
@click="baseAction(source.id, DataSourcesSubTab.Edit)"
>
<GeneralIcon icon="edit" class="text-gray-600" />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('general.remove') }}
</template>
<NcButton
v-if="!source.is_meta && !source.is_local"
size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text"
@click="openDeleteBase(source)"
>
<GeneralIcon icon="delete" class="text-red-500" />
</NcButton>
</NcTooltip>
</div>
</div>
</template>
</Draggable>
</div>
</div>
<LazyDashboardSettingsDataSourcesCreateBase
v-model:open="isNewBaseModalOpen"
:connection-type="clientType"
@source-created="loadBases(true)"
/>
<GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]">
<LazyDashboardSettingsErd :source-id="activeBaseId" :show-all-columns="false" />
</div>
</GeneralModal>
<GeneralModal v-model:visible="isMetaDataModal" size="medium">
<div class="p-6">
<LazyDashboardSettingsMetadata :source-id="activeBaseId" @source-synced="loadBases(true)" />
</div>
</GeneralModal>
<GeneralModal v-model:visible="isUIAclModalOpen" class="!w-[60rem]">
<div class="p-6">
<LazyDashboardSettingsUIAcl :source-id="activeBaseId" />
</div>
</GeneralModal>
<GeneralModal v-model:visible="isEditBaseModalOpen" closable :mask-closable="false" size="medium">
<div class="p-6">
<LazyDashboardSettingsDataSourcesEditBase
:source-id="activeBaseId"
@source-updated="loadBases(true)"
@close="isEditBaseModalOpen = false"
/>
</div>
</GeneralModal>
<GeneralModal v-model:visible="isBaseAuditModalOpen" class="!w-[70rem]">
<div class="p-6">
<LazyDashboardSettingsBaseAudit :source-id="activeBaseId" @close="isBaseAuditModalOpen = false" />
</template>
</Draggable>
</div>
</div>
</GeneralModal>
<GeneralDeleteModal
v-model:visible="isDeleteBaseModalOpen"
:entity-name="$t('general.datasource')"
:on-delete="deleteBase"
:delete-label="$t('general.remove')"
>
<template #entity-preview>
<div v-if="toBeDeletedBase" class="flex flex-row items-center py-2 px-3.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralBaseLogo :source-type="toBeDeletedBase.type" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-3"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ toBeDeletedBase.alias }}
<LazyDashboardSettingsDataSourcesCreateBase
v-model:open="isNewBaseModalOpen"
:connection-type="clientType"
@source-created="loadBases(true)"
/>
<GeneralDeleteModal
v-model:visible="isDeleteBaseModalOpen"
:entity-name="$t('general.datasource')"
:on-delete="deleteBase"
:delete-label="$t('general.remove')"
>
<template #entity-preview>
<div v-if="toBeDeletedBase" class="flex flex-row items-center py-2 px-3.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralBaseLogo :source-type="toBeDeletedBase.type" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-3"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ toBeDeletedBase.alias }}
</div>
</div>
</div>
</template>
</GeneralDeleteModal>
</template>
</GeneralDeleteModal>
</div>
</div>
</div>
</template>
<style>
<style scoped lang="scss">
.ds-table-head {
@apply flex items-center border-0 text-gray-500;
}
@ -613,7 +497,7 @@ const isEditBaseModalOpen = computed({
}
.ds-table-row {
@apply grid grid-cols-20 border-b border-gray-100 w-full h-full;
@apply grid grid-cols-18 border-b border-gray-100 w-full h-full;
}
.ds-table-col {
@ -633,11 +517,7 @@ const isEditBaseModalOpen = computed({
}
.ds-table-actions {
@apply col-span-5 flex w-full justify-end;
}
.ds-table-crud {
@apply col-span-2;
@apply col-span-5 flex w-full justify-center;
}
.ds-table-col:last-child {
@ -647,4 +527,15 @@ const isEditBaseModalOpen = computed({
.ds-table-handle {
@apply cursor-pointer justify-self-start mr-2 w-[16px];
}
.ds-table-body .ds-table-row:hover {
@apply bg-gray-50/60;
}
:deep(.ant-tabs-content),
:deep(.ant-tabs) {
@apply !h-full;
}
:deep(.ant-tabs-content-holder) {
@apply !min-h-0 !flex-shrink;
}
</style>

107
packages/nc-gui/components/dashboard/settings/Modal.vue

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { FunctionalComponent, SVGAttributes } from 'vue'
import Misc from './Misc.vue'
import DataSources from '~/components/dashboard/settings/DataSources.vue'
interface Props {
modelValue?: boolean
@ -39,6 +40,8 @@ const vDataState = useVModel(props, 'dataSourcesState', emits)
const baseId = toRef(props, 'baseId')
const { isUIAllowed } = useRoles()
provide(ProjectIdInj, baseId)
const { $e } = useNuxtApp()
@ -77,21 +80,6 @@ const tabsInfo: TabGroup = {
// $e('c:settings:team-auth')
// },
// },
// dataSources: {
// // Data Sources
// title: 'Data Sources',
// icon: iconMap.datasource,
// subTabs: {
// dataSources: {
// title: 'Data Sources',
// body: DataSources,
// },
// },
// onClick: () => {
// vDataState.value = ''
// $e('c:settings:data-sources')
// },
// },
// audit: {
// // Audit
// title: t('title.audit'),
@ -123,6 +111,22 @@ const tabsInfo: TabGroup = {
$e('c:settings:base-settings')
},
},
dataSources: {
// Data Sources
title: 'Data Sources',
icon: iconMap.database,
subTabs: {
dataSources: {
title: 'Data Sources',
body: DataSources,
},
},
onClick: () => {
vDataState.value = ''
$e('c:settings:data-sources')
},
},
}
const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
@ -154,6 +158,7 @@ watch(
:footer="null"
width="max(90vw, 600px)"
:closable="false"
class="!top-50px !bottom-50px"
wrap-class-name="nc-modal-settings"
@cancel="emits('update:modelValue', false)"
>
@ -173,24 +178,30 @@ watch(
</a-button>
</div>
<a-layout class="mt-3 h-[75vh] overflow-y-auto flex">
<a-layout class="mt-3 overflow-y-auto flex">
<!-- Side tabs -->
<a-layout-sider>
<a-menu v-model:selected-keys="selectedTabKeys" class="tabs-menu h-full" :open-keys="[]">
<a-menu-item v-for="(tab, key) of tabsInfo" :key="key" class="active:(!ring-0) hover:(!bg-primary !bg-opacity-25)">
<div class="flex items-center space-x-2" @click="tab.onClick">
<component :is="tab.icon" />
<div class="select-none">
{{ tab.title }}
<template v-for="(tab, key) of tabsInfo" :key="key">
<a-menu-item
v-if="key !== 'dataSources' || isUIAllowed('sourceCreate')"
:key="key"
class="active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
>
<div class="flex items-center space-x-2" @click="tab.onClick">
<component :is="tab.icon" />
<div class="select-none">
{{ tab.title }}
</div>
</div>
</div>
</a-menu-item>
</a-menu-item>
</template>
</a-menu>
</a-layout-sider>
<!-- Sub Tabs -->
<a-layout-content class="h-auto px-4 scrollbar-thumb-gray-500">
<a-layout-content class="h-auto h-80vh px-4 scrollbar-thumb-gray-500">
<a-menu
v-if="selectedTabKeys[0] !== 'dataSources'"
v-model:selectedKeys="selectedSubTabKeys"
@ -206,51 +217,19 @@ watch(
{{ tab.title }}
</a-menu-item>
</a-menu>
<div v-else>
<div class="flex items-center">
<a-breadcrumb class="w-full cursor-pointer">
<a-breadcrumb-item v-if="vDataState !== ''" @click="vDataState = ''">
<a class="!no-underline">Data Sources</a>
</a-breadcrumb-item>
<a-breadcrumb-item v-else @click="vDataState = ''">Data Sources</a-breadcrumb-item>
<a-breadcrumb-item v-if="vDataState !== ''">{{ vDataState }}</a-breadcrumb-item>
</a-breadcrumb>
<div v-if="vDataState === ''" class="flex flex-row justify-end items-center w-full gap-1">
<a-button
v-if="!isDataSourceLimitReached"
type="primary"
class="self-start !rounded-md nc-btn-new-datasource"
@click="vDataState = DataSourcesSubTab.New"
>
<div v-if="vDataState === ''" class="flex items-center gap-2 font-light">
<component :is="iconMap.plusCircle" class="group-hover:text-accent" />
New
</div>
</a-button>
<!-- Reload -->
<a-button
v-e="['a:proj-meta:data-sources:reload']"
type="text"
class="self-start !rounded-md nc-btn-metasync-reload"
@click="dataSourcesReload = true"
>
<div class="flex items-center gap-2 text-gray-600 font-light">
<component :is="iconMap.reload" :class="{ 'animate-infinite animate-spin !text-success': dataSourcesReload }" />
{{ $t('general.reload') }}
</div>
</a-button>
</div>
</div>
<a-divider style="margin: 10px 0" />
</div>
<div class="h-[600px]">
<div
class="overflow-auto"
:class="{
'h-full': selectedSubTabKeys[0] === 'dataSources',
}"
>
<component
:is="selectedSubTab?.body"
v-if="selectedSubTabKeys[0] === 'dataSources'"
v-model:state="vDataState"
v-model:reload="dataSourcesReload"
class="px-2 pb-2"
class="px-2 pb-2 h-full"
:data-testid="`nc-settings-subtab-${selectedSubTab.key}`"
:base-id="baseId"
/>

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

@ -131,7 +131,7 @@ const toggleSelectAll = (role: Role) => {
<template>
<div class="flex flex-row w-full items-center justify-center">
<div class="flex flex-col w-[900px]">
<div class="flex flex-col">
<NcTooltip class="mb-4 first-letter:capital font-bold max-w-100 truncate" show-on-truncate-only>
<template #title>{{ base.title }}</template>
<span> UI ACL : {{ base.title }} </span>

53
packages/nc-gui/components/dlg/ProjectAudit.vue

@ -0,0 +1,53 @@
<script lang="ts" setup>
const props = defineProps<{
baseId: string
sourceId: string
modelValue: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const isOpen = useVModel(props, 'modelValue', emit)
const activeSourceId = computed(() => props.sourceId)
const { openedProject: base } = storeToRefs(useBases())
const { baseTables } = storeToRefs(useTablesStore())
const { loadProjectTables } = useTablesStore()
const isLoading = ref(true)
const { getMeta } = useMetas()
const baseId = computed(() => props.baseId || base.value?.id)
onMounted(async () => {
if (baseId.value && baseTables.value.get(baseId.value)) {
return (isLoading.value = false)
}
try {
await loadProjectTables(baseId.value!)
await Promise.all(
baseTables.value.get(baseId.value!)!.map(async (table) => {
await getMeta(table.id!, false, false, baseId.value!)
}),
)
} catch (e) {
message.error('Failed to load tables/bases. Please try again later.')
console.error(e)
} finally {
isLoading.value = false
}
})
</script>
<template>
<GeneralModal v-model:visible="isOpen" size="large" class="!w-[70rem]">
<div class="p-6">
<DashboardSettingsBaseAudit v-if="!isLoading" :source-id="activeSourceId" :base-id="baseId" :show-all-columns="false" />
</div>
</GeneralModal>
</template>

95
packages/nc-gui/components/nc/DateWeekSelector.vue

@ -2,15 +2,12 @@
import dayjs from 'dayjs'
interface Props {
size?: 'medium' | 'large' | 'small'
size?: 'medium'
selectedDate?: dayjs.Dayjs | null
isDisabled?: boolean
pageDate?: dayjs.Dayjs
activeDates?: Array<dayjs.Dayjs>
isMondayFirst?: boolean
disablePagination?: boolean
isWeekPicker?: boolean
disableHeader?: boolean
hideCalendar?: boolean
selectedWeek?: {
start: dayjs.Dayjs
@ -19,19 +16,16 @@ interface Props {
}
const props = withDefaults(defineProps<Props>(), {
size: 'large',
size: 'medium',
selectedDate: null,
isDisabled: false,
isMondayFirst: true,
disablePagination: false,
pageDate: dayjs(),
isWeekPicker: false,
disableHeader: false,
activeDates: [] as Array<dayjs.Dayjs>,
selectedWeek: null,
hideCalendar: false,
})
const emit = defineEmits(['change', 'dblClick', 'update:selectedDate', 'update:pageDate', 'update:selectedWeek'])
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:selectedWeek'])
// Page date is the date we use to manage which month/date that is currently being displayed
const pageDate = useVModel(props, 'pageDate', emit)
@ -139,27 +133,12 @@ const paginate = (action: 'next' | 'prev') => {
pageDate.value = newDate
emit('update:pageDate', newDate)
}
const emitDblClick = (date: dayjs.Dayjs) => {
emit('dblClick', date)
}
</script>
<template>
<div
:class="{
'gap-1': size === 'small',
}"
class="flex flex-col"
>
<div
v-if="!disableHeader"
:class="{
'!justify-center': disablePagination,
}"
class="flex justify-between border-b-1 px-3 py-0.5 nc-date-week-header items-center"
>
<NcTooltip v-if="!disablePagination">
<div class="flex flex-col">
<div class="flex justify-between border-b-1 px-3 py-0.5 nc-date-week-header items-center">
<NcTooltip hide-on-click>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.arrowLeft" class="h-4 w-4" />
</NcButton>
@ -168,16 +147,9 @@ const emitDblClick = (date: dayjs.Dayjs) => {
</template>
</NcTooltip>
<span
:class="{
'text-xs': size === 'small',
'text-sm': size === 'medium',
}"
class="text-gray-700 font-semibold"
>{{ currentMonthYear }}</span
>
<span class="text-gray-700 text-sm font-semibold">{{ currentMonthYear }}</span>
<NcTooltip v-if="!disablePagination">
<NcTooltip hide-on-click>
<NcButton class="!border-0" data-testid="nc-calendar-next-btn" size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.arrowRight" class="h-4 w-4" />
</NcButton>
@ -186,42 +158,16 @@ const emitDblClick = (date: dayjs.Dayjs) => {
</template>
</NcTooltip>
</div>
<div
v-if="!hideCalendar"
:class="{
'rounded-lg': size === 'small',
'rounded-y-xl': size !== 'small',
}"
class="max-w-[320px]"
>
<div
:class="{
'gap-1 px-3.5': size === 'medium',
'gap-2': size === 'large',
'px-2 !rounded-t-lg': size === 'small',
'rounded-t-xl': size !== 'small',
}"
class="flex py-1 flex-row nc-date-week-header border-gray-200 justify-between"
>
<div v-if="!hideCalendar" class="max-w-[320px] rounded-y-xl">
<div class="flex py-1 gap-1 px-2.5 rounded-t-xl flex-row border-gray-200 justify-between">
<span
v-for="(day, index) in days"
:key="index"
:class="{
'w-9 h-9': size === 'large',
'w-8 h-8': size === 'medium',
'text-[10px]': size === 'small',
}"
class="flex items-center uppercase font-medium justify-center text-gray-500"
class="flex w-8 h-8 items-center uppercase font-medium justify-center text-gray-500"
>{{ day[0] }}</span
>
</div>
<div
:class="{
'gap-2 pt-2': size === 'large',
'gap-1 py-1 px-3.5': size === 'medium',
}"
class="grid nc-date-week-grid-wrapper grid-cols-7"
>
<div class="grid gap-1 py-1 px-2.5 nc-date-week-grid-wrapper grid-cols-7">
<span
v-for="(date, index) in dates"
:key="index"
@ -235,29 +181,20 @@ const emitDblClick = (date: dayjs.Dayjs) => {
'text-gray-400': !isDateInCurrentMonth(date),
'nc-selected-week-start': isSameDate(date, selectedWeek?.start),
'nc-selected-week-end': isSameDate(date, selectedWeek?.end),
'rounded-md text-brand-500 !font-semibold nc-calendar-today ':
isSameDate(date, dayjs()) && isDateInCurrentMonth(date),
'h-9 w-9': size === 'large',
'rounded-md text-brand-500 !font-semibold nc-calendar-today': isSameDate(date, dayjs()) && isDateInCurrentMonth(date),
'text-gray-500': date.get('day') === 0 || date.get('day') === 6,
'h-8 w-8': size === 'medium',
'h-6 w-6 text-[10px]': size === 'small',
}"
class="px-1 py-1 relative border-1 font-medium flex items-center cursor-pointer justify-center"
class="px-1 h-8 w-8 py-1 relative transition border-1 font-medium flex text-gray-700 items-center cursor-pointer justify-center"
data-testid="nc-calendar-date"
@dblclick="emitDblClick(date)"
@click="handleSelectDate(date)"
>
<span
v-if="isActiveDate(date)"
:class="{
'h-2 w-2': size === 'large',
'h-1.5 w-1.5': size === 'medium',
'h-1.25 w-1.25 top-0.5 right-0.5': size === 'small',
'top-1 right-1': size !== 'small',
'!border-white': isSelectedDate(date),
'!border-brand-50': isSameDate(date, dayjs()),
}"
class="absolute z-2 border-1 rounded-full border-white bg-brand-500"
class="absolute top-1 transition right-1 h-1.5 w-1.5 z-2 border-1 rounded-full border-white bg-brand-500"
></span>
<span class="z-2">
{{ date.get('date') }}
@ -270,7 +207,7 @@ const emitDblClick = (date: dayjs.Dayjs) => {
<style lang="scss" scoped>
.nc-selected-week {
@apply relative;
@apply relative transition-all;
}
.nc-selected-week:before {

16
packages/nc-gui/components/nc/MonthYearSelector.vue

@ -3,18 +3,14 @@ import dayjs from 'dayjs'
interface Props {
selectedDate?: dayjs.Dayjs | null
isDisabled?: boolean
pageDate?: dayjs.Dayjs
isYearPicker?: boolean
hideHeader?: boolean
hideCalendar?: boolean
}
const props = withDefaults(defineProps<Props>(), {
selectedDate: null,
isDisabled: false,
pageDate: dayjs(),
hideHeader: false,
isYearPicker: false,
hideCalendar: false,
})
@ -90,9 +86,9 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
<template>
<div class="flex flex-col">
<div v-if="!hideHeader" class="flex px-2 border-b-1 py-0.5 justify-between items-center">
<div class="flex px-2 border-b-1 py-0.5 justify-between items-center">
<div class="flex">
<NcTooltip>
<NcTooltip hide-on-click>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.arrowLeft" class="h-4 w-4" />
</NcButton>
@ -106,7 +102,7 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
isYearPicker ? dayjs(selectedDate).year() : dayjs(pageDate).format('YYYY')
}}</span>
<div class="flex">
<NcTooltip>
<NcTooltip hide-on-click>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.arrowRight" class="h-4 w-4" />
</NcButton>
@ -123,10 +119,10 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
v-for="(month, id) in months"
:key="id"
:class="{
'!bg-gray-200 !text-brand-500 !font-bold ': isMonthSelected(month),
'!bg-gray-200 !text-brand-900 !font-bold ': isMonthSelected(month),
'!text-brand-500': dayjs().isSame(month, 'month'),
}"
class="h-9 rounded-lg flex items-center font-medium justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer"
class="h-8 rounded-lg flex items-center transition-all font-medium justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-700 cursor-pointer"
@click="selectedDate = month"
>
{{ month.format('MMM') }}
@ -140,7 +136,7 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
'!bg-gray-200 !text-brand-500 !font-bold ': compareYear(year, selectedDate),
'!text-brand-500': dayjs().isSame(year, 'year'),
}"
class="h-9 rounded-lg flex items-center font-medium justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer"
class="h-8 rounded-lg flex items-center transition-all font-medium justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer"
@click="selectedDate = year"
>
{{ year.format('YYYY') }}

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

@ -84,7 +84,7 @@ const onChange = (value: string) => {
height: fit-content;
.ant-select-selector {
box-shadow: 0px 5px 3px -2px rgba(0, 0, 0, 0.02), 0px 3px 1px -2px rgba(0, 0, 0, 0.06);
@apply border-1 border-gray-200 rounded-lg !px-3;
@apply border-1 border-gray-200 rounded-lg;
}
.ant-select-selection-item {

6
packages/nc-gui/components/project/AllTables.vue

@ -105,7 +105,7 @@ const onCreateBaseClick = () => {
<GeneralIcon icon="download" />
<div class="label">{{ $t('activity.import') }} {{ $t('general.data') }}</div>
</div>
<component :is="isDataSourceLimitReached ? NcTooltip : 'div'" v-if="isUIAllowed('sourceCreate')">
<!-- <component :is="isDataSourceLimitReached ? NcTooltip : 'div'" v-if="isUIAllowed('sourceCreate')">
<template #title>
<div>
{{ $t('tooltip.reachedSourceLimit') }}
@ -124,7 +124,7 @@ const onCreateBaseClick = () => {
<GeneralIcon icon="dataSource" />
<div class="label">{{ $t('labels.connectDataSource') }}</div>
</div>
</component>
</component>-->
</div>
<div
v-if="base?.isLoading"
@ -191,7 +191,7 @@ const onCreateBaseClick = () => {
</div>
<ProjectImportModal v-if="defaultBase" v-model:visible="isImportModalOpen" :source="defaultBase" />
<LazyDashboardSettingsDataSourcesCreateBase v-model:open="isNewBaseModalOpen" />
<!-- <LazyDashboardSettingsDataSourcesCreateBase v-model:open="isNewBaseModalOpen" />-->
</div>
</template>

4
packages/nc-gui/components/project/View.vue

@ -162,7 +162,7 @@ watch(
</template>
<ProjectAccessSettings :base-id="currentBase?.id" />
</a-tab-pane>
<a-tab-pane v-if="isUIAllowed('sourceCreate')" key="data-source">
<!-- <a-tab-pane v-if="isUIAllowed('sourceCreate')" key="data-source">
<template #tab>
<div class="tab-title" data-testid="proj-view-tab__data-sources">
<GeneralIcon icon="database" />
@ -180,7 +180,7 @@ watch(
</div>
</template>
<DashboardSettingsDataSources v-model:state="baseSettingsState" />
</a-tab-pane>
</a-tab-pane>-->
</a-tabs>
</div>
</div>

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

@ -1301,7 +1301,7 @@ useEventListener(
<Pane min-size="30" size="50" class="nc-form-right-splitpane-item p-4 flex flex-col space-y-4 !min-h-200px">
<div class="flex flex-wrap justify-between items-center gap-2">
<div class="flex gap-3">
<div class="text-base font-bold text-gray-900">
<div class="text-base font-bold text-gray-600">
{{ $t('objects.viewType.form') }} {{ $t('objects.fields') }}
</div>
<NcBadge color="border-gray-200">
@ -1633,7 +1633,7 @@ useEventListener(
<LazyCellRichText
v-if="!isLocked && isEditable"
v-model:value="formViewData.success_msg"
class="nc-form-after-submit-msg"
class="nc-form-after-submit-msg editable"
is-form-field
:hidden-bubble-menu-options="hiddenBubbleMenuOptions"
data-testid="nc-form-after-submit-msg"
@ -1833,6 +1833,13 @@ useEventListener(
.form-meta-input {
.nc-textarea-rich-editor {
@apply pl-3 pr-4 !rounded-lg !text-sm border-1 border-gray-200 focus-within:border-brand-500;
&:hover {
@apply border-brand-400;
}
&:focus-within {
@apply shadow-selected;
}
}
&.nc-form-input-label .nc-textarea-rich-editor {
@ -1848,6 +1855,14 @@ useEventListener(
.nc-form-after-submit-msg {
.nc-textarea-rich-editor {
@apply pl-1 pr-2 pt-2 pb-1 !rounded-lg !text-sm border-1 border-gray-200 focus-within:border-brand-500;
&:hover {
@apply border-brand-400;
}
&:focus-within {
@apply shadow-selected;
}
.ProseMirror {
min-height: 5rem;
max-height: 7.5rem !important;

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

@ -22,9 +22,12 @@ const { allowCSVDownload } = useSharedView()
<template>
<div
v-if="!isMobileMode"
v-if="!isMobileMode || isCalendar"
ref="containerRef"
class="nc-table-toolbar relative px-3 xs:(px-1) flex gap-2 items-center border-b border-gray-200 overflow-hidden xs:(min-h-14) min-h-9 max-h-9 z-7"
:class="{
'px-4': isMobileMode,
}"
class="nc-table-toolbar relative px-3 flex gap-2 items-center border-b border-gray-200 overflow-hidden xs:(min-h-14) min-h-9 max-h-9 z-7"
>
<template v-if="isViewsLoading">
<a-skeleton-input :active="true" class="!w-44 !h-4 ml-2 !rounded overflow-hidden" />
@ -76,10 +79,12 @@ const { allowCSVDownload } = useSharedView()
'w-full': isMobileMode,
}"
/>
<div v-if="isCalendar && isMobileMode" class="flex-1 pointer-events-none" />
<LazySmartsheetToolbarCalendarMode v-if="isCalendar && !isTab" :tab="isTab" />
<LazySmartsheetToolbarFieldsMenu v-if="isCalendar" :show-system-fields="false" />
<LazySmartsheetToolbarColumnFilterMenu v-if="isCalendar" />
<LazySmartsheetToolbarFieldsMenu v-if="isCalendar && !isMobileMode" :show-system-fields="false" />
<LazySmartsheetToolbarColumnFilterMenu v-if="isCalendar && !isMobileMode" />
</template>
</div>
</template>

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

@ -889,7 +889,7 @@ watch(
:class="{
'!border-brand-500': hour.isSame(selectedTime),
}"
class="flex w-full border-l-gray-100 h-13 nc-calendar-day-hour relative border-1 group hover:bg-gray-50 border-white border-b-gray-100"
class="flex w-full border-l-gray-100 h-13 transition nc-calendar-day-hour relative border-1 group hover:bg-gray-50 border-white border-b-gray-100"
data-testid="nc-calendar-day-hour"
@click="selectHour(hour)"
@dblclick="newRecord(hour)"
@ -999,7 +999,7 @@ watch(
:data-testid="`nc-calendar-day-record-${record.row[displayField!.title!]}`"
:data-unique-id="record.rowMeta.id"
:style="record.rowMeta.style"
class="absolute draggable-record group cursor-pointer pointer-events-auto"
class="absolute draggable-record transition group cursor-pointer pointer-events-auto"
@mousedown="dragStart($event, record)"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id as string"

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

@ -700,7 +700,7 @@ const addRecord = (date: dayjs.Dayjs) => {
'!text-gray-400': !isDayInPagedMonth(day),
'!bg-gray-50': day.get('day') === 0 || day.get('day') === 6,
}"
class="text-right relative group last:border-r-0 text-sm h-full border-r-1 border-b-1 border-gray-200 font-medium hover:bg-gray-50 text-gray-800 bg-white"
class="text-right relative group last:border-r-0 transition text-sm h-full border-r-1 border-b-1 border-gray-200 font-medium hover:bg-gray-50 text-gray-800 bg-white"
data-testid="nc-calendar-month-day"
@click="selectDate(day)"
@dblclick="addRecord(day)"
@ -812,7 +812,7 @@ const addRecord = (date: dayjs.Dayjs) => {
...record.rowMeta.style,
zIndex: record.rowMeta.id === draggingId ? 100 : 0,
}"
class="absolute group draggable-record cursor-pointer pointer-events-auto"
class="absolute group draggable-record transition cursor-pointer pointer-events-auto"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id"
@mousedown.stop="dragStart($event, record)"

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

@ -315,25 +315,29 @@ onClickOutside(searchRef, toggleSearch)
<template>
<NcTooltip
:class="{
'!right-26 top-[-36px]': showSideMenu && isMobileMode,
'right-2': !showSideMenu,
'right-74': showSideMenu,
}"
class="absolute transition-all ease-in-out top-2 z-30"
class="absolute transition-all ease-in-out z-9 top-2"
hide-on-click
>
<template #title> {{ $t('activity.toggleSidebar') }}</template>
<NcButton v-if="!isMobileMode" data-testid="nc-calendar-side-bar-btn" size="small" type="secondary" @click="toggleSideMenu">
<NcButton data-testid="nc-calendar-side-bar-btn" size="small" type="secondary" @click="toggleSideMenu">
<component :is="iconMap.sidebar" class="h-4 w-4 text-gray-600 transition-all" />
</NcButton>
</NcTooltip>
<div
:class="{
'!min-w-[100svw]': props.visible && isMobileMode,
'!w-0 hidden': !props.visible,
'nc-calendar-side-menu-open block !min-w-[288px]': props.visible,
}"
class="h-full relative border-l-1 border-gray-200 transition-all"
data-testid="nc-calendar-side-menu"
>
<div class="flex flex-col">
<div class="flex min-w-[288px] flex-col">
<NcDateWeekSelector
v-if="activeCalendarView === ('day' as const)"
v-model:active-dates="activeDates"

4
packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue

@ -551,14 +551,14 @@ const addRecord = (date: dayjs.Dayjs) => {
<template>
<div class="flex relative flex-col prevent-select" data-testid="nc-calendar-week-view" @drop="dropEvent">
<div class="flex">
<div class="flex h-6">
<div
v-for="(date, weekIndex) in weekDates"
:key="weekIndex"
:class="{
'!border-brand-500 !border-b-gray-200': dayjs(date).isSame(selectedDate, 'day'),
}"
class="w-1/7 cursor-pointer text-center font-regular uppercase text-xs text-gray-500 w-full py-1 border-gray-200 border-l-gray-50 border-t-gray-50 last:border-r-0 border-1 bg-gray-50"
class="w-1/7 cursor-pointer text-center text-[10px] font-semibold leading-4 flex items-center justify-center uppercase text-gray-500 w-full py-1 border-gray-200 border-l-gray-50 border-t-gray-50 last:border-r-0 border-1 bg-gray-50"
@click="selectDate(date)"
@dblclick="addRecord(date)"
>

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

@ -877,14 +877,14 @@ watch(
data-testid="nc-calendar-week-view"
@drop="dropEvent"
>
<div class="flex sticky h-7.1 z-1 top-0 pl-16 bg-gray-50 w-full">
<div class="flex sticky h-6 z-1 top-0 pl-16 bg-gray-50 w-full">
<div
v-for="date in datesHours"
:key="date[0].toISOString()"
:class="{
'text-brand-500': date[0].isSame(dayjs(), 'date'),
}"
class="w-1/7 text-center font-regular uppercase text-xs text-gray-500 w-full py-1 border-gray-200 last:border-r-0 border-b-1 border-l-1 border-r-0 bg-gray-50"
class="w-1/7 text-center text-[10px] font-semibold leading-4 flex items-center justify-center uppercase text-gray-500 w-full py-1 border-gray-200 last:border-r-0 border-b-1 border-l-1 border-r-0 bg-gray-50"
>
{{ dayjs(date[0]).format('DD ddd') }}
</div>
@ -907,7 +907,7 @@ watch(
'border-1 !border-brand-500 bg-gray-50': hour.isSame(selectedTime, 'hour'),
'!bg-gray-50': hour.get('day') === 0 || hour.get('day') === 6,
}"
class="text-center relative h-13 text-sm text-gray-500 w-full hover:bg-gray-50 py-1 border-transparent border-1 border-x-gray-100 border-t-gray-100 border-l-gray-200"
class="text-center relative transition h-13 text-sm text-gray-500 w-full hover:bg-gray-50 py-1 border-transparent border-1 border-x-gray-100 border-t-gray-100 border-l-gray-200"
data-testid="nc-calendar-week-hour"
@dblclick="addRecord(hour)"
@click="
@ -944,7 +944,7 @@ watch(
:data-testid="`nc-calendar-week-record-${record.row[displayField!.title!]}`"
:data-unique-id="record.rowMeta!.id"
:style="record.rowMeta!.style "
class="absolute draggable-record w-1/7 group cursor-pointer pointer-events-auto"
class="absolute transition draggable-record w-1/7 group cursor-pointer pointer-events-auto"
@mousedown.stop="dragStart($event, record)"
@mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id"

159
packages/nc-gui/components/smartsheet/calendar/YearView/Month.vue

@ -0,0 +1,159 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
interface Props {
size?: 'medium' | 'small'
selectedDate?: dayjs.Dayjs | null
pageDate?: dayjs.Dayjs
activeDates?: Array<dayjs.Dayjs>
isMondayFirst?: boolean
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium',
selectedDate: null,
isMondayFirst: true,
pageDate: dayjs(),
activeDates: [] as Array<dayjs.Dayjs>,
})
const emit = defineEmits(['dblClick', 'update:selectedDate', 'update:pageDate'])
// Page date is the date we use to manage which month/date that is currently being displayed
const pageDate = useVModel(props, 'pageDate', emit)
const selectedDate = useVModel(props, 'selectedDate', emit)
const activeDates = useVModel(props, 'activeDates', emit)
const days = computed(() => {
if (props.isMondayFirst) {
return ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
} else {
return ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
}
})
const currentMonthYear = computed(() => {
return dayjs(pageDate.value).format('MMMM')
})
// Generates all dates should be displayed in the calendar
// Includes all blank days at the start and end of the month
const dates = computed(() => {
const startOfMonth = dayjs(pageDate.value).startOf('month')
const dayOffset = +props.isMondayFirst
const firstDayOfWeek = startOfMonth.day()
const startDay = startOfMonth.subtract((firstDayOfWeek - dayOffset + 7) % 7, 'day')
const datesArray = []
for (let i = 0; i < 42; i++) {
datesArray.push(startDay.add(i, 'day'))
}
return datesArray
})
// Used to check if two dates are the same
const isSameDate = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
if (!date1 || !date2) return false
return date1.isSame(date2, 'day')
}
// Used in DatePicker for checking if the date is currently selected
const isSelectedDate = (dObj: dayjs.Dayjs) => {
if (!selectedDate.value) return false
const propDate = dayjs(selectedDate.value)
return props.selectedDate ? isSameDate(propDate, dObj) : false
}
const isDayInPagedMonth = (date: dayjs.Dayjs) => {
return date.month() === dayjs(pageDate.value).month()
}
// Since we are using the same component for week picker and date picker we need to handle the date selection differently
const handleSelectDate = (date: dayjs.Dayjs) => {
if (!isDayInPagedMonth(date)) {
pageDate.value = date
emit('update:pageDate', date)
}
selectedDate.value = date
emit('update:selectedDate', date)
}
// Used to check if a date is in the current month
const isDateInCurrentMonth = (date: dayjs.Dayjs) => {
return date.month() === dayjs(pageDate.value).month()
}
// Used to Check if an event is in the date
const isActiveDate = (date: dayjs.Dayjs) => {
return activeDates.value.some((d) => isSameDate(d, date))
}
const emitDblClick = (date: dayjs.Dayjs) => {
emit('dblClick', date)
}
</script>
<template>
<div>
<div class="flex justify-center px-2 nc-date-week-header text-gray-700 text-sm py-2 font-semibold items-center">
{{ currentMonthYear }}
</div>
<div
:class="{
'rounded-lg': size === 'small',
'rounded-y-xl': size !== 'small',
}"
class="max-w-[320px]"
>
<div class="px-2.5">
<div class="flex border-b-1 justify-between gap-0.5 border-gray-200">
<span
v-for="(day, index) in days"
:key="index"
:class="{
'w-8 h-8 text-sm': size === 'medium',
'text-xs w-6 h-6': size === 'small',
}"
class="flex items-center uppercase py-1 font-medium justify-center text-gray-500"
>{{ day[0] }}</span
>
</div>
</div>
<div class="grid gap-x-0.5 gap-y-2 px-2.5 py-1 nc-date-week-grid-wrapper grid-cols-7">
<span
v-for="(date, index) in dates"
:key="index"
:class="{
'bg-gray-300 border-1 !font-semibold': isSelectedDate(date) && isDayInPagedMonth(date),
'hover:(border-1 border-gray-200 bg-gray-100)': !isSelectedDate(date),
'text-gray-400': !isDateInCurrentMonth(date),
'text-brand-500 !font-semibold nc-calendar-today': isSameDate(date, dayjs()) && isDateInCurrentMonth(date),
'text-gray-500': date.get('day') === 0 || date.get('day') === 6,
'h-8 w-8 text-sm': size === 'medium',
'h-6 w-6 text-xs': size === 'small',
}"
class="px-1 py-1.5 relative rounded border-transparent transition border-1 font-medium flex text-gray-700 items-center cursor-pointer justify-center"
data-testid="nc-calendar-date"
@click="handleSelectDate(date)"
@dblclick="emitDblClick(date)"
>
<span
v-if="isActiveDate(date)"
:class="{
'h-1.25 w-1.25 top-0.5 right-0.5': size === 'small',
'!border-white': isSelectedDate(date),
'!border-brand-50': isSameDate(date, dayjs()),
}"
class="absolute z-2 h-1.5 top-1 right-1 w-1.5 transition border-1 rounded-full border-white bg-brand-500"
></span>
<span class="z-2">
{{ date.get('date') }}
</span>
</span>
</div>
</div>
</div>
</template>
<style lang="scss" scoped></style>

37
packages/nc-gui/components/smartsheet/calendar/YearView.vue → packages/nc-gui/components/smartsheet/calendar/YearView/index.vue

@ -13,17 +13,27 @@ const months = computed(() => {
const calendarContainer = ref<HTMLElement | null>(null)
const { width } = useWindowSize()
const { width } = useElementSize(calendarContainer)
const size = ref('small')
const size = ref<'small' | 'medium'>('small')
const cols = ref(4)
const handleResize = () => {
if (width.value < 1608) {
size.value = 'small'
} else if (width.value < 2000) {
if (width.value > 1250) {
size.value = 'medium'
cols.value = 4
} else if (width.value > 850) {
size.value = 'medium'
cols.value = 3
} else if (width.value > 680) {
size.value = 'small'
cols.value = 3
} else if (width.value > 375) {
size.value = 'small'
cols.value = 2
} else {
size.value = 'large'
size.value = 'medium'
cols.value = 1
}
}
@ -40,15 +50,19 @@ watch(width, handleResize)
</script>
<template>
<div ref="calendarContainer" class="overflow-auto flex my-2 justify-center nc-scrollbar-md">
<div ref="calendarContainer" class="overflow-auto flex my-2 transition-all justify-center nc-scrollbar-md">
<div
:class="{
'!gap-12': size === 'large',
'grid-cols-1': cols === 1,
'grid-cols-2': cols === 2,
'grid-cols-3': cols === 3,
'grid-cols-4': cols === 4,
'!gap-5': cols < 3 && size === 'small',
}"
class="grid grid-cols-4 justify-items-center gap-6 scale-1"
class="grid justify-items-center gap-8"
data-testid="nc-calendar-year-view"
>
<NcDateWeekSelector
<LazySmartsheetCalendarYearViewMonth
v-for="(_, index) in months"
:key="index"
v-model:active-dates="activeDates"
@ -57,7 +71,6 @@ watch(width, handleResize)
:size="size"
class="nc-year-view-calendar"
data-testid="nc-calendar-year-view-month-selector"
disable-pagination
@dbl-click="changeView"
/>
</div>
@ -67,7 +80,7 @@ watch(width, handleResize)
<style lang="scss" scoped>
.nc-year-view-calendar {
:deep(.nc-date-week-header) {
@apply border-gray-200;
@apply border-gray-200 h-8 py-2;
}
}
</style>

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

@ -161,12 +161,7 @@ reloadViewDataHook?.on(async (params: void | { shouldShowLoading?: boolean }) =>
</div>
</template>
</div>
<LazySmartsheetCalendarSideMenu
v-if="!isMobileMode"
:visible="showSideMenu"
@expand-record="expandRecord"
@new-record="newRecord"
/>
<LazySmartsheetCalendarSideMenu :visible="showSideMenu" @expand-record="expandRecord" @new-record="newRecord" />
</div>
<Suspense>

66
packages/nc-gui/components/smartsheet/column/FormulaOptions.vue

@ -102,36 +102,47 @@ const sortOrder: Record<string, number> = {
const suggestionsList = computed(() => {
const unsupportedFnList = sqlUi.value.getUnsupportedFnList()
return [
...availableFunctions
.filter((fn: string) => !unsupportedFnList.includes(fn))
.map((fn: string) => ({
return (
[
...availableFunctions.map((fn: string) => ({
text: `${fn}()`,
type: 'function',
description: formulas[fn].description,
syntax: formulas[fn].syntax,
examples: formulas[fn].examples,
docsUrl: formulas[fn].docsUrl,
unsupported: unsupportedFnList.includes(fn),
})),
...supportedColumns.value
.filter((c) => {
// skip system LTAR columns
if (c.uidt === UITypes.LinkToAnotherRecord && c.system) return false
// v1 logic? skip the current column
if (!column) return true
return column.value?.id !== c.id
})
.map((c: any) => ({
text: c.title,
type: 'column',
icon: getUIDTIcon(c.uidt),
uidt: c.uidt,
...supportedColumns.value
.filter((c) => {
// skip system LTAR columns
if (c.uidt === UITypes.LinkToAnotherRecord && c.system) return false
// v1 logic? skip the current column
if (!column) return true
return column.value?.id !== c.id
})
.map((c: any) => ({
text: c.title,
type: 'column',
icon: getUIDTIcon(c.uidt),
uidt: c.uidt,
})),
...availableBinOps.map((op: string) => ({
text: op,
type: 'op',
})),
...availableBinOps.map((op: string) => ({
text: op,
type: 'op',
})),
]
]
// move unsupported functions to the end
.sort((a: Record<string, any>, b: Record<string, any>) => {
if (a.unsupported && !b.unsupported) {
return 1
}
if (!a.unsupported && b.unsupported) {
return -1
}
return 0
})
)
})
// set default suggestion list
@ -214,6 +225,7 @@ function handleInput() {
function selectText() {
if (suggestion.value && selected.value > -1 && selected.value < suggestionsList.value.length) {
if (selected.value < suggestedFormulas.value.length) {
if (suggestedFormulas.value[selected.value].unsupported) return
appendText(suggestedFormulas.value[selected.value])
} else {
appendText(variableList.value[selected.value + suggestedFormulas.value.length])
@ -276,7 +288,7 @@ onMounted(() => {
<template>
<div class="formula-wrapper relative">
<div
v-if="suggestionPreviewed && suggestionPreviewed.type === 'function'"
v-if="suggestionPreviewed && !suggestionPreviewed.unsupported && suggestionPreviewed.type === 'function'"
class="absolute -left-91 w-84 top-0 bg-white z-10 pl-3 pt-3 border-1 shadow-md rounded-xl"
>
<div class="pr-3">
@ -357,20 +369,22 @@ onMounted(() => {
class="cursor-pointer !overflow-hidden hover:bg-gray-50"
:class="{
'!bg-gray-100': selected === index,
'cursor-not-allowed': item.unsupported,
}"
@click.prevent.stop="appendText(item)"
@click.prevent.stop="!item.unsupported && appendText(item)"
@mouseenter="suggestionPreviewed = item"
>
<a-list-item-meta>
<template #title>
<div class="flex items-center gap-x-1">
<div class="flex items-center gap-x-1" :class="{ 'text-gray-400': item.unsupported }">
<component :is="iconMap.function" v-if="item.type === 'function'" class="text-lg" />
<component :is="iconMap.calculator" v-if="item.type === 'op'" class="text-lg" />
<component :is="item.icon" v-if="item.type === 'column'" class="text-lg" />
<span class="prose-sm text-gray-600">{{ item.text }}</span>
<span class="prose-sm" :class="{ 'text-gray-600': !item.unsupported }">{{ item.text }}</span>
</div>
<div v-if="item.unsupported" class="ml-5 text-gray-400 text-xs">{{ $t('msg.formulaNotSupported') }}</div>
</template>
</a-list-item-meta>
</a-list-item>

4
packages/nc-gui/components/smartsheet/form/field-settings.vue

@ -64,7 +64,7 @@ const columnSupportsScanning = (elementType: UITypes) =>
<div v-if="isSelectTypeCol(activeField.uidt)" class="w-full flex items-start justify-between gap-3">
<div class="flex-1 max-w-[calc(100%_-_40px)]">
<div class="font-medium text-gray-800">{{ $t('labels.limitOptions') }}</div>
<div class="text-gray-500 mt-2">{{ $t('labels.limitOptionsSubtext') }}.</div>
<div class="text-gray-500 mt-1">{{ $t('labels.limitOptionsSubtext') }}.</div>
<div v-if="activeField.meta.isLimitOption" class="mt-3">
<LazySmartsheetFormLimitOptions
v-model:model-value="activeField.meta.limitOptions"
@ -95,7 +95,7 @@ const columnSupportsScanning = (elementType: UITypes) =>
v-if="isSelectTypeCol(activeField.uidt)"
class="nc-form-field-appearance-settings p-4 flex flex-col gap-4 border-b border-gray-200"
>
<div class="text-base font-bold">{{ $t('general.appearance') }}</div>
<div class="text-base font-bold text-gray-600">{{ $t('general.appearance') }}</div>
<div class="flex flex-col gap-6">
<!-- Select type field Options Layout -->
<div>

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

@ -2447,7 +2447,7 @@ onKeyStroke('ArrowDown', onDown)
:deep(.nc-cell-icon),
:deep(.nc-virtual-cell-icon) {
@apply !w-3.5 !h-3.5 !text-gray-500 !text-small;
@apply !w-3.5 !h-3.5 !text-small;
}
}

2
packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -220,7 +220,7 @@ const onClick = (e: Event) => {
<GeneralIcon
v-if="isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit')"
icon="arrowDown"
class="flex-none text-grey h-full text-grey cursor-pointer ml-1 group-hover:visible"
class="flex-none h-full cursor-pointer ml-1 group-hover:visible"
:class="{
visible: editColumnDropdown || isDropDownOpen,
invisible: !(editColumnDropdown || isDropDownOpen),

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

@ -17,7 +17,7 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
case RelationTypes.BELONGS_TO:
return { icon: iconMap.bt_solid }
case RelationTypes.ONE_TO_ONE:
return { icon: iconMap.oneToOneSolid, color: 'text-blue-500' }
return { icon: iconMap.oneToOneSolid, color: 'text-purple-500' }
}
break
case UITypes.SpecificDBType:
@ -36,6 +36,8 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
return { icon: iconMap.cellLookup, color: 'text-orange-500' }
case RelationTypes.BELONGS_TO:
return { icon: iconMap.cellLookup, color: 'text-blue-500' }
case RelationTypes.ONE_TO_ONE:
return { icon: iconMap.cellLookup, color: 'text-purple-500' }
}
return { icon: iconMap.cellLookup, color: 'text-grey' }
case UITypes.Rollup:
@ -46,6 +48,8 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
return { icon: iconMap.cellRollup, color: 'text-orange-500' }
case RelationTypes.BELONGS_TO:
return { icon: iconMap.cellRollup, color: 'text-blue-500' }
case RelationTypes.ONE_TO_ONE:
return { icon: iconMap.cellRollup, color: 'text-purple-500' }
}
return { icon: iconMap.cellRollup, color: 'text-grey' }
case UITypes.Count:
@ -76,15 +80,16 @@ export default defineComponent({
const column = computed(() => columnMeta.value ?? injectedColumn.value)
const { metas } = useMetas()
let relationColumn: ColumnType
return () => {
if (!column.value) return null
if (column && column.value) {
if (isMm(column.value) || isHm(column.value) || isBt(column.value) || isLookup(column.value) || isRollup(column.value)) {
const meta = inject(MetaInj, ref())
relationColumn = meta.value?.columns?.find(
if (isLookup(column.value) || isRollup(column.value)) {
relationColumn = metas.value?.[column.value.fk_model_id]?.columns?.find(
(c) => c.id === column.value?.colOptions?.fk_relation_column_id,
) as ColumnType
}
@ -92,7 +97,7 @@ export default defineComponent({
const { icon: Icon, color } = renderIcon(column.value, relationColumn)
return h(Icon, { class: `${color} mx-1 nc-virtual-cell-icon` })
return h(Icon, { class: `${color || 'text-grey'} mx-1 nc-virtual-cell-icon` })
}
},
})

23
packages/nc-gui/components/smartsheet/toolbar/Calendar/Mode.vue

@ -5,6 +5,8 @@ const props = defineProps<{
const { changeCalendarView, activeCalendarView } = useCalendarViewStoreOrThrow()
const isTab = computed(() => props.tab)
const highlightStyle = ref({ left: '0px' })
const setActiveCalendarMode = (mode: 'day' | 'week' | 'month' | 'year', event: MouseEvent) => {
@ -22,15 +24,19 @@ const updateHighlightPosition = () => {
})
}
onMounted(() => {
updateHighlightPosition()
})
watch(activeCalendarView, () => {
if (!props.tab) return
if (!isTab.value) return
updateHighlightPosition()
})
</script>
<template>
<div
v-if="props.tab"
v-if="isTab"
class="flex flex-row px-1 pointer-events-auto mx-3 mt-3 rounded-lg gap-x-0.5 nc-calendar-mode-tab"
data-testid="nc-calendar-view-mode"
>
@ -47,12 +53,12 @@ watch(activeCalendarView, () => {
</div>
</div>
<NcSelect v-else v-model:value="activeCalendarView" class="!w-22" data-testid="nc-calendar-view-mode" size="small">
<a-select-option v-for="option in ['day', 'week', 'month', 'year']" :key="option" :value="option" class="!h-7 !w-20">
<NcSelect v-else v-model:value="activeCalendarView" class="!w-21" data-testid="nc-calendar-view-mode" size="small">
<a-select-option v-for="option in ['day', 'week', 'month', 'year']" :key="option" :value="option" class="!h-7 !w-21">
<div class="flex gap-2 mt-0.5 items-center">
<NcTooltip class="truncate !capitalize flex-1 max-w-18" placement="top" show-on-truncate-only>
<NcTooltip class="!capitalize flex-1 max-w-21" placement="top" show-on-truncate-only>
<template #title>
<span class="capitalize">
<span class="capitalize min-w-21">
{{ option }}
</span>
</template>
@ -81,6 +87,11 @@ watch(activeCalendarView, () => {
@apply !text-[13px];
}
}
.nc-select.ant-select {
.ant-select-selector {
@apply !px-3;
}
}
.tab {
@apply flex items-center h-7 w-14 z-10 justify-center px-2 py-1 rounded-lg gap-x-1.5 text-gray-500 hover:text-black cursor-pointer transition-all duration-300 select-none;

13
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, UITypes, ViewTypes, isSystemColumn } from 'nocodb-sdk'
import type { SelectProps } from 'ant-design-vue'
const meta = inject(MetaInj, ref())
@ -119,10 +119,15 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
<template #overlay>
<div v-if="calendarRangeDropdown" class="w-98 space-y-6 rounded-2xl p-6" data-testid="nc-calendar-range-menu" @click.stop>
<div>
<div class="flex justify-between">
<div class="flex mb-3 justify-between">
<div class="flex items-center gap-3">
<component :is="iconMap.calendar" class="text-maroon-500 w-5 h-5" />
<span class="font-bold"> {{ `${$t('activity.calendar')} ${$t('activity.viewSettings')}` }}</span>
<GeneralViewIcon
:meta="{
type: ViewTypes.CALENDAR,
}"
class="w-6 h-6"
/>
<span class="font-bold text-base"> {{ `${$t('activity.calendar')} ${$t('activity.viewSettings')}` }}</span>
</div>
<a

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

@ -16,6 +16,8 @@ const customColumns = toRef(restProps, 'columns')
const meta = inject(MetaInj, ref())
const { metas } = useMetas()
const localValue = computed({
get: () => modelValue,
set: (val) => emit('update:modelValue', val),
@ -80,6 +82,31 @@ const filterOption = (input: string, option: any) => option.label.toLowerCase()?
if (!localValue.value && allowEmpty !== true) {
localValue.value = (options.value?.[0].value as string) || ''
}
const relationColor = {
[RelationTypes.BELONGS_TO]: 'text-blue-500',
[RelationTypes.ONE_TO_ONE]: 'text-purple-500',
[RelationTypes.HAS_MANY]: 'text-orange-500',
[RelationTypes.MANY_TO_MANY]: 'text-pink-500',
}
// extract colors for Lookup and Rollup columns
const colors = computed(() => {
return (
meta.value?.columns?.reduce((obj, col) => {
if ((col && isLookup(col)) || isRollup(col)) {
const relationColumn = metas.value?.[meta.value.id]?.columns?.find(
(c) => c.id === col.colOptions?.fk_relation_column_id,
) as ColumnType
if (relationColumn) {
obj[col.id] = relationColor[relationColumn.colOptions?.type as RelationTypes]
}
}
return obj
}, {}) || {}
)
})
</script>
<template>
@ -94,7 +121,7 @@ if (!localValue.value && allowEmpty !== true) {
<a-select-option v-for="option in options" :key="option.value" :label="option.label" :value="option.value">
<div class="flex items-center w-full justify-between w-full gap-2 max-w-50">
<div class="flex gap-1.5 flex-1 items-center truncate items-center h-full">
<component :is="option.icon" class="!w-3.5 !h-3.5 !mx-0 !text-gray-500" />
<component :is="option.icon" class="!w-3.5 !h-3.5 !mx-0" :class="colors[option.value] || '!text-gray-500'" />
<NcTooltip
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
class="max-w-[15rem] truncate select-none"

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

@ -20,13 +20,12 @@ const rowHeight = inject(RowHeightInj, ref(1) as any)
provide(RowHeightInj, providedHeightRef)
const relationColumn = computed(
() =>
meta.value?.columns?.find((c: ColumnType) => c.id === (column.value?.colOptions as LookupType)?.fk_relation_column_id) as
| (ColumnType & {
colOptions: LinkToAnotherRecordType | undefined
})
| undefined,
const relationColumn = computed(() =>
meta.value?.id
? metas.value[meta.value?.id]?.columns?.find(
(c: ColumnType) => c.id === (column.value?.colOptions as LookupType)?.fk_relation_column_id,
)
: undefined,
)
watch(

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

@ -35,6 +35,8 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const { isUIAllowed } = useRoles()
const { isMobileMode } = useGlobal()
const displayField = computed(() => meta.value?.columns?.find((c) => c.pv))
const activeCalendarView = ref<'month' | 'year' | 'day' | 'week'>()
@ -54,7 +56,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
const isCalendarMetaLoading = ref<boolean>(false)
const showSideMenu = ref(true)
const showSideMenu = ref(!isMobileMode.value)
const selectedDateRange = ref<{
start: dayjs.Dayjs

14
packages/nc-gui/composables/useCommandPalette/index.ts

@ -169,19 +169,11 @@ export const useCommandPalette = createSharedComposable(() => {
}
}
} else {
if (route.value.path.startsWith('/account')) {
if (activeScope.value.scope === 'account_settings') return
if (activeScope.value.scope === 'root') return
activeScope.value = { scope: 'account_settings', data: {} }
activeScope.value = { scope: 'root', data: {} }
loadScope()
} else {
if (activeScope.value.scope === 'root') return
activeScope.value = { scope: 'root', data: {} }
loadScope()
}
loadScope()
}
},
{ immediate: true, deep: true },

21
packages/nc-gui/composables/useFormViewStore.ts

@ -48,9 +48,24 @@ const [useProvideFormViewStore, useFormViewStore] = useInjectionState(
for (const column of visibleColumns.value) {
let rules: RuleObject[] = [
{
required: isRequired(column, column.required),
message: t('msg.error.fieldRequired'),
...(column.uidt === UITypes.Checkbox && isRequired(column, column.required) ? { type: 'enum', enum: [1, true] } : {}),
validator: (_rule: RuleObject, value: any) => {
return new Promise((resolve, reject) => {
if (isRequired(column, column.required)) {
if (typeof value === 'string') {
value = value.trim()
}
if (
(column.uidt === UITypes.Checkbox && !value) ||
(column.uidt !== UITypes.Checkbox && !requiredFieldValidatorFn(value))
) {
return reject(t('msg.error.fieldRequired'))
}
}
return resolve()
})
},
},
]

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

@ -194,23 +194,28 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
if (!formColumns.value) return rulesObj
for (const column of formColumns.value) {
let rules: RuleObject[] = []
rules.push({
validator: (_rule: RuleObject, value: any) => {
return new Promise((resolve, reject) => {
if (isRequired(column)) {
if (column.uidt === UITypes.Checkbox && !value) {
return reject(t('msg.error.fieldRequired'))
} else if (column.uidt !== UITypes.Checkbox)
if (value === null || !value?.length) {
let rules: RuleObject[] = [
{
validator: (_rule: RuleObject, value: any) => {
return new Promise((resolve, reject) => {
if (isRequired(column)) {
if (typeof value === 'string') {
value = value.trim()
}
if (
(column.uidt === UITypes.Checkbox && !value) ||
(column.uidt !== UITypes.Checkbox && !requiredFieldValidatorFn(value))
) {
return reject(t('msg.error.fieldRequired'))
}
}
return resolve()
})
}
return resolve()
})
},
},
})
]
const additionalRules = extractFieldValidator(parseProp(column.meta).validators ?? [], column)
rules = [...rules, ...additionalRules]

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

@ -167,7 +167,7 @@ export const useUndoRedo = createSharedComposable(() => {
useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
if (e && (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)) return
if ((e && (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)) || isExpandedFormOpen()) return
if (cmdOrCtrl && !e.altKey) {
switch (e.keyCode) {

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

@ -448,6 +448,8 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results"
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -1099,6 +1101,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1498,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Base not accessible",
"copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -1495,7 +1495,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Los tipos de archivo aceptados son .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "La clave del parámetro no puede estar vacía",
"duplicateParameterKeysAreNotAllowed": "No se permiten claves de parámetros duplicadas",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "{value} no puede estar vacía.",
"projectNotAccessible": "Proyecto no accesible",
"copyToClipboardError": "Fallo al copiar al portapapeles",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -40,7 +40,7 @@
},
"general": {
"role": "Role",
"general": "General",
"general": "Général",
"quit": "Quitter",
"home": "Accueil",
"load": "Charger",
@ -162,7 +162,7 @@
"insertAbove": "Insérer au-dessus",
"insertBelow": "Insérer en-dessous",
"hideField": "Masquer le champ",
"showField": "Show Field",
"showField": "Afficher le champ",
"sortAsc": "Trier par ordre croissant",
"sortDesc": "Trier par ordre décroissant",
"move": "Déplacer",
@ -201,14 +201,14 @@
"logo": "Logo",
"dropdown": "Liste déroulante",
"list": "Liste",
"verify": "Verify",
"verify": "Vérifier",
"apply": "Appliquer",
"text": "Texte",
"appearance": "Apparence"
},
"objects": {
"owner": "Owner",
"member": "Member",
"owner": "Propriétaire",
"member": "Membre",
"day": "Jour",
"week": "Semaine",
"month": "Mois",
@ -253,7 +253,7 @@
"viewer": "Lecture seule",
"noaccess": "Accès interdit",
"superAdmin": "Super administrateur",
"orgLevelOwner": "Organization Level Owner",
"orgLevelOwner": "Propriétaire au niveau de l'organisation",
"orgLevelCreator": "Créateur au niveau de l'organisation",
"orgLevelViewer": "Visualiseur de niveau d'organisation"
},
@ -321,10 +321,10 @@
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renameWorkspace": "Renommer l'espace de travail",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"renamingBase": "Renommer la Base",
"sso": "Authentification (SSO)",
"docs": "Documents",
"forum": "Forum",
"parameter": "Paramètre",
@ -337,34 +337,34 @@
"dateJoined": "Date d'inscription",
"tokenName": "Nom du jeton",
"inDesktop": "in Desktop",
"rowData": "Record data",
"creator": "Creator",
"rowData": "Enregistrer les données",
"creator": "Auteur",
"qrCode": "QR Code",
"termsOfService": "Terms of Service",
"updateSelectedRows": "Update Selected Records",
"noFiltersAdded": "No filters added",
"editCards": "Edit Cards",
"noFieldsFound": "No fields found",
"displayValue": "Display Value",
"expand": "Expand",
"termsOfService": "Conditions d'utilisation",
"updateSelectedRows": "Mettre à jour les enregistrements sélectionnés",
"noFiltersAdded": "Aucun filtre ajouté",
"editCards": "Modifier les cartes",
"noFieldsFound": "Aucun champ trouvé",
"displayValue": "Afficher la valeur",
"expand": "Déployer",
"hideAll": "Tout cacher",
"hideSystemFields": "Masquer les champs système",
"removeFile": "Remove File",
"hasMany": "Has Many",
"manyToMany": "Many to Many",
"oneToOne": "One to One",
"removeFile": "Supprimer le fichier",
"hasMany": "A plusieurs",
"manyToMany": "Plusieurs à plusieurs",
"oneToOne": "Un à un",
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMoreRecords": "Link more records",
"linkRecords": "Link Records",
"downloadFile": "Download File",
"linkMoreRecords": "Lier plus d'enregistrements",
"linkRecords": "Lier les enregistrements",
"downloadFile": "Télécharger le fichier",
"renameTable": "Renommer la table",
"renamingTable": "Renaming Table",
"renamingWs": "Renaming Workspace",
"renameWs": "Rename Workspace",
"deleteWs": "Delete Workspace",
"deletingWs": "Deleting Workspace",
"copyAuthToken": "Copy Auth Token",
"renamingTable": "Renommer la table",
"renamingWs": "Renommage de l'espace de travail",
"renameWs": "Renommer l'espace de travail",
"deleteWs": "Supprimer l’espace de travail",
"deletingWs": "Suppression de l'espace de travail",
"copyAuthToken": "Copier le jeton d'authentification",
"copiedAuthToken": "Jeton d'authentification copié",
"copyInviteToken": "Copier le jeton d'invitation",
"showSidebar": "Afficher la barre latérale",
@ -373,10 +373,10 @@
"erdView": "Vue ERD",
"newBase": "Nouvelle source de données",
"newProj": "Nouveau projet",
"createBase": "Create Base",
"createBase": "Créer une base",
"myProject": "Mes projets",
"formTitle": "Intitulé du formulaire",
"collaborative": "Collaborative",
"collaborative": "Collaboratif",
"locked": "Verrouillé",
"personal": "Personal",
"appStore": "Magasin d'applications",
@ -423,12 +423,12 @@
"findRowByScanningCode": "Find row by scanning a QR or Barcode",
"tokenManagement": "Gestion des jetons",
"addNewToken": "Ajouter un nouveau jeton",
"createNewToken": "Create new token",
"createNewToken": "Créer un nouveau jeton",
"accountSettings": "Paramètres du compte",
"resetPasswordMenu": "Réinitialiser le mot de passe",
"tokens": "Jetons",
"userManagement": "Gestion des utilisateurs",
"accountManagement": "Account management",
"accountManagement": "Gestion des comptes",
"licence": "Licence",
"allowAllMimeTypes": "Autoriser tous les types Mime",
"defaultView": "Vue par défaut",
@ -439,8 +439,8 @@
"noAction": "No Action",
"cascade": "Cascade",
"restrict": "Restrict",
"setNull": "Set NULL",
"setDefault": "Set Default"
"setNull": "Définir NULL",
"setDefault": "Définir à la valeur par défaut"
},
"selectFieldsFromRightPannelToAddHere": "Select fields from right panel to add here",
"noOptionsFound": "No options found",
@ -454,7 +454,7 @@
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"addMember": "Ajouter un membre",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
@ -462,7 +462,7 @@
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"addMembersToOrganization": "Ajouter des membres à l'organisation",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
@ -470,7 +470,7 @@
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"dateAdded": "Date ajoutée",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
@ -484,30 +484,30 @@
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
"metadataUrl": "Metadata URL",
"save": "Enregistrer",
"cancel": "Annuler",
"metadataUrl": "URL des métadonnées",
"audience-entityId": "Audience/ Entity ID",
"redirectUrl": "URL de redirection",
"oidc": "OpenID Connect (OIDC)",
"saml": "Langage de balisage d'assertion de sécurité (SAML)",
"newProvider": "Nouveau fournisseur",
"generalSettings": "Paramètres généraux",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"adminPanel": "Panneau d'administration",
"moveWorkspaceToOrg": "Déplacer l'espace de travail vers l'organisation",
"ssoSettings": "Paramètres SSO",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"settings": "Réglages",
"workspaces": "Espaces de travail",
"back": "Précédent",
"dashboard": "Tableau de bord",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
"previousMonth": "Previous Month",
"next": "Next",
"organiseBy": "Organise by",
"previous": "Précédent",
"nextMonth": "Mois suivant",
"previousMonth": "Mois précédent",
"next": "Suivant",
"organiseBy": "Trier par",
"heading1": "Titre 1",
"heading2": "Titre 2",
"heading3": "Titre 3",
@ -552,7 +552,7 @@
"timeFormat": "Format d'heure",
"singularLabel": "Libellé au singulier",
"pluralLabel": "Libellé au pluriel",
"selectDateField": "Select a date field",
"selectDateField": "Sélectionner un champ date",
"endDateField": "End date field",
"optional": "(Facultatif)",
"clickToMake": "Cliquez pour faire",
@ -562,7 +562,7 @@
"clickToHide": "Cliquez pour masquer",
"clickToDownload": "Cliquez pour télécharger",
"forRole": "pour le rôle",
"clickToCopyTableID": "Click to copy Table ID",
"clickToCopyTableID": "Cliquer pour copier l'ID de la table",
"clickToCopyViewID": "Cliquer pour copier l'ID de la vue",
"viewMode": "Mode d'affichage",
"searchUsers": "Rechercher des utilisateurs",
@ -597,9 +597,9 @@
"duplicateFormView": "Dupliquer une vue Formulaire",
"createFormView": "Créer une vue Formulaire",
"duplicateKanbanView": "Dupliquer une vue Kanban",
"duplicateCalendarView": "Duplicate Calendar View",
"duplicateCalendarView": "Dupliquer la vue calendrier",
"createKanbanView": "Créer une vue Kanban",
"createCalendarView": "Create Calendar View",
"createCalendarView": "Créer une vue calendrier",
"viewName": "Vue",
"viewLink": "Lien de vue",
"columnName": "Nom de la colonne",
@ -734,43 +734,43 @@
"noAccess": "Accès interdit",
"restApis": "API REST",
"apis": "APIs",
"includeData": "Include Data",
"includeView": "Include View",
"includeWebhook": "Include Webhook",
"includeData": "Inclure les données",
"includeView": "Inclure la vue",
"includeWebhook": "Inclure le Webhook",
"zoomInToViewColumns": "Zoom in to view columns",
"embedInSite": "Embed this view in your site",
"titleRequired": "title is required.",
"sourceNameRequired": "Source name is required",
"changeWsName": "Change Workspace Name",
"titleRequired": "le titre est obligatoire.",
"sourceNameRequired": "Le nom de la source est obligatoire",
"changeWsName": "Changer le nom de l'espace de travail",
"pressEnter": "Appuyez sur Entrée",
"newFormLoaded": "New form will be loaded after",
"webhook": "Webhook",
"multiField": {
"newField": "New field",
"saveChanges": "Save changes",
"updatedField": "Updated field",
"deletedField": "Deleted field",
"newField": "Ajouter Champ",
"saveChanges": "Enregistrer les modifications",
"updatedField": "Champ mis à jour",
"deletedField": "Champ supprimé",
"incompleteConfiguration": "Incomplete configuration",
"selectField": "Select a field",
"selectField": "Sélectionner un champ",
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
},
"appearanceSettings": "Appearance Settings",
"backgroundColor": "Background Color",
"hideNocodbBranding": "Hide NocoDB Branding",
"backgroundColor": "Couleur d'arrière-plan",
"hideNocodbBranding": "Masquer la marque NocoDB",
"showOnConditions": "Show on conditions",
"showFieldOnConditionsMet": "Shows field only when conditions are met",
"limitOptions": "Limit options",
"limitOptionsSubtext": "Limit options visible to users by selecting available options",
"clearSelection": "Clear selection"
"clearSelection": "Effacer la sélection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"renameBase": "Renommer la Base",
"renameWorkspace": "Renommer l'espace de travail",
"deactivate": "Désactiver",
"manageUsers": "Gérer les utilisateurs",
"newWorkspace": "Nouvel espace de travail",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"addMembers": "Ajouter des membres",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
@ -780,22 +780,22 @@
"toggleSidebar": "Toggle Sidebar",
"addEndDate": "Add end date",
"withEndDate": "with end date",
"calendar": "Calendar",
"calendar": "Calendrier",
"viewSettings": "View settings",
"googleOAuth": "Google OAuth",
"registerOIDC": "Register OIDC Identity Provider",
"registerSAML": "Register SAML Identity Provider",
"openInANewTab": "Open in a new tab",
"copyIFrameCode": "Copy IFrame code",
"openInANewTab": "Ouvrir dans un nouvel onglet",
"copyIFrameCode": "Copier le code IFrame",
"onCondition": "On Condition",
"bulkDownload": "Bulk Download",
"attachFile": "Attach File",
"viewAttachment": "View Attachments",
"attachmentDrop": "Click or drop a file into cell",
"addFiles": "Add File(s)",
"hideInUI": "Hide in UI",
"attachmentDrop": "Cliquez ou déposez un fichier dans la cellule",
"addFiles": "Ajouter un ou des fichier(s)",
"hideInUI": "Masquer dans l'interface",
"addBase": "Add Base",
"addParameter": "Add Parameter",
"addParameter": "Ajouter un paramètre",
"submitAnotherForm": "Soumettre un autre formulaire",
"dragAndDropFieldsHereToAdd": "Drag and drop fields here to add",
"editSource": "Edit Data Source",
@ -807,10 +807,10 @@
"newWebhook": "New Webhook",
"enablePublicAccess": "Activer l'accès public",
"doYouWantToSaveTheChanges": "Voulez-vous enregistrer les modifications ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable Public Viewing",
"editingAccess": "Accès en modification",
"enabledPublicViewing": "Activer l'affichage anonyme",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"manageProjectAccess": "Gérer l'accès à la base",
"allowDownload": "Autoriser le téléchargement",
"surveyMode": "Mode Sondage",
"rtlOrientation": "RTL Orientation",
@ -861,7 +861,7 @@
"groupBy": "Grouper par",
"addSubGroup": "Ajouter un sous-groupe",
"shareBase": {
"label": "Share Base",
"label": "Partager la base",
"disable": "Désactiver la base partagée",
"enable": "N'importe qui disposant du lien",
"link": "Partager le lien de la base"
@ -896,7 +896,7 @@
"addField": "Ajouter un nouveau champ à ce tableau",
"setDisplay": "Définir comme valeur d'affichage",
"addRow": "Ajouter une nouvelle ligne",
"saveRow": "Enregistrer la ligne",
"saveRow": "Enregistrer",
"saveAndExit": "Enregistrer et quitter",
"saveAndStay": "Enregistrer et rester",
"insertRow": "Insérer une nouvelle ligne",
@ -1010,10 +1010,10 @@
"lockedFieldTooltip": "Pre-filled value"
},
"getPreFilledLink": "Get Pre-filled Link",
"group": "Group"
"group": "Grouper par"
},
"tooltip": {
"reachedSourceLimit": "Limited to 10 data sources per base",
"reachedSourceLimit": "Limité à une seule source de données pour le moment",
"saveChanges": "Sauvegarder les modifications",
"xcDB": "Créer un nouveau projet",
"extDB": "Base de données supportées MySQL, PostgreSQL, SQL Server & SQLite",
@ -1055,7 +1055,7 @@
"notFoundContent": "No valid field Type can be found.",
"selectBarcodeFormat": "Select a Barcode format",
"projName": "Saisir le nom du projet",
"selectGroupField": "Select a Grouping Field",
"selectGroupField": "Choisir un champ de regroupement",
"selectGroupFieldNotFound": "No Single Select Field can be found. Please create one first.",
"selectGeoField": "Select a GeoData Field",
"notSelected": "-not selected-",
@ -1101,7 +1101,7 @@
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"restrictUsersFromSharing": "Empêcher les utilisateurs de partager leurs bases publiquement.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Cliquer pour copier l'ID du champ",
@ -1169,7 +1169,7 @@
"hm": {
"title": "Has Many Relation",
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with multiple records from table "
"tooltip_desc2": " peut être lié avec plusieurs enregistrements de la table "
},
"mm": {
"title": "Many to Many Relation",
@ -1186,14 +1186,14 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
"clickLinkRecordsToAddLinkFromTable": "Il semble qu'aucun enregistrement n'ait encore été lié.",
"noRecordsLinked": "Aucun enregistrement lié",
"noLinkedRecords": "Aucun enregistrement lié",
"recordsLinked": "enregistrements liés",
"acceptOnlyValid": "Accepts only",
"apiTokenCreate": "Create personal API tokens to use in automation or external apps.",
"selectFieldToSort": "Select Field to Sort",
"selectFieldToGroup": "Select Field to Group",
"selectFieldToSort": "Sélectionner le champ de tri",
"selectFieldToGroup": "Sélectionner le champ de regroupement",
"thereAreNoRecordsInTable": "Il n'y a aucun enregistrement dans la table",
"createWebhookMsg1": "Commencez à utiliser les web-hooks !",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
@ -1203,8 +1203,8 @@
"length59Required": "The length exceeds the max 59 characters",
"noNewNotifications": "You have no new notifications",
"noRecordFound": "Record not found",
"noRecordsFound": "No records found",
"noRecordsMatchYourSearchQuery": "No records match your search query",
"noRecordsFound": "Aucun enregistrement trouvé",
"noRecordsMatchYourSearchQuery": "Aucun enregistrement ne correspond à votre recherche",
"rowDeleted": "Record deleted",
"saveChanges": "Do you want to save the changes?",
"tooLargeFieldEntity": "The field is too large to be converted to {entity}",
@ -1408,7 +1408,7 @@
"preventHideAllOptions": "You cannot hide all options if field is required"
},
"error": {
"fetchingCalendarData": "Error fetching calendar data",
"fetchingCalendarData": "Erreur lors de la récupération des données du calendrier",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
@ -1495,7 +1495,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Les types de fichiers acceptés sont .xls, .xlsx, .xlsm, .ods, .ots.",
"parameterKeyCannotBeEmpty": "La clé de paramètre ne peut pas être vide",
"duplicateParameterKeysAreNotAllowed": "Les doublons de clés de paramètres ne sont pas autorisés",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "{value} ne peut pas être vide.",
"projectNotAccessible": "Projet non accessible",
"copyToClipboardError": "Échec de la copie dans le presse-papiers",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -1013,7 +1013,7 @@
"group": "Group"
},
"tooltip": {
"reachedSourceLimit": "Limited to 10 data sources per base",
"reachedSourceLimit": "Limitato a una sola sorgente dati per il momento",
"saveChanges": "Salva le modifiche",
"xcDB": "Crea un nuovo progetto",
"extDB": "Supporta MySQL, PostgreSQL, SQL Server & SQLite",
@ -1495,7 +1495,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "I tipi di file accettati sono .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "La chiave del parametro non può essere vuota",
"duplicateParameterKeysAreNotAllowed": "Le chiavi dei parametri duplicate non sono consentite",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "{value} non può essere vuoto.",
"projectNotAccessible": "Progetto non accessibile",
"copyToClipboardError": "Non è riuscito a copiare negli appunti",
"pasteFromClipboardError": "Impossibile incollare dagli appunti",

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

@ -1013,7 +1013,7 @@
"group": "Group"
},
"tooltip": {
"reachedSourceLimit": "Limited to 10 data sources per base",
"reachedSourceLimit": "데이터 소스 제한에 도달했습니다.",
"saveChanges": "변경 사항 저장",
"xcDB": "새 프로젝트 생성",
"extDB": "MySQL, PostgreSQL, SQL Server 및 SQLite 지원",
@ -1495,7 +1495,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "허용되는 파일 형식은 .xls, .xlsx, .xlsm, .ods, .ots입니다",
"parameterKeyCannotBeEmpty": "매개 변수 키는 비워 둘 수 없습니다.",
"duplicateParameterKeysAreNotAllowed": "중복 매개 변수 키는 허용되지 않습니다.",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "{value}은(는) 비워 둘 수 없습니다.",
"projectNotAccessible": "프로젝트에 액세스할 수 없습니다.",
"copyToClipboardError": "클립 보드에 복사할 수 없습니다.",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -39,8 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"role": "Rola",
"general": "Ogólne",
"quit": "Wyjdź",
"home": "Start",
"load": "Załaduj",
@ -162,7 +162,7 @@
"insertAbove": "Wstaw powyżej",
"insertBelow": "Wstaw poniżej",
"hideField": "Ukryj pole",
"showField": "Show Field",
"showField": "Pokaż pole",
"sortAsc": "Sortuj rosnąco",
"sortDesc": "Sortuj malejąco",
"move": "Przenieś",
@ -201,14 +201,14 @@
"logo": "Logo",
"dropdown": "Rozwijane menu",
"list": "Lista",
"verify": "Verify",
"verify": "Weryfikuj",
"apply": "Zastosuj",
"text": "Tekst",
"appearance": "Wygląd"
},
"objects": {
"owner": "Owner",
"member": "Member",
"owner": "Właściciel",
"member": "Członek",
"day": "Dzień",
"week": "Tydzień",
"month": "Miesiąc",
@ -253,7 +253,7 @@
"viewer": "Obserwator",
"noaccess": "Brak dostępu",
"superAdmin": "Superadministrator",
"orgLevelOwner": "Organization Level Owner",
"orgLevelOwner": "Obserwator na poziomie organizacji",
"orgLevelCreator": "Twórca na poziomie organizacji",
"orgLevelViewer": "Obserwator na poziomie organizacji"
},
@ -320,10 +320,10 @@
"isNotNull": "Nie jest null"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"renameBase": "Zmień nazwę tabeli",
"renameWorkspace": "Zmień nazwę przestrzeni roboczej",
"renamingWorkspace": "Zmiana nazwy przestrzeni roboczej",
"renamingBase": "Zmiana nazwy tabeli",
"sso": "Autoryzacja (SSO)",
"docs": "Dokumentacja",
"forum": "Forum",
@ -448,41 +448,41 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"today": "Dziś",
"workspace": "Obszar roboczy",
"txt": "Wartość rekordu TXT",
"transferOwnership": "Zmień właściciela",
"recentActivity": "Ostatnia aktywność",
"goToMembers": "Przejdź do członków",
"addMember": "Dodaj członka",
"numberOfMembers": "Liczba członków",
"numberOfBases": "Liczba tabel",
"numberOfRecords": "Liczba rekordów",
"workspaceName": "Nazwa obszaru roboczego",
"workspaceWithoutOwner": "Przestrzeń robocza bez właścicieli",
"inviteUsersToWorkspace": "Zapraszanie użytkowników do obszaru roboczego",
"selectWorkspace": "-wybierz projekty do zaproszenia-",
"addMembersToOrganization": "Dodaj użytkowników do organizacji",
"memberIn": "Członek w:",
"assignAs": "Przypisz jako",
"signOutUser": "Wyloguj użytkownika",
"signOutUsers": "Wyloguj użytkowników",
"deactivateUser": "Dezaktywuj użytkownika",
"deactivateUsers": "Dezaktywuj użytkowników",
"lastActive": "Ostatnio aktywny",
"dateAdded": "Data dodania",
"uploadImage": "Prześlij obrazek",
"organizationProfile": "Profil organizacji",
"organizationImage": "Obraz organizacji",
"organizationName": "Nazwa organizacji",
"activeDomains": "Aktywne domeny",
"domains": "Domeny",
"disablePublicSharing": "Wyłącz udostępnianie publiczne",
"shareSettings": "Ustawienia udostępniania",
"deleteUserAndData": "Usuń użytkownika i jego dane",
"userOptions": "Opcje użytkownika",
"deleteThisOrganization": "Usuń tę organizację",
"dangerZone": "Strefa zagrożenia",
"selectYear": "Wybierz rok",
"save": "Zapisz",
"cancel": "Anuluj",
@ -493,15 +493,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "Nowy dostawca",
"generalSettings": "Ustawienia ogólne",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"adminPanel": "Panel administracyjny",
"moveWorkspaceToOrg": "Przenieś obszar roboczy do organizacji",
"ssoSettings": "Ustawienia SSO",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"addDomain": "Dodaj domenę",
"domain": "Domena",
"settings": "Ustawienia",
"workspaces": "Obszary robocze",
"back": "Powrót",
"dashboard": "Pulpit",
"organizeBy": "Organizuj według",
"previous": "Poprzedni",
"nextMonth": "Następny miesiąc",
@ -717,7 +717,7 @@
"hasMany": "ma wiele",
"belongsTo": "należy do",
"manyToMany": "ma wiele relacji do wielu",
"oneToOne": "have one to one relation",
"oneToOne": "relacja Jeden do Jednego",
"extraConnectionParameters": "Dodatkowe parametry połączenia",
"commentsOnly": "Tylko komentarze",
"documentation": "Dokumentacja",
@ -759,21 +759,21 @@
"hideNocodbBranding": "Ukryj branding NocoDB",
"showOnConditions": "Pokaż na warunkach",
"showFieldOnConditionsMet": "Pokazuje pole tylko, gdy spełnione są warunki",
"limitOptions": "Limit options",
"limitOptions": "Ogranicz opcje",
"limitOptionsSubtext": "Ogranicz opcje widoczne dla użytkowników, wybierając dostępne opcje",
"clearSelection": "Wyczyść wybór"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"renameBase": "Zmień nazwę tabeli",
"renameWorkspace": "Zmień nazwę przestrzeni roboczej",
"deactivate": "Deaktywuj",
"manageUsers": "Zarządzaj użytkownikami",
"newWorkspace": "Nowy obszar roboczy",
"addDomain": "Dodaj domenę",
"addMembers": "Dodaj Członków",
"enterEmail": "Wpisz adresy e-mail",
"inviteToBase": "Zaproś do bazy",
"inviteToWorkspace": "Invite to Workspace",
"inviteToWorkspace": "Zapraszanie użytkowników do obszaru roboczego",
"addMember": "Dodaj członka do bazy",
"noRange": "Widok kalendarza wymaga zakresu dat",
"goToToday": "Przejdź do dzisiaj",
@ -945,7 +945,7 @@
"importZip": "Importuj jako Zip",
"metaSync": "Synchronizuj teraz",
"settings": "Ustawienia",
"validations": "Validations",
"validations": "Zatwierdzenia",
"previewAs": "Podgląd jako",
"resetReview": "Zresetuj podgląd",
"testDbConn": "Testuj połączenie z bazą danych",
@ -1010,10 +1010,10 @@
"lockedFieldTooltip": "Wstępnie wypełniona wartość"
},
"getPreFilledLink": "Uzyskaj link z wstępnie wypełnionymi danymi",
"group": "Group"
"group": "Grupa"
},
"tooltip": {
"reachedSourceLimit": "Limited to 10 data sources per base",
"reachedSourceLimit": "Ograniczenie do jednego źródła danych w tej chwili",
"saveChanges": "Zapisz zmiany",
"xcDB": "Utwórz nowy projekt",
"extDB": "Obsługuje MySQL, PostgreSQL, SQL Server i Sqlite",
@ -1041,7 +1041,7 @@
"clientKey": "Wybierz plik .key.",
"clientCert": "Wybierz plik .Cert.",
"clientCA": "Wybierz plik CA.",
"changeIconColour": "Change icon colour",
"changeIconColour": "Zmiana koloru ikony",
"preFillFormInfo": "Generuj URL formularza udostępniania z wstępnie wypełnionymi danymi pola. Aby uzyskać link z wstępnie wypełnionymi danymi, upewnij się, że wypełniłeś wymagane pola w kreatorze widoku formularza.",
"surveyFormInfo": "Tryb formularza z jednym polem na stronę"
},
@ -1099,11 +1099,11 @@
"searchOptions": "Opcje wyszukiwania"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"controlOrgAppearance": "Kontroluj nazwę i wygląd organizacji.",
"addCompanyDomains": "Dodaj domeny firmy, aby ograniczyć dostęp od niechcianych użytkowników.",
"restrictUsersFromSharing": "Ogranicz użytkowników z możliwości publicznego udostępniania baz danych.",
"selectUsersToBeRemoved": "Proszę wybrać użytkowników, którzy mają zostać usunięci ze wszystkich obszarów roboczych organizacji.",
"deleteOrganization": "Usuń wszystkich użytkowników, bazy i dane związane z tą organizacją",
"clickToCopyFieldId": "Kliknij, aby skopiować Identyfikator Pola",
"enterPassword": "Wprowadź hasło",
"bySigningUp": "Rejestrując się, zgadzasz się na",
@ -1186,7 +1186,7 @@
"tooltip_desc": "Pojedynczy rekord z tabeli ",
"tooltip_desc2": " może być powiązany z pojedynczym rekordem z tabeli "
},
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"clickLinkRecordsToAddLinkFromTable": "Kliknij 'Połącz Rekordy', aby rozpocząć kojarzenie danych z '{tableName}'.",
"noRecordsLinked": "Żadne rekordy nie są powiązane",
"noLinkedRecords": "Brak powiązanych rekordów",
"recordsLinked": "rekordy powiązane",
@ -1196,7 +1196,7 @@
"selectFieldToGroup": "Wybierz pole do grupowania",
"thereAreNoRecordsInTable": "Nie ma rekordów w tabeli",
"createWebhookMsg1": "Rozpocznij pracę z webhookami!",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Zasil automatyzację. Otrzymuj powiadomienia jak tylko pojawią się zmiany w Twoich danych",
"areYouSureUWantTo": "Czy na pewno chcesz usunąć następujące",
"areYouSureUWantToDeleteLabel": "Czy na pewno chcesz {deleteLabel} następujące",
"idColumnRequired": "Pole ID jest wymagane, możesz to później zmienić, jeśli będzie to konieczne.",
@ -1204,7 +1204,7 @@
"noNewNotifications": "Nie masz nowych powiadomień",
"noRecordFound": "Nie znaleziono rekordu",
"noRecordsFound": "Nie znaleziono rekordów",
"noRecordsMatchYourSearchQuery": "No records match your search query",
"noRecordsMatchYourSearchQuery": "Brak rekordów pasujących do Twojego zapytania",
"rowDeleted": "Rekord usunięty",
"saveChanges": "Czy chcesz zapisać zmiany?",
"tooLargeFieldEntity": "Pole jest zbyt duże, aby przekonwertować je na {entity}",
@ -1231,11 +1231,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"enterWorkspaceName": "Wprowadź nazwę projektu",
"enterBaseName": "Wpisz nazwę tabeli",
"idpPaste": "Proszę wkleić ten adres URL w konsoli dostawcy tożsamości",
"noSaml": "Nie ma skonfigurowanych uwierzytelnień SAML.",
"noOIDC": "There are no configured OpenID authentications.",
"noOIDC": "Nie ma skonfigurowanych uwierzytelnień OpenID.",
"disabledAsViewLocked": "Wyłączone, ponieważ widok jest zablokowany",
"basesMigrated": "Bazy zostały przeniesione. Proszę spróbować ponownie.",
"pasteNotSupported": "Operacja wklejania nie jest obsługiwana w aktywnej komórce",
@ -1411,7 +1411,7 @@
"fetchingCalendarData": "Błąd podczas pobierania danych kalendarza",
"fetchingActiveDates": "Błąd podczas pobierania aktywnych dat",
"scopesRequired": "Wymagane zakresy",
"domainRequired": "Domain name is required",
"domainRequired": "Nazwa domeny jest wymagana",
"authUrlRequired": "Wymagany jest URL autoryzacji",
"userNameAttributeRequired": "Wymagany jest atrybut nazwy użytkownika",
"clientIdRequired": "Wymagane jest ID klienta",
@ -1425,7 +1425,7 @@
"nameMinLength": "Nazwa musi mieć co najmniej 2 znaki",
"nameMaxLength": "Nazwa musi mieć maksymalnie 60 znaków",
"viewNameRequired": "Wymagana nazwa widoku",
"domainNameRequired": "Domain name is required",
"domainNameRequired": "Nazwa domeny jest wymagana",
"nameMaxLength256": "Nazwa musi mieć maksymalnie 256 znaków",
"viewNameUnique": "Nazwa widoku powinna być unikalna",
"searchProject": "Twoje wyszukiwanie dla {search}, nie znaleziono żadnych wyników",
@ -1495,7 +1495,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Akceptowane typy plików to: .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Klucz parametru nie może być pusty",
"duplicateParameterKeysAreNotAllowed": "Zduplikowane klucze parametrów są niedozwolone",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "{value} nie może być puste.",
"projectNotAccessible": "Projekt niedostępny",
"copyToClipboardError": "Nie udało się skopiować do schowka",
"pasteFromClipboardError": "Nie udało się wkleić ze schowka",

24
packages/nc-gui/lang/ru.json

@ -320,10 +320,10 @@
"isNotNull": "не равно Null"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"renameBase": "Переименовать базу",
"renameWorkspace": "Переименовать рабочую область",
"renamingWorkspace": "Переименование рабочей области",
"renamingBase": "Переименование базы",
"sso": "Аутентификация (SSO)",
"docs": "Документация",
"forum": "Форум",
@ -423,7 +423,7 @@
"findRowByScanningCode": "Найти строку путем сканирования QR или штрих-кода",
"tokenManagement": "Управление токенами",
"addNewToken": "Добавить новый токен",
"createNewToken": "Create new token",
"createNewToken": "Создать новый токен",
"accountSettings": "Настройки аккаунта",
"resetPasswordMenu": "Сбросить пароль",
"tokens": "Токены",
@ -467,9 +467,9 @@
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"deactivateUser": "Отключить пользователя",
"deactivateUsers": "Отключить пользователей",
"lastActive": "Последняя активность",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
@ -480,9 +480,9 @@
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"userOptions": "Настройки пользователя",
"deleteThisOrganization": "Удалить эту организацию",
"dangerZone": "Опасная зона",
"selectYear": "Выберите год",
"save": "Сохранить",
"cancel": "Отмена",
@ -1186,7 +1186,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",

4
packages/nc-gui/lang/zh-Hans.json

@ -1013,7 +1013,7 @@
"group": "分组"
},
"tooltip": {
"reachedSourceLimit": "每个项目仅限 10 个数据源",
"reachedSourceLimit": "暂时只限于一个数据源",
"saveChanges": "保存更改",
"xcDB": "新建项目",
"extDB": "支持 MySQL、PostgreSQL、SQL Server 和 SQLite",
@ -1196,7 +1196,7 @@
"selectFieldToGroup": "选择要分组的字段",
"thereAreNoRecordsInTable": "表中没有记录",
"createWebhookMsg1": "开始使用Web Hooks!",
"createWebhookMsg2": "为您的自动化提供动力。在数据发生变化时立即获得通知",
"createWebhookMsg2": "数据变更自动发送通知",
"areYouSureUWantTo": "您确定要删除以下内容",
"areYouSureUWantToDeleteLabel": "您确定要 {deleteLabel} 以下内容吗?",
"idColumnRequired": "ID 字段是必填字段,如果需要,您可以稍后重新命名。",

38
packages/nc-gui/package.json

@ -38,7 +38,7 @@
"dependencies": {
"@braks/revue-draggable": "^0.4.3",
"@ckpack/vue-color": "^1.5.0",
"@iconify/vue": "^4.1.1",
"@iconify/vue": "^4.1.2",
"@nuxt/image": "^1.3.0",
"@pinia/nuxt": "^0.5.1",
"@tiptap/extension-link": "2.2.6",
@ -60,9 +60,9 @@
"crossoriginworker": "^1.1.0",
"d3-scale": "^4.0.2",
"dagre": "^0.8.5",
"dayjs": "^1.11.10",
"dayjs": "^1.11.11",
"deep-object-diff": "^1.1.9",
"emoji-mart-vue-fast": "^15.0.1",
"emoji-mart-vue-fast": "^15.0.2",
"file-saver": "^2.0.5",
"fuse.js": "^6.6.2",
"httpsnippet": "^2.0.0",
@ -75,7 +75,7 @@
"marked": "^4.3.0",
"monaco-editor": "^0.45.0",
"monaco-sql-languages": "^0.11.0",
"nocodb-sdk": "0.207.1",
"nocodb-sdk": "workspace:^",
"papaparse": "^5.4.1",
"parse-github-url": "^1.0.2",
"pinia": "^2.1.7",
@ -92,7 +92,7 @@
"validator": "^13.11.0",
"vue-advanced-cropper": "^2.8.8",
"vue-barcode-reader": "^1.0.3",
"vue-chartjs": "^5.3.0",
"vue-chartjs": "^5.3.1",
"vue-dompurify-html": "^3.1.2",
"vue-github-button": "^3.1.0",
"vue-i18n": "^9.9.1",
@ -107,32 +107,32 @@
"devDependencies": {
"@antfu/eslint-config": "^0.26.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@iconify-json/ant-design": "^1.1.15",
"@iconify-json/ant-design": "^1.1.16",
"@iconify-json/bi": "^1.1.23",
"@iconify-json/carbon": "^1.1.31",
"@iconify-json/carbon": "^1.1.33",
"@iconify-json/cil": "^1.1.8",
"@iconify-json/clarity": "^1.1.12",
"@iconify-json/eva": "^1.1.10",
"@iconify-json/ic": "^1.1.17",
"@iconify-json/ion": "^1.1.17",
"@iconify-json/ion": "^1.1.18",
"@iconify-json/la": "^1.1.8",
"@iconify-json/logos": "^1.1.42",
"@iconify-json/lucide": "^1.1.180",
"@iconify-json/material-symbols": "^1.1.77",
"@iconify-json/lucide": "^1.1.187",
"@iconify-json/material-symbols": "^1.1.80",
"@iconify-json/mdi": "^1.1.66",
"@iconify-json/mi": "^1.1.8",
"@iconify-json/ph": "^1.1.12",
"@iconify-json/ph": "^1.1.13",
"@iconify-json/ri": "^1.1.20",
"@iconify-json/simple-icons": "^1.1.99",
"@iconify-json/simple-icons": "^1.1.101",
"@iconify-json/system-uicons": "^1.1.12",
"@iconify-json/tabler": "^1.1.109",
"@iconify-json/vscode-icons": "^1.1.33",
"@iconify-json/tabler": "^1.1.112",
"@iconify-json/vscode-icons": "^1.1.34",
"@intlify/unplugin-vue-i18n": "^0.13.0",
"@nuxt/image": "^1.3.0",
"@types/d3-scale": "^4.0.8",
"@types/dagre": "^0.7.52",
"@types/file-saver": "^2.0.7",
"@types/leaflet": "^1.9.9",
"@types/leaflet": "^1.9.12",
"@types/leaflet.markercluster": "^1.5.4",
"@types/papaparse": "^5.3.14",
"@types/parse-github-url": "^1.0.3",
@ -142,12 +142,12 @@
"@types/splitpanes": "^2.2.6",
"@types/tinycolor2": "^1.4.6",
"@types/turndown": "^5.0.4",
"@types/validator": "^13.11.9",
"@types/validator": "^13.11.10",
"@types/vue-barcode-reader": "^0.0.3",
"@unocss/nuxt": "^0.58.9",
"@vitest/ui": "^0.34.7",
"@vue/compiler-sfc": "^3.4.21",
"@vue/test-utils": "^2.4.5",
"@vue/compiler-sfc": "^3.4.27",
"@vue/test-utils": "^2.4.6",
"@vueuse/nuxt": "^10.7.2",
"@windicss/plugin-animations": "^1.0.9",
"@windicss/plugin-question-mark": "^0.1.1",
@ -156,7 +156,7 @@
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.2.1",
"happy-dom": "^6.0.4",
"nuxt": "^3.10.3",
"nuxt": "^3.11.2",
"nuxt-windicss": "^2.6.1",
"prettier": "^2.8.8",
"sass": "^1.71.1",

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

@ -2,6 +2,7 @@
export const isMac = () => /Mac/i.test(navigator.platform)
export const isDrawerExist = () => document.querySelector('.ant-drawer-open')
export const isDrawerOrModalExist = () => document.querySelector('.ant-modal.active, .ant-drawer-open')
export const isExpandedFormOpen = () => document.querySelector('.nc-drawer-expanded-form.active')
export const isExpandedCellInputExist = () => document.querySelector('.expanded-cell-input')
export const cmdKActive = () => document.querySelector('.cmdk-modal-active')

31
packages/nc-gui/utils/formValidations.ts

@ -64,6 +64,37 @@ export const formNumberInputValidator = (cal: ColumnType) => {
}
}
export const requiredFieldValidatorFn = (value: unknown) => {
value = unref(value)
if (Array.isArray(value)) return !!value.length
if (value === undefined || value === null) {
return false
}
if (value === false) {
return true
}
if (typeof value === 'object') {
for (let _ in value) return true
return false
}
return !!String(value).length
}
export const isEmptyValidatorValue = (v: Validation) => {
if (v.type === StringValidationType.Regex) {
return v.type && typeof v.regex === 'string' ? !v.regex.trim() : v.regex === null
} else if (v.type && v.value !== undefined) {
return v.type && typeof v.value === 'string' ? !v.value.trim() : v.value === null
}
return false
}
export const extractFieldValidator = (_validators: Validation[], element: ColumnType) => {
const rules: RuleObject[] = []

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

@ -22,7 +22,17 @@ export default defineConfig({
},
darkMode: 'class',
safelist: ['text-yellow-500', 'text-sky-500', 'text-red-500', 'bg-primary-selected'],
safelist: [
'text-yellow-500',
'text-sky-500',
'text-red-500',
'bg-primary-selected',
'text-pink-500',
'text-orange-500',
'text-blue-500',
'text-purple-500',
'text-grey',
],
plugins: [
scrollbar,
animations,
@ -101,6 +111,9 @@ export default defineConfig({
primary: 'rgba(var(--color-primary), var(--tw-ring-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-ring-opacity))',
},
boxShadow: {
selected: '0px 0px 0px 2px var(--ant-primary-color-outline)',
},
colors: {
...windiColors,
...themeColors,

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

@ -6,68 +6,75 @@ tags: ['Open Source']
keywords : ['NocoDB environment variables', 'NocoDB env variables', 'NocoDB envs', 'NocoDB env']
---
For production use-cases, it is **recommended** to configure
- `NC_DB`,
- `NC_AUTH_JWT_SECRET`,
- `NC_PUBLIC_URL`,
For production use cases, it is **recommended** to set at least:
- `NC_DB`
- `NC_AUTH_JWT_SECRET`
- `NC_PUBLIC_URL`
- `NC_REDIS_URL`
| Variable | Comments | If absent |
|------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
| NC_DB | See our example database URLs [here](https://github.com/nocodb/nocodb#docker). | A local SQLite will be created in root folder if `NC_DB` is not provided |
| NC_DB_JSON | Can be used instead of `NC_DB` and value should be valid knex connection JSON | |
| NC_DB_JSON_FILE | Can be used instead of `NC_DB` and value should be a valid path to knex connection JSON | |
| DATABASE_URL | Can be used instead of `NC_DB` and value should be in JDBC URL format | |
| DATABASE_URL_FILE | Can be used instead of `DATABASE_URL` and value should be a valid path to file containing JDBC URL format. | |
| NC_AUTH_JWT_SECRET | JWT secret used for auth and storing other secrets | A random secret will be generated |
| PORT | For setting app running port | `8080` |
| DB_QUERY_LIMIT_DEFAULT | Pagination limit | 25 |
| DB_QUERY_LIMIT_GROUP_BY_GROUP | Group per page limit | 10 |
| DB_QUERY_LIMIT_GROUP_BY_RECORD | Record per group limit | 10 |
| DB_QUERY_LIMIT_MAX | Maximum allowed pagination limit | 1000 |
| DB_QUERY_LIMIT_MIN | Minimum allowed pagination limit | 1 |
| NC_TOOL_DIR | App directory to keep metadata and app related files | Defaults to current working directory. In docker maps to `/usr/app/data/` for mounting volume. |
| NC_PUBLIC_URL | Used for sending Email invitations | Best guess from http request params |
| NC_JWT_EXPIRES_IN | JWT token expiry time | `10h` |
| NC_CONNECT_TO_EXTERNAL_DB_DISABLED | Disable Project creation with external database | |
| NC_INVITE_ONLY_SIGNUP | Removed since version 0.99.0 and now it's recommended to use [super admin settings menu](/account-settings/oss-specific-details#enable--disable-signup). Allow users to signup only via invite URL, value should be any non-empty string. | |
| NUXT_PUBLIC_NC_BACKEND_URL | Custom Backend URL | ``http://localhost:8080`` will be used |
| NC_REQUEST_BODY_SIZE | Request body size [limit](https://expressjs.com/en/resources/middleware/body-parser.html#limit) | `1048576` |
| NC_EXPORT_MAX_TIMEOUT | After NC_EXPORT_MAX_TIMEOUT, CSV gets downloaded in batches | Default value 5000(in millisecond) will be used |
| NC_DISABLE_TELE | Disable telemetry | |
| NC_DASHBOARD_URL | Custom dashboard URL path | `/dashboard` |
| NC_GOOGLE_CLIENT_ID | Google client ID to enable Google authentication | |
| NC_GOOGLE_CLIENT_SECRET | Google client secret to enable Google authentication | |
| NC_MIGRATIONS_DISABLED | Disable NocoDB migration | |
| NC_MIN | If set to any non-empty string the default splash screen(initial welcome animation) and matrix screensaver will disable | |
| NC_SENTRY_DSN | For Sentry monitoring | |
| NC_REDIS_URL | Custom Redis URL. Example: `redis://:authpassword@127.0.0.1:6380/4` | Meta data will be stored in memory |
| NC_DISABLE_ERR_REPORT | Disable error reporting | |
| NC_DISABLE_CACHE | To be used only while debugging. On setting this to `true` - meta data be fetched from db instead of redis/cache. | `false` |
| AWS_ACCESS_KEY_ID | For Litestream - S3 access key id | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 |
| AWS_SECRET_ACCESS_KEY | For Litestream - S3 secret access key | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 |
| AWS_BUCKET | For Litestream - S3 bucket | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 |
| AWS_BUCKET_PATH | For Litestream - S3 bucket path (like folder within S3 bucket) | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 |
| NC_SMTP_FROM | For SMTP plugin - Email sender address | |
| NC_SMTP_HOST | For SMTP plugin - SMTP host value | |
| NC_SMTP_PORT | For SMTP plugin - SMTP port value | |
| NC_SMTP_USERNAME | For SMTP plugin (Optional) - SMTP username value for authentication | |
| NC_SMTP_PASSWORD | For SMTP plugin (Optional) - SMTP password value for authentication | |
| NC_SMTP_SECURE | For SMTP plugin (Optional) - To enable secure set value as `true` any other value treated as false | |
| NC_SMTP_IGNORE_TLS | For SMTP plugin (Optional) - To ignore tls set value as `true` any other value treated as false. For more info visit https://nodemailer.com/smtp/ | |
| NC_S3_BUCKET_NAME | For S3 storage plugin - AWS S3 bucket name | |
| NC_S3_REGION | For S3 storage plugin - AWS S3 region | |
| NC_S3_ACCESS_KEY | For S3 storage plugin - AWS access key credential for accessing resource | |
| NC_S3_ACCESS_SECRET | For S3 storage plugin - AWS access secret credential for accessing resource | |
| NC_ATTACHMENT_FIELD_SIZE | For setting the attachment field size(in Bytes) | Defaults to 20MB |
| NC_MAX_ATTACHMENTS_ALLOWED | Maximum Number of attachments per cell | |
| NC_ADMIN_EMAIL | For updating/creating super admin with provided email and password | |
| NC_ADMIN_PASSWORD | For updating/creating super admin with provided email and password. Your password should have at least 8 letters with one uppercase, one number and one special letter(Allowed special chars $&+,:;=?@#\|'.^*()%!_-" ) | |
| NODE_OPTIONS | For passing Node.js [options](https://nodejs.org/api/cli.html#node_optionsoptions) to instance | |
| NC_MINIMAL_DBS | Create a new SQLite file for each project. All the db files are stored in `nc_minimal_dbs` folder in current working directory. (This option restricts project creation on external sources) | |
| NC_DISABLE_AUDIT | Disable Audit Log | `false` |
| NC_AUTOMATION_LOG_LEVEL | Possible Values: `OFF`, `ERROR`, `ALL`. See [Webhooks](/automation/webhook/create-webhook#call-log) for details. | `OFF` |
| NC_SECURE_ATTACHMENTS | Allow accessing attachments only through presigned urls. To enable set value as `true` any other value treated as false. (⚠ this will make existing links inaccessible ⚠) | `false` |
| NC_ATTACHMENT_EXPIRE_SECONDS | How many seconds before expiring presigned attachment urls. (Attachments will expire in at least set seconds and at most 10mins after set time) | 7200 (2 hours) |
| NC_ALLOW_LOCAL_HOOKS | To enable set value as `true` any other value treated as false. (⚠ this will allow webhooks to call local links which can raise security issues ⚠) | `false` |
| NC_SANITIZE_COLUMN_NAME | Sanitize the column name during column creation. To enable set value as `true` any other value treated as false. | `true` |
| Variable | Description | If absent |
| -------- | ----------- | --------- |
| `NC_DB` | See our example database URLs [here](https://github.com/nocodb/nocodb#docker). | A local SQLite database is created in root folder if `NC_DB` is not set. |
| `NC_DB_JSON` | Can be used instead of `NC_DB` and value should be valid knex connection JSON string. | |
| `NC_DB_JSON_FILE` | Can be used instead of `NC_DB` and value should be a valid path to knex connection JSON file. | |
| `DATABASE_URL` | Can be used instead of `NC_DB` and value should be a JDBC URL string. | |
| `DATABASE_URL_FILE` | Can be used instead of `NC_DB` and value should be a valid path to a JDBC URL file. | |
| `NC_AUTH_JWT_SECRET` | JWT secret used for auth and storing other secrets. | A random secret is generated. |
| `NC_ADMIN_EMAIL` | Super admin e-mail address. | |
| `NC_ADMIN_PASSWORD` | Super admin password. The password should have at least 8 letters with one uppercase, one number and one special letter. Allowed special characters include `$&+,:;=?@#\|'.^*()%!_-"`. | |
| `PORT` | Network port NocoDB runs on. | Defaults to `8080`. |
| `DB_QUERY_LIMIT_DEFAULT` | Pagination limit. | Defaults to `25`. |
| `DB_QUERY_LIMIT_GROUP_BY_GROUP` | Group per page limit. | Defaults to `10`. |
| `DB_QUERY_LIMIT_GROUP_BY_RECORD` | Record per group limit. | Defaults to `10`. |
| `DB_QUERY_LIMIT_MAX` | Maximum allowed pagination limit. | Defaults to `1000`. |
| `DB_QUERY_LIMIT_MIN` | Minimum allowed pagination limit. | Defaults to `1`. |
| `NC_TOOL_DIR` | App directory to keep metadata and app related files in. | Defaults to the current working directory. In docker, maps to `/usr/app/data/` for mounting volume. |
| `NC_PUBLIC_URL` | Used for sending E-mail invitations. | Best guess from HTTP request params. |
| `NC_JWT_EXPIRES_IN` | JWT token expiry time | Defaults to `10h`. |
| `NC_CONNECT_TO_EXTERNAL_DB_DISABLED` | Disable base creation on external databases. | |
| `NC_MINIMAL_DBS` | Create a new SQLite file for each base. All the SQLite database files are stored in the `nc_minimal_dbs` folder in the current working directory. Enabling this option automatically sets `NC_CONNECT_TO_EXTERNAL_DB_DISABLED`, i.e. disables base creation on external databases. | |
| `NC_INVITE_ONLY_SIGNUP` | Removed since version 0.99.0, and now it's recommended to use the [super admin settings menu](/account-settings/oss-specific-details#enable--disable-signup). Disable public signup and allow signup only via invitations. | |
| `NUXT_PUBLIC_NC_BACKEND_URL` | Custom backend URL. | Defaults to `http://localhost:8080`. |
| `NC_REQUEST_BODY_SIZE` | Request body size [limit](https://expressjs.com/en/resources/middleware/body-parser.html#limit) | Defaults to `1048576`. |
| `NC_EXPORT_MAX_TIMEOUT` | After `NC_EXPORT_MAX_TIMEOUT` (in milliseconds), CSV gets downloaded in batches. | Defaults to `5000` (5 seconds). |
| `NC_DISABLE_TELE` | Disable telemetry. | |
| `NC_DASHBOARD_URL` | Custom dashboard URL path | Defaults to `/dashboard`. |
| `NC_GOOGLE_CLIENT_ID` | Google client ID to enable Google authentication. | |
| `NC_GOOGLE_CLIENT_SECRET` | Google client secret to enable Google authentication. | |
| `NC_MIGRATIONS_DISABLED` | Disable NocoDB migrations. | |
| `NC_MIN` | Disable default splash screen (initial welcome animation) and matrix screensaver. | |
| `NC_SENTRY_DSN` | Data Source Name (DSN) for Sentry monitoring. | |
| `NC_REDIS_URL` | Redis URL. Example: `redis://:authpassword@127.0.0.1:6380/4` | Meta data is stored in memory. |
| `NC_DISABLE_ERR_REPORT` | Disable error reporting. | |
| `NC_DISABLE_CACHE` | Disable cache. To be used only while debugging. If `true`, meta data is fetched from database instead of redis/cache. | Defaults to `false`. |
| `NC_SMTP_FROM` | E-mail sender address for SMTP plugin. | *SMTP plugin is disabled if this variable is not set.* |
| `NC_SMTP_HOST` | E-mail server hostname for SMTP plugin. | *SMTP plugin is disabled if this variable is not set.* |
| `NC_SMTP_PORT` | E-mail server network for SMTP plugin. | *SMTP plugin is disabled if this variable is not set.* |
| `NC_SMTP_USERNAME` | Username for authentication in SMTP plugin. | |
| `NC_SMTP_PASSWORD` | Password for authentication in SMTP plugin. | |
| `NC_SMTP_SECURE` | Enable secure authentication in SMTP plugin. Set to `true` to enable, any other value is treated as `false`. | |
| `NC_SMTP_IGNORE_TLS` | Ignore TLS in SMTP plugin. Set to `true` to ignore TLS, any other value is treated as `false`. For more information, visit [Nodemailer's SMTP documentation](https://nodemailer.com/smtp/). | |
| `NC_S3_BUCKET_NAME` | AWS S3 bucket name for S3 storage plugin. | |
| `NC_S3_REGION` | AWS S3 region for S3 storage plugin. | |
| `NC_S3_ACCESS_KEY` | AWS access key ID for S3 storage plugin. | |
| `NC_S3_ACCESS_SECRET` | AWS access secret for S3 storage plugin. | |
| `NC_ATTACHMENT_FIELD_SIZE` | Maximum file size for [attachments](/fields/field-types/custom-types/attachment/) in bytes. | Defaults to `20971520` (20 MiB). |
| `NC_MAX_ATTACHMENTS_ALLOWED` | Maximum number of attachments per cell. | Defaults to `10`. |
| `NC_SECURE_ATTACHMENTS` | Allow accessing attachments only through pre-signed URLs. Set to `true` to enable, any other value is treated as `false`. (⚠ this will make existing links inaccessible ⚠) | Defaults to `false`. |
| `NC_ATTACHMENT_EXPIRE_SECONDS` | Number of seconds after which pre-signed attachment URLs will begin to expire. The URLs will expire after `NC_ATTACHMENT_EXPIRE_SECONDS` plus 10 minutes at the very latest. | Defaults to `7200` (2 hours). |
| `NC_DISABLE_AUDIT` | Disable audit log. | Defaults to `false`. |
| `NC_AUTOMATION_LOG_LEVEL` | Possible Values: `OFF`, `ERROR`, `ALL`. See [Webhooks](/automation/webhook/create-webhook#call-log) for details. | Defaults to `OFF`. |
| `NC_ALLOW_LOCAL_HOOKS` | ⚠ Allow webhooks to call local links, which can raise security issues. ⚠ Set to `true` to enable, any other value is treated as `false` | Defaults to `false`. |
| `NC_SANITIZE_COLUMN_NAME` | Sanitize the column name during column creation. Set to `true` to enable, any other value is treated as `false` | Defaults to `true`. |
| `NODE_OPTIONS` | Node.js [options](https://nodejs.org/api/cli.html#node_optionsoptions) to pass to instance. | |
| `LITESTREAM_S3_ENDPOINT` | URL of an S3-compatible object storage service endpoint for [Litestream](https://litestream.io/) replication of NocoDB's default SQLite database. Example: `s3.eu-central-1.amazonaws.com` | *Litestream replication is disabled if this variable is not set.* |
| `LITESTREAM_S3_BUCKET` | Name of the object storage bucket to store the Litestream replication in. | *Litestream replication is disabled if this variable is not set.* |
| `LITESTREAM_S3_PATH` | Directory path to use within the Litestream replication object storage bucket. | Defaults to `nocodb`. |
| `LITESTREAM_S3_ACCESS_KEY_ID` | Authentication key ID for the Litestream replication object storage bucket. | *Litestream replication is disabled if this variable is not set.* |
| `LITESTREAM_S3_SECRET_ACCESS_KEY` | Authentication secret for the Litestream replication object storage bucket. | *Litestream replication is disabled if this variable is not set.* |
| `LITESTREAM_S3_SKIP_VERIFY` | Whether to disable TLS verification for the Litestream replication object storage service. This is useful when testing against a local node such as MinIO and you are using self-signed certificates. | Defaults to `false`. |
| `LITESTREAM_RETENTION` | Amount of time Litestream snapshot and WAL files are kept. After the retention period, a new snapshot is created and the old one is removed. WAL files that exist before the oldest snapshot will also be removed. | Defaults to `1440h` (60 days). |
| `LITESTREAM_RETENTION_CHECK_INTERVAL` | Frequency in which Litestream will check if retention needs to be enforced. | Defaults to `72h` (3 days). |
| `LITESTREAM_SNAPSHOT_INTERVAL` | Frequency in which new Litestream snapshots are created. A higher frequency reduces the time to restore since newer snapshots will have fewer WAL frames to apply. Retention still applies to these snapshots. | Defaults to `24h` (1 day). |
| `LITESTREAM_SYNC_INTERVAL` | Frequency in which frames are pushed to the Litestream replica. Increasing this frequency can increase object storage costs significantly. | Defaults to `60s` (1 minute). |

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

@ -62,12 +62,13 @@ Rename file only renames the file in NocoDB display (expand record & tool tip on
:::
## Environment variables
In self-hosted version, you can configure the following environment variables to customize the behavior of `Attachment` field.
- NC_ATTACHMENT_FIELD_SIZE: Max size of attachment file in bytes. Default: 20MB
- NC_SECURE_ATTACHMENTS: Allow accessing attachments only through pre-signed URLs. Default: false
- NC_ATTACHMENT_EXPIRE_SECONDS: Expiry time for pre-signed URLs. Default: 7200
In self-hosted version, you can configure the following environment variables to customize the behavior of `Attachment` field:
- `NC_ATTACHMENT_FIELD_SIZE`: Maximum size of attachment files in bytes. Default: `20971520` (20 MiB)
- `NC_MAX_ATTACHMENTS_ALLOWED`: Maximum number of attachments per cell. Default: `10`
- `NC_SECURE_ATTACHMENTS`: Allow accessing attachments only through pre-signed URLs. Default: `false`
- `NC_ATTACHMENT_EXPIRE_SECONDS`: Expiry time for pre-signed URLs. Default: `7200` (2 hours)
Find more about environment variables [here](/getting-started/self-hosted/environment-variables)
All supported environment variables are described [here](/getting-started/self-hosted/environment-variables).
## Related articles
- [Attaching a file from mobile](/views/view-types/form#attaching-a-file-from-mobile-device)

8
packages/noco-docs/docs/100.data-sources/010.data-source-overview.md

@ -21,10 +21,12 @@ Currently only one external data source can be added per project.
## Accessing `Data Sources`
1. Access Base context menu by clicking on the `Base` name in the left sidebar
2. Click on `Data Sources` tab
1. Access Base context menu by clicking on the `...` in the left sidebar against the base name
2. Click on `Settings` tab
3. In the popup modal, click on `Data Sources` tab
![data source](/img/v2/data-source/data-source.png)
![data source](/img/v2/data-source/data-source-1.png)
![data source](/img/v2/data-source/data-source-2.png)
Learn more about working with Data sources in the following sections:

29
packages/noco-docs/docs/100.data-sources/020.connect-to-data-source.md

@ -7,10 +7,11 @@ keywords: ['NocoDB data source', 'connect data source', 'external data source',
To connect to an external data source, follow the steps below:
1. Access Base context menu by clicking on the `Base` name in the left sidebar
2. Select `Data Sources` tab
3. Click on `+ New Data Source` button
4. On the pop-up modal, provide the following details:
1. Access the Base context menu by clicking on the `...` in the left sidebar against the base name
2. Click on `Settings` tab
3. In the popup modal, click on `Data Sources` tab
4. Click on `+ New Data Source` button
5. On the input modal, provide the following details:
| Field Name | Description |
|---------------|--------------------------------------------------------------------------------------|
@ -23,7 +24,7 @@ To connect to an external data source, follow the steps below:
| Database | Name of the database to connect to |
| Schema name | Name of the schema to connect to |
4a. Optionally, if the connection required is TLS/MTLS for MITM protection, follow these additional steps below:
5a. Optionally, if the connection required is TLS/MTLS for MITM protection, follow these additional steps below:
- Click on `SSL & Advanced Parameters`
- Select `SSL Mode` and upload the client certificate, client key, and Root CA files by clicking on the file.
@ -33,16 +34,18 @@ To connect to an external data source, follow the steps below:
Example: In PostgreSQL when SSL mode set to "Required-Identity," if the server certificate's common name (cname) differs from the actual DNS/IP used for connection, the connection will fail.\
To resolve, add "servername" property with same cname value under the SSL section. Additional details are available at [knex configuration options](https://knexjs.org/guide/#configuration-options).
5. Click on `Test Database Connection` button to verify the connection
6. Wait for the connection to be verified.
- After connection is successful, `Submit` button will be enabled.
- Click on `Submit` button to save the data source.
6. Click on the `Test Database Connection` button to verify the connection
7. Wait for the connection to be verified.
- After test is successful, `Counnect to Data Source` button will be enabled.
- Click on `Connect to Data Source` button to save the data source.
![data source-1](/img/v2/data-source/data-source-connect-1.png)
![data source-2](/img/v2/data-source/data-source-connect-2.png)
![data source-3](/img/v2/data-source/data-source-connect-3.png)
![data source-1](/img/v2/data-source/ds-connect-1.png)
![data source-4](/img/v2/data-source/data-source-connect-4a.png)
![data source-2](/img/v2/data-source/ds-connect-2.png)
![data source-3](/img/v2/data-source/ds-connect-3.png)
![data source-4](/img/v2/data-source/ds-connect-4.png)

18
packages/noco-docs/docs/100.data-sources/030.sync-with-data-source.md

@ -5,17 +5,15 @@ tags: ['Data Sources', 'Sync', 'External', 'PG', 'MySQL']
keywords: ['NocoDB data source', 'connect data source', 'external data source', 'PG data source', 'MySQL data source']
---
1. Access Base context menu by clicking on the `Base` name in the left sidebar
2. Select `Data Sources` tab
3. Click on `Sync Metadata` button listed under `Actions` column for the data source that you wish to sync metadata for
4. Click on `Reload` button to refresh Sync state (Optional)
5. Any changes to the metadata identified will be listed in the `Sync State` column
6. Click on `Sync Now` button to sync the metadata changes
Access `Data Sources` tab in the `Base Settings` to sync changes done in the external data source with NocoDB.
1. Select the data source that you wish to sync metadata for
2. Click on the `Meta Sync` button listed under `Actions` column for the data source that you wish to sync metadata for
3. Click on the `Reload` button to refresh Sync state (Optional)
4. Any changes to the metadata identified will be listed in the `Sync State` column
5. Click on `Sync Now` button to sync the metadata changes
![sync metadata](/img/v2/data-source/data-source-2.png)
![sync metadata](/img/v2/data-source/data-source-meta-sync.png)
![sync metadata](/img/v2/data-source/data-source-meta-sync-1.png)
![sync metadata](/img/v2/data-source/data-source-meta-sync-2.png)
After the sync is complete, you can see the updated state in the `Sync State` column.
Sync modal also marks `Tables metadata is in Sync` in the header.

69
packages/noco-docs/docs/100.data-sources/040.actions-on-data-sources.md

@ -6,24 +6,22 @@ keywords: ['NocoDB data source', 'UI ACL', 'Audit logs', 'Relations', 'Edit', 'U
---
## Edit external database configuration parameters
- Access `Data Sources` tab in the `Base Settings`
- Click on `Connection Details` tab
- Re-configure database credentials as required
1. Access Base context menu by clicking on the `Base` name in the left sidebar
2. Click on `Data Sources` tab
3. Click on `Edit` icon listed under `Actions` column for the data source that you wish to access ERD (Relations view) for
Go to `Data Sources`, click ``Edit`` icon, you can re-configure database credentials.
Please make sure database configuration parameters are valid. Any incorrect parameters could lead to schema loss!
![relations](/img/v2/data-source/data-source-edit.png)
:::info
Please make sure database configuration parameters are valid. Any incorrect parameters could lead to schema loss!
:::
![edit db config](/img/v2/data-source/edit-base.png)
![edit-data-source](/img/v2/data-source/data-source-edit.png)
## Remove data source
1. Access Base context menu by clicking on the `Base` name in the left sidebar
2. Click on `Data Sources` tab
3. Click on `Delete` icon listed under `Actions` column for the data source that you wish to Unlink
- Access `Data Sources` tab in the `Base Settings`
- Click on `Delete` icon listed under `Actions` column for the data source that you wish to remove
![datasource unlink](/img/v2/data-source/data-source-unlink.png)
![datasource unlink](/img/v2/data-source/data-source-remove.png)
:::note
Unlinking a data source will not delete the external data source. It will only remove the data source from the current project.
@ -32,52 +30,45 @@ Unlinking a data source will not delete the external data source. It will only r
## Data source visibility
1. Access Base context menu by clicking on the `Base` name in the left sidebar
2. Click on `Data Sources` tab
3. Toggle radio button listed under `Visibility` column for the data source that you wish to hide/un-hide
- Access `Data Sources` tab in the `Base Settings`
- Toggle radio button listed under `Visibility` column for the data source that you wish to hide/un-hide
![datasource visibility](/img/v2/data-source/data-source-visibility.png)
![datasource visibility](/img/v2/data-source/data-source-hide.png)
## UI Access Control
:::info
UI Access Control is available only in Open Source version of NocoDB.
UI Access Control is available only in Open-Source version of NocoDB.
:::
1. Access Base context menu by clicking on the `Base` name in the left sidebar
2. Click on `Data Sources` tab
3. Click on `UI ACL` button listed under `Actions` column for the data source that you wish to manage UI access control for
4. On the UI ACL modal, you can see the list of tables available in the data source as rows & roles available as columns. Toggle checkboxes to enable/disable access to tables for specific roles.
5. Click on `Save` button to save the changes
Access `Data Sources` tab in the `Base Settings` to manage UI access control for the data source.
1. Click on `UI ACL` button listed under `Actions` column for the data source that you wish to manage UI access control for
2. On the UI ACL modal, you can see the list of tables available in the data source as rows & roles available as columns. Toggle checkboxes to enable/disable access to tables for specific roles.
3. Click on `Save` button to save the changes
![ui acl](/img/v2/data-source/data-source-3.png)
![ui acl](/img/v2/data-source/ui-acl.png)
![ui acl](/img/v2/data-source/data-source-uiacl.png)
## Audit logs
1. Access Base context menu by clicking on the `Base` name in the left sidebar
2. Click on `Data Sources` tab
3. Click on `Audit` button listed under `Actions` column for the data source that you wish to access Audit logs for
![audit](/img/v2/data-source/audit.png)
Access `Data Sources` tab in the `Base Settings` to access Audit logs for the data source.
- Click on `Default` datasource & then
- Access `Audit` tab to view the audit logs.
![audit logs](/img/v2/data-source/audit-logs.png)
![audit](/img/v2/data-source/data-source-audit.png)
:::info
Audit logs are not available for external data source connections.
:::
## Relations
Access `Data Sources` tab in the `Base Settings` to access Relations view for the data source.
- Select the data source that you wish to access ERD (Relations view) for
- Click on `ERD` tab
1. Access Base context menu by clicking on the `Base` name in the left sidebar
2. Click on `Data Sources` tab
3. Click on `Relations` button listed under `Actions` column for the data source that you wish to access ERD (Relations view) for
![relations](/img/v2/data-source/data-source-4.png)
![relations](https://github.com/nocodb/nocodb/assets/86527202/c3775d27-f75d-4263-8903-dd66427de4b4)
![relations](/img/v2/data-source/data-source-erd.png)
### Junction table names within Relations

BIN
packages/noco-docs/static/img/v2/data-source/data-source-1.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
packages/noco-docs/static/img/v2/data-source/data-source-2.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 128 KiB

BIN
packages/noco-docs/static/img/v2/data-source/data-source-audit.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

BIN
packages/noco-docs/static/img/v2/data-source/data-source-edit.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 278 KiB

BIN
packages/noco-docs/static/img/v2/data-source/data-source-erd.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

BIN
packages/noco-docs/static/img/v2/data-source/data-source-hide.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
packages/noco-docs/static/img/v2/data-source/data-source-meta-sync-1.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
packages/noco-docs/static/img/v2/data-source/data-source-meta-sync-2.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

BIN
packages/noco-docs/static/img/v2/data-source/data-source-remove.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
packages/noco-docs/static/img/v2/data-source/data-source-uiacl.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

BIN
packages/noco-docs/static/img/v2/data-source/ds-connect-1.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
packages/noco-docs/static/img/v2/data-source/ds-connect-2.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

BIN
packages/noco-docs/static/img/v2/data-source/ds-connect-3.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

BIN
packages/noco-docs/static/img/v2/data-source/ds-connect-4.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

6
packages/nocodb-sdk/package.json

@ -41,7 +41,7 @@
"dependencies": {
"axios": "^1.6.8",
"jsep": "^1.3.8",
"dayjs": "^1.11.10"
"dayjs": "^1.11.11"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.21.0",
@ -56,8 +56,8 @@
"eslint-plugin-prettier": "^4.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"tsc-alias": "^1.8.8",
"rimraf": "^5.0.7",
"tsc-alias": "^1.8.10",
"typescript": "^5.3.3",
"ts-jest": "^29.1.2"
},

11
packages/nocodb-sdk/src/lib/formulaHelpers.ts

@ -1631,15 +1631,18 @@ export async function validateFormulaAndExtractTreeWithType({
if (parsedTree.type === JSEPNode.CALL_EXP) {
const calleeName = parsedTree.callee.name.toUpperCase();
// validate function name
if (
!formulas[calleeName] ||
sqlUI?.getUnsupportedFnList().includes(calleeName)
) {
if (!formulas[calleeName]) {
throw new FormulaError(
FormulaErrorType.INVALID_FUNCTION_NAME,
{},
`Function ${calleeName} is not available`
);
} else if (sqlUI?.getUnsupportedFnList().includes(calleeName)) {
throw new FormulaError(
FormulaErrorType.INVALID_FUNCTION_NAME,
{},
`Function ${calleeName} is unavailable for your database`
);
}
// validate arguments

45
packages/nocodb/Dockerfile

@ -1,7 +1,9 @@
# syntax=docker/dockerfile:1
###########
# Litestream Builder
###########
FROM golang:alpine3.18 as lt-builder
FROM golang:alpine3.19 as lt-builder
WORKDIR /usr/src/
@ -9,12 +11,9 @@ RUN apk add --no-cache git make musl-dev gcc
# build litestream
RUN git clone https://github.com/benbjohnson/litestream.git litestream
RUN cd litestream ; go install ./cmd/litestream
RUN cd litestream && go install ./cmd/litestream
RUN cp $GOPATH/bin/litestream /usr/src/lt
###########
# Builder
###########
@ -28,11 +27,10 @@ RUN apk add --no-cache python3 make g++
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy application dependency manifests to the container image.
COPY ./package.json ./package.json
COPY ./docker/main.js ./docker/main.js
#COPY ./docker/start.sh /usr/src/appEntry/start.sh
COPY ./docker/start-litestream.sh /usr/src/appEntry/start.sh
COPY src/public/ ./docker/public/
COPY --link ./package.json ./package.json
COPY --link ./docker/main.js ./docker/main.js
COPY --link ./docker/start-litestream.sh /usr/src/appEntry/start.sh
COPY --link src/public/ ./docker/public/
# for pnpm to generate a flat node_modules without symlinks
# so that modclean could work as expected
@ -52,20 +50,27 @@ RUN pnpm install --prod --shamefully-hoist \
FROM alpine:3.19
WORKDIR /usr/src/app
ENV NC_DOCKER 0.6
ENV NODE_ENV production
ENV PORT 8080
ENV NC_TOOL_DIR=/usr/app/data/
RUN apk --update --no-cache add \
ENV LITESTREAM_S3_SKIP_VERIFY=false \
LITESTREAM_S3_PATH=nocodb \
LITESTREAM_RETENTION=1440h \
LITESTREAM_RETENTION_CHECK_INTERVAL=72h \
LITESTREAM_SNAPSHOT_INTERVAL=24h \
LITESTREAM_SYNC_INTERVAL=60s \
NC_DOCKER=0.6 \
NC_TOOL_DIR=/usr/app/data/ \
NODE_ENV=production \
PORT=8080
RUN apk add --update --no-cache \
nodejs \
dumb-init
# Copy litestream binary build
COPY --from=lt-builder /usr/src/lt /usr/src/appEntry/litestream
# Copy litestream binary and config file
COPY --link --from=lt-builder /usr/src/lt /usr/local/bin/litestream
COPY --link ./docker/litestream.yml /etc/litestream.yml
# Copy production code & main entry file
COPY --from=builder /usr/src/app/ /usr/src/app/
COPY --from=builder /usr/src/appEntry/ /usr/src/appEntry/
COPY --link --from=builder /usr/src/app/ /usr/src/app/
COPY --link --from=builder /usr/src/appEntry/ /usr/src/appEntry/
EXPOSE 8080
ENTRYPOINT ["/usr/bin/dumb-init", "--"]

26
packages/nocodb/Dockerfile.local

@ -1,3 +1,5 @@
# syntax=docker/dockerfile:1
###########
# Builder
###########
@ -11,11 +13,11 @@ RUN apk add --no-cache python3 make g++
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy application dependency manifests to the container image.
COPY ./package.json ./package.json
COPY ./docker/nc-gui/ ./docker/nc-gui/
COPY ./docker/main.js ./docker/index.js
COPY ./docker/start-local.sh /usr/src/appEntry/start.sh
COPY src/public/ ./docker/public/
COPY --link ./package.json ./package.json
COPY --link ./docker/nc-gui/ ./docker/nc-gui/
COPY --link ./docker/main.js ./docker/index.js
COPY --link ./docker/start-local.sh /usr/src/appEntry/start.sh
COPY --link src/public/ ./docker/public/
# for pnpm to generate a flat node_modules without symlinks
# so that modclean could work as expected
@ -36,20 +38,20 @@ RUN pnpm install --prod --shamefully-hoist --reporter=silent \
FROM alpine:3.19
WORKDIR /usr/src/app
ENV NC_DOCKER 0.6
ENV NODE_ENV production
ENV PORT 8080
ENV NC_TOOL_DIR=/usr/app/data/
ENV NC_DOCKER=0.6 \
NC_TOOL_DIR=/usr/app/data/ \
NODE_ENV=production \
PORT=8080
RUN apk --update --no-cache add \
RUN apk add --update --no-cache \
nodejs \
dumb-init \
curl \
jq
# Copy production code & main entry file
COPY --from=builder /usr/src/app/ /usr/src/app/
COPY --from=builder /usr/src/appEntry/ /usr/src/appEntry/
COPY --link --from=builder /usr/src/app/ /usr/src/app/
COPY --link --from=builder /usr/src/appEntry/ /usr/src/appEntry/
EXPOSE 8080
ENTRYPOINT ["/usr/bin/dumb-init", "--"]

22
packages/nocodb/docker/litestream.yml

@ -0,0 +1,22 @@
# Docs: https://litestream.io/reference/config/
dbs:
- path: ${NC_TOOL_DIR}noco.db
replicas:
- type: s3
endpoint: ${LITESTREAM_S3_ENDPOINT}
force-path-style: true
skip-verify: ${LITESTREAM_S3_SKIP_VERIFY}
bucket: ${LITESTREAM_S3_BUCKET}
path: ${LITESTREAM_S3_PATH}
access-key-id: ${LITESTREAM_S3_ACCESS_KEY_ID}
secret-access-key: ${LITESTREAM_S3_SECRET_ACCESS_KEY}
retention: ${LITESTREAM_RETENTION}
retention-check-interval: ${LITESTREAM_RETENTION_CHECK_INTERVAL}
snapshot-interval: ${LITESTREAM_SNAPSHOT_INTERVAL}
sync-interval: ${LITESTREAM_SYNC_INTERVAL}
# age:
# identities:
# - ${LITESTREAM_AGE_SECRET_KEY}
# recipients:
# - ${LITESTREAM_AGE_PUBLIC_KEY}

35
packages/nocodb/docker/start-litestream.sh

@ -1,26 +1,37 @@
#!/bin/sh
#sleep 5
if [ -n "${NC_TOOL_DIR}" ]; then
if [ ! -d "${NC_TOOL_DIR}" ] ; then
mkdir -p "$NC_TOOL_DIR"
fi
if [ -n "${AWS_ACCESS_KEY_ID}" ] && [ -n "${AWS_SECRET_ACCESS_KEY}" ] && [ -n "${AWS_BUCKET}" ] && [ -n "${AWS_BUCKET_PATH}" ]; then
use_litestream() {
[ -z "${NC_DB}" ] \
&& [ -z "${NC_DB_JSON}" ] \
&& [ -z "${NC_DB_JSON_FILE}" ] \
&& [ -z "${DATABASE_URL}" ] \
&& [ -z "${DATABASE_URL_FILE}" ] \
&& [ -z "${NC_MINIMAL_DBS}" ] \
&& [ -n "${LITESTREAM_S3_ENDPOINT}" ] \
&& [ -n "${LITESTREAM_S3_BUCKET}" ] \
&& [ -n "${LITESTREAM_ACCESS_KEY_ID}" ] \
&& [ -n "${LITESTREAM_SECRET_ACCESS_KEY}" ]
}
if use_litestream ; then
if [ -f "${NC_TOOL_DIR}noco.db" ]
then
if [ -f "${NC_TOOL_DIR}noco.db" ] ; then
rm "${NC_TOOL_DIR}noco.db"
rm "${NC_TOOL_DIR}noco.db-shm"
rm "${NC_TOOL_DIR}noco.db-wal"
rm -f "${NC_TOOL_DIR}noco.db-shm"
rm -f "${NC_TOOL_DIR}noco.db-wal"
fi
/usr/src/appEntry/litestream restore -o "${NC_TOOL_DIR}noco.db" "s3://$AWS_BUCKET/$AWS_BUCKET_PATH"
if [ ! -f "${NC_TOOL_DIR}noco.db" ]
then
litestream restore "${NC_TOOL_DIR}noco.db"
if [ ! -f "${NC_TOOL_DIR}noco.db" ] ; then
touch "${NC_TOOL_DIR}noco.db"
fi
/usr/src/appEntry/litestream replicate "${NC_TOOL_DIR}noco.db" "s3://$AWS_BUCKET/$AWS_BUCKET_PATH" &
litestream replicate &
fi
node docker/main.js

2
packages/nocodb/docker/start-local.sh

@ -1,6 +1,6 @@
#!/bin/sh
if [ -n "${NC_TOOL_DIR}" ]; then
if [ ! -d "${NC_TOOL_DIR}" ]; then
mkdir -p "$NC_TOOL_DIR"
fi

96
packages/nocodb/litestream/Dockerfile

@ -1,96 +0,0 @@
FROM golang:alpine3.18 as lt
WORKDIR /usr/src/
RUN apk add --no-cache git make musl-dev gcc
# build litestream
RUN git clone https://github.com/benbjohnson/litestream.git litestream
RUN cd litestream ; go install ./cmd/litestream
RUN cp $GOPATH/bin/litestream /usr/src/lt
FROM node:18.19.1-alpine as builder
WORKDIR /usr/src/app
# install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy application dependency manifests to the container image.
COPY ./package*.json ./
COPY ./docker/main.js ./docker/main.js
#COPY ./docker/start.sh /usr/src/appEntry/start.sh
COPY ./docker/start-litestream.sh /usr/src/appEntry/start.sh
# for pnpm to generate a flat node_modules without symlinks
# so that modclean could work as expected
RUN echo "node-linker=hoisted" > .npmrc
# install production dependencies,
# reduce node_module size with modclean & removing sqlite deps,
# and add execute permission to start.sh
RUN pnpm install --prod --shamefully-hoist --reporter=silent
RUN pnpm dlx modclean --patterns="default:*" --ignore="nc-lib-gui/**,dayjs/**,express-status-monitor/**" --run
RUN rm -rf ./node_modules/sqlite3/deps
RUN chmod +x /usr/src/appEntry/start.sh
FROM alpine:3.19
#ENV AWS_ACCESS_KEY_ID=
#ENV AWS_SECRET_ACCESS_KEY=
#ENV AWS_BUCKET=
#WORKDIR /usr/src/
#
## Install go lang
#RUN apk add --no-cache git make musl-dev go
#
## Configure Go
#ENV GOROOT /usr/lib/go
#ENV GOPATH /go
#ENV PATH /go/bin:$PATH
#
#RUN mkdir -p ${GOPATH}/src ${GOPATH}/bin
#
## build litestream
#
#RUN git clone https://github.com/benbjohnson/litestream.git litestream
#RUN cd litestream ; go install ./cmd/litestream
# Bug fix for segfault ( Convert PT_GNU_STACK program header into PT_PAX_FLAGS )
#RUN apk --update --no-cache add paxctl \
# && paxctl -cm $(which node)
WORKDIR /usr/src/app
ENV NC_DOCKER 0.6
ENV PORT 8080
ENV NC_TOOL_DIR=/usr/app/data/
# Copy application dependency manifests to the container image.
# A wildcard is used to ensure both package.json AND package-lock.json are copied.
# Copying this separately prevents re-running npm install on every code change.
#COPY ./build/ ./build/
#COPY ./docker/main.js ./docker/main.js
#COPY ./package.json ./
RUN apk --update --no-cache add \
nodejs \
tar
# Copy litestream binary build
COPY --from=lt /usr/src/lt /usr/src/appEntry/litestream
# Copy production code & main entry file
COPY --from=builder /usr/src/app/ /usr/src/app/
COPY --from=builder /usr/src/appEntry/ /usr/src/appEntry/
# Run the web service on container startup.
#CMD [ "node", "docker/index.js" ]
ENTRYPOINT ["sh", "/usr/src/appEntry/start.sh"]

32
packages/nocodb/package.json

@ -53,34 +53,34 @@
"@graphql-tools/merge": "^6.2.17",
"@jm18457/kafkajs-msk-iam-authentication-mechanism": "^3.1.2",
"@nestjs/bull": "^10.0.1",
"@nestjs/common": "^10.3.7",
"@nestjs/common": "^10.3.8",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.3.7",
"@nestjs/core": "^10.3.8",
"@nestjs/event-emitter": "^2.0.4",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "^2.0.5",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.3.7",
"@nestjs/platform-socket.io": "^10.3.7",
"@nestjs/platform-express": "^10.3.8",
"@nestjs/platform-socket.io": "^10.3.8",
"@nestjs/serve-static": "^4.0.2",
"@nestjs/throttler": "^5.1.2",
"@nestjs/websockets": "^10.3.7",
"@nestjs/websockets": "^10.3.8",
"@ntegral/nestjs-sentry": "^4.0.1",
"@sentry/node": "^6.19.7",
"@techpass/passport-openidconnect": "^0.3.3",
"@types/chai": "^4.3.14",
"@types/chai": "^4.3.16",
"airtable": "^0.12.2",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"archiver": "^5.3.2",
"auto-bind": "^4.0.0",
"aws-kcl": "^2.2.5",
"aws-kcl": "^2.2.6",
"aws-sdk": "^2.1550.0",
"axios": "^1.6.8",
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
"boxen": "^5.1.2",
"bull": "^4.12.2",
"bull": "^4.12.5",
"bullmq": "^1.91.1",
"clear": "^0.1.0",
"clickhouse": "^2.6.0",
@ -92,10 +92,10 @@
"cron": "^1.8.2",
"crypto-js": "^4.2.0",
"dataloader": "^2.2.2",
"dayjs": "^1.11.10",
"dayjs": "^1.11.11",
"debug": "^4.3.4",
"dotenv": "^8.6.0",
"ejs": "^3.1.9",
"ejs": "^3.1.10",
"emittery": "^0.13.1",
"express": "^4.18.3",
"extract-zip": "^2.0.1",
@ -109,7 +109,7 @@
"html-to-json-parser": "^2.0.1",
"import-fresh": "^3.3.0",
"inflection": "^1.13.4",
"ioredis": "^5.3.2",
"ioredis": "^5.4.1",
"ioredis-mock": "^8.9.0",
"is-docker": "^2.2.1",
"isomorphic-dompurify": "^1.13.0",
@ -129,7 +129,7 @@
"morgan": "^1.10.0",
"mssql": "^10.0.2",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.9.3",
"mysql2": "^3.9.7",
"nanoid": "^3.3.7",
"nc-help": "0.3.1",
"nc-lib-gui": "0.207.1",
@ -137,7 +137,7 @@
"ncp": "^2.0.0",
"nestjs-kafka": "^1.0.6",
"nestjs-throttler-storage-redis": "^0.4.4",
"nocodb-sdk": "0.207.1",
"nocodb-sdk": "workspace:^",
"nodemailer": "^6.9.13",
"object-hash": "^3.0.0",
"object-sizeof": "^2.6.4",
@ -177,7 +177,7 @@
"devDependencies": {
"@nestjs/cli": "^10.3.2",
"@nestjs/schematics": "^10.1.1",
"@nestjs/testing": "^10.3.7",
"@nestjs/testing": "^10.3.8",
"@nestjsplus/dyn-schematics": "^1.0.12",
"@types/ejs": "^3.1.5",
"@types/express": "^4.17.21",
@ -185,7 +185,7 @@
"@types/mocha": "^10.0.6",
"@types/multer": "^1.4.11",
"@types/node": "20.11.30",
"@types/passport-google-oauth20": "^2.0.14",
"@types/passport-google-oauth20": "^2.0.16",
"@types/passport-jwt": "^3.0.13",
"@types/supertest": "^2.0.16",
"@typescript-eslint/eslint-plugin": "^6.21.0",
@ -229,4 +229,4 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
}

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

@ -2817,7 +2817,7 @@ class BaseModelSqlv2 {
? response?.[0]?.[ai.id]
: response?.[ai.id];
response = await this.readByPk(
this.extractCompositePK({ rowId: id, insertObj, ag }),
this.extractCompositePK({ rowId: id, insertObj, ag, ai }),
false,
{},
{ ignoreView: true, getHiddenColumn: true },
@ -3250,8 +3250,8 @@ class BaseModelSqlv2 {
insertObj,
force = false,
}: {
ai?: Column<any>;
ag?: Column<any>;
ai: Column<any>;
ag: Column<any>;
rowId;
insertObj: Record<string, any>;
force?: boolean;

1
packages/nocodb/src/interface/Jobs.ts

@ -11,6 +11,7 @@ export enum JobTypes {
UpdateModelStat = 'update-model-stat',
UpdateWsStat = 'update-ws-stats',
UpdateSrcStat = 'update-source-stat',
HealthCheck = 'health-check',
}
export enum JobStatus {

76
packages/nocodb/src/models/Source.ts

@ -76,6 +76,10 @@ export default class Source implements SourceType {
insertObj.meta = stringifyMetaProp(insertObj);
}
insertObj.order = await ncMeta.metaGetNextOrder(MetaTable.BASES, {
base_id: source.baseId,
});
const { id } = await ncMeta.metaInsert2(
source.baseId,
null,
@ -92,8 +96,6 @@ export default class Source implements SourceType {
`${CacheScope.BASE}:${id}`,
);
await this.reorderBases(source.baseId);
return returnBase;
}
@ -101,7 +103,6 @@ export default class Source implements SourceType {
sourceId: string,
source: SourceType & {
baseId: string;
skipReorder?: boolean;
meta?: any;
deleted?: boolean;
fk_sql_executor_id?: string;
@ -138,6 +139,36 @@ export default class Source implements SourceType {
updateObj.type = oldBase.type;
}
// if order is missing (possible in old versions), get next order
if (!oldBase.order && !updateObj.order) {
updateObj.order = await ncMeta.metaGetNextOrder(MetaTable.BASES, {
base_id: source.baseId,
});
if (updateObj.order <= 1 && !oldBase.isMeta()) {
updateObj.order = 2;
}
}
// keep order 1 for default source
if (oldBase.isMeta()) {
updateObj.order = 1;
}
// keep order 1 for default source
if (!oldBase.isMeta()) {
if (updateObj.order <= 1) {
NcError.badRequest('Cannot change order to 1 or less');
}
// if order is 1 for non-default source, move it to last
if (oldBase.order <= 1 && !updateObj.order) {
updateObj.order = await ncMeta.metaGetNextOrder(MetaTable.BASES, {
base_id: source.baseId,
});
}
}
await ncMeta.metaUpdate(
source.baseId,
null,
@ -154,10 +185,6 @@ export default class Source implements SourceType {
// call before reorder to update cache
const returnBase = await this.get(oldBase.id, false, ncMeta);
if (!source.skipReorder && source.order && source.order !== oldBase.order) {
await this.reorderBases(source.baseId, returnBase.id, ncMeta);
}
return returnBase;
}
@ -288,41 +315,6 @@ export default class Source implements SourceType {
return this.castType(source);
}
static async reorderBases(
baseId: string,
keepBase?: string,
ncMeta = Noco.ncMeta,
) {
const sources = await this.list({ baseId: baseId }, ncMeta);
if (keepBase) {
const kpBase = sources.splice(
sources.indexOf(sources.find((source) => source.id === keepBase)),
1,
);
if (kpBase.length) {
sources.splice(kpBase[0].order - 1, 0, kpBase[0]);
}
}
// update order for sources
for (const [i, b] of Object.entries(sources)) {
b.order = parseInt(i) + 1;
await ncMeta.metaUpdate(
b.base_id,
null,
MetaTable.BASES,
{
order: b.order,
},
b.id,
);
await NocoCache.set(`${CacheScope.BASE}:${b.id}`, b);
}
}
public async getConnectionConfig(): Promise<any> {
const config = this.getConfig();

28
packages/nocodb/src/modules/jobs/jobs/health-check.processor.ts

@ -0,0 +1,28 @@
import { Process, Processor } from '@nestjs/bull';
import { Inject, Logger } from '@nestjs/common';
import type { Queue } from 'bull';
import { JOBS_QUEUE, JobTypes } from '~/interface/Jobs';
@Processor(JOBS_QUEUE)
export class HealthCheckProcessor {
private logger = new Logger(HealthCheckProcessor.name);
constructor(@Inject('JobsService') protected readonly jobsService) {}
@Process(JobTypes.HealthCheck)
async healthCheck() {
const queue = this.jobsService.jobsQueue as Queue;
if (queue) {
queue
.getJobCounts()
.then((stats) => {
// log stats periodically
this.logger.log({ stats });
})
.catch((err) => {
this.logger.error(err);
});
}
}
}

37
packages/nocodb/src/modules/jobs/redis/jobs.service.ts

@ -21,25 +21,24 @@ export class JobsService implements OnModuleInit {
// pause primary instance queue
async onModuleInit() {
if (process.env.NC_WORKER_CONTAINER !== 'true') {
await this.jobsQueue.pause(true);
} else {
this.jobsRedisService.workerCallbacks[InstanceCommands.RESUME_LOCAL] =
async () => {
this.logger.log('Resuming local queue');
await this.jobsQueue.resume(true);
};
this.jobsRedisService.workerCallbacks[InstanceCommands.PAUSE_LOCAL] =
async () => {
this.logger.log('Pausing local queue');
await this.jobsQueue.pause(true);
};
}
await this.toggleQueue();
this.jobsRedisService.workerCallbacks[InstanceCommands.RESUME_LOCAL] =
async () => {
this.logger.log('Resuming local queue');
await this.jobsQueue.resume(true);
};
this.jobsRedisService.workerCallbacks[InstanceCommands.PAUSE_LOCAL] =
async () => {
this.logger.log('Pausing local queue');
await this.jobsQueue.pause(true);
};
}
async add(name: string, data: any) {
// if NC_WORKER_CONTAINER is false, then skip dynamic queue pause/resume
if (process.env.NC_WORKER_CONTAINER !== 'false') {
async toggleQueue() {
if (process.env.NC_WORKER_CONTAINER === 'false') {
await this.jobsQueue.pause(true);
} else if (process.env.NC_WORKER_CONTAINER !== 'true') {
// resume primary instance queue if there is no worker
const workerCount = await this.jobsRedisService.workerCount();
const localWorkerPaused = await this.jobsQueue.isPaused(true);
@ -52,6 +51,10 @@ export class JobsService implements OnModuleInit {
await this.jobsQueue.pause(true);
}
}
}
async add(name: string, data: any) {
await this.toggleQueue();
const job = await this.jobsQueue.add(name, data);

136
packages/nocodb/src/services/command-palette.service.ts

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { type UserType, ViewTypes } from 'nocodb-sdk';
import { Base } from '~/models';
import { BaseUser } from '~/models';
import { TablesService } from '~/services/tables.service';
import { deserializeJSON } from '~/utils/serialize';
@ -20,95 +20,73 @@ export class CommandPaletteService {
async commandPalette(param: { body: any; user: UserType }) {
const cmdData = [];
try {
const { scope } = param.body;
const allBases = [];
if (scope === 'root') {
const bases = await Base.list({ user: param.user });
const bases = await BaseUser.getProjectsList(param.user.id, param);
for (const base of bases) {
cmdData.push({
id: `p-${base.id}`,
title: base.title,
icon: 'project',
iconColor: deserializeJSON(base.meta)?.iconColor,
section: 'Bases',
scopePayload: {
scope: `p-${base.id}`,
data: {
base_id: base.id,
},
},
});
}
} else if (scope.startsWith('p-')) {
const allBases = [];
const bases = await Base.list({ user: param.user });
allBases.push(...bases);
allBases.push(...bases);
const viewList = [];
const viewList = [];
for (const base of bases) {
viewList.push(
...(
(await this.tablesService.xcVisibilityMetaGet(
base.id,
null,
false,
)) as any[]
).filter((v) => {
return Object.keys(param.user.roles).some(
(role) => param.user.roles[role] && !v.disabled[role],
);
}),
);
}
for (const base of bases) {
viewList.push(
...(
(await this.tablesService.xcVisibilityMetaGet(
base.id,
null,
false,
)) as any[]
).filter((v) => {
return Object.keys(param.user.roles).some(
(role) => param.user.roles[role] && !v.disabled[role],
);
}),
);
}
const tableList = [];
const vwList = [];
const tableList = [];
const vwList = [];
for (const b of allBases) {
cmdData.push({
id: `p-${b.id}`,
title: b.title,
icon: 'project',
iconColor: deserializeJSON(b.meta)?.iconColor,
section: 'Bases',
});
}
for (const b of allBases) {
cmdData.push({
id: `p-${b.id}`,
title: b.title,
icon: 'project',
iconColor: deserializeJSON(b.meta)?.iconColor,
section: 'Bases',
});
}
for (const v of viewList) {
if (!tableList.find((el) => el.id === `tbl-${v.fk_model_id}`)) {
tableList.push({
id: `tbl-${v.fk_model_id}`,
title: v._ptn,
parent: `p-${v.base_id}`,
icon: v?.table_meta?.icon || v.ptype,
projectName: bases.find((el) => el.id === v.base_id)?.title,
section: 'Tables',
});
}
vwList.push({
id: `vw-${v.id}`,
title: `${v.title}`,
parent: `tbl-${v.fk_model_id}`,
icon: v?.meta?.icon || viewTypeAlias[v.type] || 'table',
for (const v of viewList) {
if (!tableList.find((el) => el.id === `tbl-${v.fk_model_id}`)) {
tableList.push({
id: `tbl-${v.fk_model_id}`,
title: v._ptn,
parent: `p-${v.base_id}`,
icon: v?.table_meta?.icon || v.ptype,
projectName: bases.find((el) => el.id === v.base_id)?.title,
section: 'Views',
is_default: v?.is_default,
handler: {
type: 'navigate',
payload: `/nc/${v.base_id}/${v.fk_model_id}/${encodeURIComponent(
v.id,
)}`,
},
section: 'Tables',
});
}
cmdData.push(...tableList);
cmdData.push(...vwList);
vwList.push({
id: `vw-${v.id}`,
title: `${v.title}`,
parent: `tbl-${v.fk_model_id}`,
icon: v?.meta?.icon || viewTypeAlias[v.type] || 'table',
projectName: bases.find((el) => el.id === v.base_id)?.title,
section: 'Views',
is_default: v?.is_default,
handler: {
type: 'navigate',
payload: `/nc/${v.base_id}/${v.fk_model_id}/${encodeURIComponent(
v.id,
)}`,
},
});
}
cmdData.push(...tableList);
cmdData.push(...vwList);
} catch (e) {
console.log(e);
return [];

1
packages/nocodb/src/services/org-users.service.ts

@ -52,7 +52,6 @@ export class OrgUsersService {
return await User.update(param.userId, {
...updateBody,
token_version: randomTokenString(),
});
}

1
packages/nocodb/src/utils/acl.ts

@ -276,6 +276,7 @@ const rolePermissions:
baseList: true,
testConnection: true,
isPluginActive: true,
commandPalette: true,
},
},
[OrgUserRoles.CREATOR]: {

6
packages/nocodb/src/utils/emailUtils.ts

@ -8,8 +8,8 @@ const encode = (str: string) => {
// a method to sanitise content and avoid any link/url injection in email content and html encode special chars
// for example: example.com to be converted as example<span>.<span>com
export const sanitiseEmailContent = (content: string) => {
export const sanitiseEmailContent = (content?: string) => {
return content
.replace(/[<>&;?#,'"$]+/g, encode)
.replace(/\.|\/\/:/g, '<span>$&</span>');
?.replace(/[<>&;?#,'"$]+/g, encode)
?.replace(/\.|\/\/:/g, '<span>$&</span>');
};

1
packages/nocodb/src/version-upgrader/ncProjectConfigUpgrader.ts

@ -37,7 +37,6 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
id: source.id,
baseId: source.base_id,
config,
skipReorder: true,
},
ncMeta,
),

3479
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

4
scripts/pkg-executable/package.json

@ -27,8 +27,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"@nestjs/common": "^10.3.7",
"@nestjs/core": "^10.3.7",
"@nestjs/common": "^10.3.8",
"@nestjs/core": "^10.3.8",
"express": "^4.18.3",
"nocodb": "0.204.9"
},

1
scripts/upgradeNcGui.js

@ -36,7 +36,6 @@ if (process.env.targetEnv === 'DEV') {
// replace nc-lib-gui by nc-lib-gui-daily if it is nightly build / pr release
const filePaths = [
path.join(__dirname, '..', 'packages', 'nocodb', 'Dockerfile'),
path.join(__dirname, '..', 'packages', 'nocodb', 'litestream', 'Dockerfile'),
path.join(__dirname, '..', 'packages', 'nocodb', 'package.json'),
path.join(__dirname, '..', 'packages', 'nocodb', 'src', 'Noco.ts'),
path.join(__dirname, '..', 'packages', 'nocodb', 'src', 'nocobuild.ts'),

4
tests/playwright/pages/Dashboard/Calendar/CalendarWeekDateTime.ts

@ -59,8 +59,8 @@ export class CalendarWeekDateTimePage extends BasePage {
hour.click({
force: true,
position: {
x: 1,
y: 1,
x: 0,
y: 0,
},
}),
requestUrlPathToMatch: '/api/v1/db/data/noco',

4
tests/playwright/pages/Dashboard/ProjectView/Metadata.ts

@ -25,7 +25,9 @@ export class MetaDataPage extends BasePage {
await this.get().click();
await this.rootPage.keyboard.press('Escape');
await this.rootPage.keyboard.press('Escape');
await this.get().waitFor({ state: 'detached' });
await this.rootPage.waitForSelector('div.ant-modal-content', {
state: 'hidden',
});
}
async sync() {

3
tests/playwright/pages/Dashboard/ProjectView/index.ts

@ -54,12 +54,9 @@ export class ProjectViewPage extends BasePage {
expect(await this.tab_allTables.isVisible()).toBeTruthy();
if (role.toLowerCase() === 'creator' || role.toLowerCase() === 'owner') {
await this.tab_dataSources.waitFor({ state: 'visible' });
await this.tab_accessSettings.waitFor({ state: 'visible' });
expect(await this.tab_dataSources.isVisible()).toBeTruthy();
expect(await this.tab_accessSettings.isVisible()).toBeTruthy();
} else {
expect(await this.tab_dataSources.isVisible()).toBeFalsy();
expect(await this.tab_accessSettings.isVisible()).toBeFalsy();
}

30
tests/playwright/pages/Dashboard/Settings/DataSources.ts

@ -20,21 +20,35 @@ export class DataSourcesPage extends BasePage {
}
get() {
return this.settings.get().locator(`[data-testid="nc-settings-subtab-Data Sources"]`);
return this.settings.get().locator('[data-testid="nc-settings-datasources"]');
}
async openErd({ dataSourceName }: { dataSourceName: string }) {
await this.get().locator('.ds-table-row', { hasText: dataSourceName }).locator('button:has-text("ERD")').click();
async openErd({ rowIndex }: { rowIndex: number }) {
const row = this.get()
.locator('.ds-table-row')
.nth(rowIndex + 1);
await row.click();
await this.get().getByTestId('nc-erd-tab').click();
}
async openAudit({ rowIndex }: { rowIndex: number }) {
const row = this.get()
.locator('.ds-table-row')
.nth(rowIndex + 1);
await row.click();
await this.get().getByTestId('nc-audit-tab').click();
}
async openAcl({ dataSourceName = defaultBaseName }: { dataSourceName?: string } = {}) {
await this.get().locator('.ds-table-row', { hasText: dataSourceName }).locator('button:has-text("UI ACL")').click();
}
async openMetaSync({ dataSourceName = defaultBaseName }: { dataSourceName?: string } = {}) {
await this.get()
.locator('.ds-table-row', { hasText: dataSourceName })
.locator('button:has-text("Sync Metadata")')
.click();
async openMetaSync({ rowIndex }: { rowIndex: number }) {
// 0th offset for header
const row = this.get()
.locator('.ds-table-row')
.nth(rowIndex + 1);
await row.click();
await this.get().getByTestId('nc-meta-sync-tab').click();
}
}

4
tests/playwright/pages/Dashboard/TreeView.ts

@ -182,7 +182,7 @@ export class TreeViewPage extends BasePage {
await this.waitForTableOptions({ title });
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator('.nc-tbl-context-menu').click();
await this.rootPage.locator('.ant-dropdown').locator('.nc-menu-item:has-text("Delete")').click();
await this.rootPage.locator('.ant-dropdown').locator('.nc-menu-item.nc-table-delete:has-text("Delete")').click();
await this.waitForResponse({
uiAction: async () => {
@ -205,7 +205,7 @@ export class TreeViewPage extends BasePage {
await this.waitForTableOptions({ title });
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator('.nc-tbl-context-menu').click();
await this.rootPage.locator('.ant-dropdown').locator('.nc-menu-item:has-text("Rename")').click();
await this.rootPage.locator('.ant-dropdown').locator('.nc-table-rename.nc-menu-item:has-text("Rename")').click();
await this.dashboard.get().locator('[placeholder="Enter table name"]').fill(newTitle);
await this.dashboard.get().locator('button:has-text("Rename Table")').click();

6
tests/playwright/tests/db/features/erd.spec.ts

@ -45,8 +45,10 @@ test.describe('Erd', () => {
};
const openProjectErd = async () => {
await dashboard.baseView.tab_dataSources.click();
await dashboard.baseView.dataSources.openERD({ rowIndex: 0 });
await dashboard.treeView.baseSettings({ title: context.base.title });
await dashboard.settings.selectTab({ tab: 'dataSources' });
await dashboard.settings.dataSources.openErd({ rowIndex: 0 });
// await dashboard.baseView.dataSources.openERD({ rowIndex: 0 });
};
const openErdOfATable = async (tableName: string) => {

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

Loading…
Cancel
Save