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. 4
      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. 425
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  9. 93
      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. 30
      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. 8
      packages/nc-gui/composables/useCommandPalette/index.ts
  38. 21
      packages/nc-gui/composables/useFormViewStore.ts
  39. 21
      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. 67
      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. 30
      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. 17
      packages/nocodb/src/modules/jobs/redis/jobs.service.ts
  87. 26
      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": { "pnpm": {
"overrides": { "overrides": {
"vue": "3.3.13", "vue": "latest",
"typescript": "latest", "typescript": "latest",
"ajv@<6.12.3": ">=6.12.3", "ajv@<6.12.3": ">=6.12.3",
"node.extend@<1.1.7": ">=1.1.7", "node.extend@<1.1.7": ">=1.1.7",
@ -56,7 +56,8 @@
"axios@>=0.8.1 <0.28.0": ">=0.28.0", "axios@>=0.8.1 <0.28.0": ">=0.28.0",
"ip@<1.1.9": ">=1.1.9", "ip@<1.1.9": ">=1.1.9",
"ip@=2.0.0": ">=2.0.1", "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"
} }
} }
} }

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

@ -19,6 +19,10 @@ body {
--navbar-bg: #fafafa; --navbar-bg: #fafafa;
--navbar-border: #e0e0e0; --navbar-border: #e0e0e0;
--nc-grid-bg: #fdfdfd; --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 { ::-moz-selection {

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 // To prevent event listener on non active cell
if (!active.value) return 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) { switch (e.key) {
case ';': 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 // To prevent event listener on non active cell
if (!active.value) return 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) { switch (e.key) {
case ';': 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 // To prevent event listener on non active cell
if (!active.value) return 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) { switch (e.key) {
case ';': 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 // To prevent event listener on non active cell
if (!active.value) return 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) { switch (e.key) {
case ';': case ';':

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

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

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

@ -18,8 +18,6 @@ const vReload = useVModel(props, 'reload', emits)
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { t } = useI18n()
const basesStore = useBases() const basesStore = useBases()
const { loadProject } = basesStore const { loadProject } = basesStore
const { isDataSourceLimitReached } = storeToRefs(basesStore) const { isDataSourceLimitReached } = storeToRefs(basesStore)
@ -27,6 +25,8 @@ const { isDataSourceLimitReached } = storeToRefs(basesStore)
const baseStore = useBase() const baseStore = useBase()
const { base } = storeToRefs(baseStore) const { base } = storeToRefs(baseStore)
const { isUIAllowed } = useRoles()
const { projectPageTab } = storeToRefs(useConfigStore()) const { projectPageTab } = storeToRefs(useConfigStore())
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
@ -42,6 +42,44 @@ const isReloading = ref(false)
const isDeleteBaseModalOpen = ref(false) const isDeleteBaseModalOpen = ref(false)
const toBeDeletedBase = ref<SourceType | undefined>() 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) { async function loadBases(changed?: boolean) {
try { try {
if (changed) refreshCommandPalette() if (changed) refreshCommandPalette()
@ -53,6 +91,7 @@ async function loadBases(changed?: boolean) {
if (baseList.list && baseList.list.length) { if (baseList.list && baseList.list.length) {
sources.value = baseList.list sources.value = baseList.list
} }
await updateIfSourceOrderIsNullOrDuplicate()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally { } finally {
@ -90,7 +129,7 @@ const deleteBase = async () => {
refreshCommandPalette() refreshCommandPalette()
} }
} }
const toggleBase = async (source: BaseType, state: boolean) => { const toggleBase = async (source: SourceType, state: boolean) => {
try { try {
if (!state && sources.value.filter((src) => src.enabled).length < 2) { if (!state && sources.value.filter((src) => src.enabled).length < 2) {
message.info('There should be at least one enabled source!') message.info('There should be at least one enabled source!')
@ -116,21 +155,27 @@ const moveBase = async (e: any) => {
// sources list is mutated so we have to get the new index and mirror it to backend // sources list is mutated so we have to get the new index and mirror it to backend
const source = sources.value[e.newIndex] const source = sources.value[e.newIndex]
if (source) { if (source) {
if (!source.order) { let nextOrder: number
// empty update call to reorder sources (migration)
await $api.source.update(source.base_id as string, source.id as string, { // set new order value based on the new order of the items
id: source.id, if (sources.value.length - 1 === e.newIndex) {
base_id: source.base_id, // 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
message.info(t('info.basesMigrated'))
} else { } else {
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, { await $api.source.update(source.base_id as string, source.id as string, {
id: source.id, id: source.id,
base_id: source.base_id, base_id: source.base_id,
order: e.newIndex + 1, order: _nextOrder,
}) })
} }
}
await loadProject(base.value.id as string, true) await loadProject(base.value.id as string, true)
await loadBases() await loadBases()
} catch (e: any) { } catch (e: any) {
@ -210,67 +255,24 @@ const isNewBaseModalOpen = computed({
}, },
}) })
const isErdModalOpen = computed({ const activeSource = ref<SourceType>(null)
get: () => { const openedTab = ref('erd')
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 = ''
}
},
})
</script> </script>
<template> <template>
<div class="flex flex-row w-full h-full nc-data-sources-view"> <div class="flex flex-col h-full">
<div class="flex flex-col w-full overflow-auto"> <div class="px-4 py-2 flex justify-between">
<div class="flex flex-row w-full justify-end mt-6.5 mb-2"> <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 <NcButton
v-if="!isDataSourceLimitReached" v-if="!isDataSourceLimitReached && !activeSource && isUIAllowed('sourceCreate')"
size="large" size="large"
class="z-10 !px-2" class="z-10 !px-2"
type="primary" type="primary"
@ -282,6 +284,69 @@ const isEditBaseModalOpen = computed({
</div> </div>
</NcButton> </NcButton>
</div> </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>
<a-tab-pane key="acl">
<template #tab>
<div class="tab" data-testid="nc-acl-tab">
<div>{{ $t('labels.uiAcl') }}</div>
</div>
</template>
<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>
<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 <div
class="overflow-y-auto nc-scrollbar-md" class="overflow-y-auto nc-scrollbar-md"
:style="{ :style="{
@ -293,16 +358,15 @@ const isEditBaseModalOpen = computed({
<div class="ds-table-col ds-table-enabled cursor-pointer">{{ $t('general.visibility') }}</div> <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-name">{{ $t('general.name') }}</div>
<div class="ds-table-col ds-table-type">{{ $t('general.type') }}</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-actions">{{ $t('labels.actions') }}</div>
<div class="ds-table-col ds-table-crud"></div>
</div> </div>
</div> </div>
<div class="ds-table-body"> <div class="ds-table-body">
<Draggable :list="sources" item-key="id" handle=".ds-table-handle" @end="moveBase"> <Draggable :list="sources" item-key="id" handle=".ds-table-handle" @end="moveBase">
<template #header> <template #header>
<div v-if="sources[0]" class="ds-table-row border-gray-200"> <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="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1" @click.stop>
<div v-if="sources.length > 2" class="ds-table-handle" /> <div v-if="sources.length > 2" class="ds-table-handle" />
<a-tooltip> <a-tooltip>
<template #title> <template #title>
@ -330,80 +394,12 @@ const isEditBaseModalOpen = computed({
</div> </div>
<div class="ds-table-col ds-table-actions"> <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 <NcButton
v-if="!sources[0].is_meta && !sources[0].is_local" v-if="!sources[0].is_meta && !sources[0].is_local"
size="small" size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg" class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text" type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.Edit)" @click.stop="baseAction(sources[0].id, DataSourcesSubTab.Edit)"
> >
<GeneralIcon icon="edit" class="text-gray-600" /> <GeneralIcon icon="edit" class="text-gray-600" />
</NcButton> </NcButton>
@ -411,9 +407,9 @@ const isEditBaseModalOpen = computed({
</div> </div>
</template> </template>
<template #item="{ element: source, index }"> <template #item="{ element: source, index }">
<div v-if="index !== 0" class="ds-table-row border-gray-200"> <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="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1" @click.stop>
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" /> <GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<a-tooltip> <a-tooltip>
<template #title> <template #title>
@ -430,7 +426,7 @@ const isEditBaseModalOpen = computed({
</div> </div>
</div> </div>
<div class="ds-table-col ds-table-name font-medium w-full"> <div class="ds-table-col ds-table-name font-medium w-full">
<div v-if="source.is_meta || source.is_local">-</div> <div v-if="source.is_meta || source.is_local" class="h-8 w-1">-</div>
<span v-else class="truncate"> <span v-else class="truncate">
{{ source.is_meta || source.is_local ? $t('general.base') : source.alias }} {{ source.is_meta || source.is_local ? $t('general.base') : source.alias }}
</span> </span>
@ -442,91 +438,7 @@ const isEditBaseModalOpen = computed({
<span class="text-gray-700 capitalize">{{ source.type }}</span> <span class="text-gray-700 capitalize">{{ source.type }}</span>
</div> </div>
</div> </div>
<div class="ds-table-col justify-end gap-x-1 ds-table-actions">
<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>
<NcTooltip>
<template #title>
{{ $t('tooltip.metaSync') }}
</template>
<NcButton
v-if="!source.is_meta && !source.is_local"
size="small"
type="text"
data-testid="nc-data-sources-view-meta-sync"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(source.id, DataSourcesSubTab.Metadata)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="sync" class="group-hover:text-accent" />
</div>
</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> <NcTooltip>
<template #title> <template #title>
{{ $t('general.remove') }} {{ $t('general.remove') }}
@ -536,7 +448,7 @@ const isEditBaseModalOpen = computed({
size="small" size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg" class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text" type="text"
@click="openDeleteBase(source)" @click.stop="openDeleteBase(source)"
> >
<GeneralIcon icon="delete" class="text-red-500" /> <GeneralIcon icon="delete" class="text-red-500" />
</NcButton> </NcButton>
@ -552,35 +464,6 @@ const isEditBaseModalOpen = computed({
:connection-type="clientType" :connection-type="clientType"
@source-created="loadBases(true)" @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" />
</div>
</GeneralModal>
<GeneralDeleteModal <GeneralDeleteModal
v-model:visible="isDeleteBaseModalOpen" v-model:visible="isDeleteBaseModalOpen"
:entity-name="$t('general.datasource')" :entity-name="$t('general.datasource')"
@ -601,9 +484,10 @@ const isEditBaseModalOpen = computed({
</GeneralDeleteModal> </GeneralDeleteModal>
</div> </div>
</div> </div>
</div>
</template> </template>
<style> <style scoped lang="scss">
.ds-table-head { .ds-table-head {
@apply flex items-center border-0 text-gray-500; @apply flex items-center border-0 text-gray-500;
} }
@ -613,7 +497,7 @@ const isEditBaseModalOpen = computed({
} }
.ds-table-row { .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 { .ds-table-col {
@ -633,11 +517,7 @@ const isEditBaseModalOpen = computed({
} }
.ds-table-actions { .ds-table-actions {
@apply col-span-5 flex w-full justify-end; @apply col-span-5 flex w-full justify-center;
}
.ds-table-crud {
@apply col-span-2;
} }
.ds-table-col:last-child { .ds-table-col:last-child {
@ -647,4 +527,15 @@ const isEditBaseModalOpen = computed({
.ds-table-handle { .ds-table-handle {
@apply cursor-pointer justify-self-start mr-2 w-[16px]; @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> </style>

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

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FunctionalComponent, SVGAttributes } from 'vue' import type { FunctionalComponent, SVGAttributes } from 'vue'
import Misc from './Misc.vue' import Misc from './Misc.vue'
import DataSources from '~/components/dashboard/settings/DataSources.vue'
interface Props { interface Props {
modelValue?: boolean modelValue?: boolean
@ -39,6 +40,8 @@ const vDataState = useVModel(props, 'dataSourcesState', emits)
const baseId = toRef(props, 'baseId') const baseId = toRef(props, 'baseId')
const { isUIAllowed } = useRoles()
provide(ProjectIdInj, baseId) provide(ProjectIdInj, baseId)
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -77,21 +80,6 @@ const tabsInfo: TabGroup = {
// $e('c:settings:team-auth') // $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: {
// // Audit // // Audit
// title: t('title.audit'), // title: t('title.audit'),
@ -123,6 +111,22 @@ const tabsInfo: TabGroup = {
$e('c:settings:base-settings') $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] const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
@ -154,6 +158,7 @@ watch(
:footer="null" :footer="null"
width="max(90vw, 600px)" width="max(90vw, 600px)"
:closable="false" :closable="false"
class="!top-50px !bottom-50px"
wrap-class-name="nc-modal-settings" wrap-class-name="nc-modal-settings"
@cancel="emits('update:modelValue', false)" @cancel="emits('update:modelValue', false)"
> >
@ -173,11 +178,16 @@ watch(
</a-button> </a-button>
</div> </div>
<a-layout class="mt-3 h-[75vh] overflow-y-auto flex"> <a-layout class="mt-3 overflow-y-auto flex">
<!-- Side tabs --> <!-- Side tabs -->
<a-layout-sider> <a-layout-sider>
<a-menu v-model:selected-keys="selectedTabKeys" class="tabs-menu h-full" :open-keys="[]"> <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)"> <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"> <div class="flex items-center space-x-2" @click="tab.onClick">
<component :is="tab.icon" /> <component :is="tab.icon" />
@ -186,11 +196,12 @@ watch(
</div> </div>
</div> </div>
</a-menu-item> </a-menu-item>
</template>
</a-menu> </a-menu>
</a-layout-sider> </a-layout-sider>
<!-- Sub Tabs --> <!-- 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 <a-menu
v-if="selectedTabKeys[0] !== 'dataSources'" v-if="selectedTabKeys[0] !== 'dataSources'"
v-model:selectedKeys="selectedSubTabKeys" v-model:selectedKeys="selectedSubTabKeys"
@ -206,51 +217,19 @@ watch(
{{ tab.title }} {{ tab.title }}
</a-menu-item> </a-menu-item>
</a-menu> </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 <component
:is="selectedSubTab?.body" :is="selectedSubTab?.body"
v-if="selectedSubTabKeys[0] === 'dataSources'" v-if="selectedSubTabKeys[0] === 'dataSources'"
v-model:state="vDataState" v-model:state="vDataState"
v-model:reload="dataSourcesReload" v-model:reload="dataSourcesReload"
class="px-2 pb-2" class="px-2 pb-2 h-full"
:data-testid="`nc-settings-subtab-${selectedSubTab.key}`" :data-testid="`nc-settings-subtab-${selectedSubTab.key}`"
:base-id="baseId" :base-id="baseId"
/> />

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

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

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

@ -3,18 +3,14 @@ import dayjs from 'dayjs'
interface Props { interface Props {
selectedDate?: dayjs.Dayjs | null selectedDate?: dayjs.Dayjs | null
isDisabled?: boolean
pageDate?: dayjs.Dayjs pageDate?: dayjs.Dayjs
isYearPicker?: boolean isYearPicker?: boolean
hideHeader?: boolean
hideCalendar?: boolean hideCalendar?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
selectedDate: null, selectedDate: null,
isDisabled: false,
pageDate: dayjs(), pageDate: dayjs(),
hideHeader: false,
isYearPicker: false, isYearPicker: false,
hideCalendar: false, hideCalendar: false,
}) })
@ -90,9 +86,9 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
<template> <template>
<div class="flex flex-col"> <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"> <div class="flex">
<NcTooltip> <NcTooltip hide-on-click>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('prev')"> <NcButton class="!border-0" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.arrowLeft" class="h-4 w-4" /> <component :is="iconMap.arrowLeft" class="h-4 w-4" />
</NcButton> </NcButton>
@ -106,7 +102,7 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
isYearPicker ? dayjs(selectedDate).year() : dayjs(pageDate).format('YYYY') isYearPicker ? dayjs(selectedDate).year() : dayjs(pageDate).format('YYYY')
}}</span> }}</span>
<div class="flex"> <div class="flex">
<NcTooltip> <NcTooltip hide-on-click>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('next')"> <NcButton class="!border-0" size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.arrowRight" class="h-4 w-4" /> <component :is="iconMap.arrowRight" class="h-4 w-4" />
</NcButton> </NcButton>
@ -123,10 +119,10 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
v-for="(month, id) in months" v-for="(month, id) in months"
:key="id" :key="id"
:class="{ :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'), '!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" @click="selectedDate = month"
> >
{{ month.format('MMM') }} {{ 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), '!bg-gray-200 !text-brand-500 !font-bold ': compareYear(year, selectedDate),
'!text-brand-500': dayjs().isSame(year, 'year'), '!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" @click="selectedDate = year"
> >
{{ year.format('YYYY') }} {{ year.format('YYYY') }}

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

@ -84,7 +84,7 @@ const onChange = (value: string) => {
height: fit-content; height: fit-content;
.ant-select-selector { .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); 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 { .ant-select-selection-item {

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

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

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

@ -162,7 +162,7 @@ watch(
</template> </template>
<ProjectAccessSettings :base-id="currentBase?.id" /> <ProjectAccessSettings :base-id="currentBase?.id" />
</a-tab-pane> </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> <template #tab>
<div class="tab-title" data-testid="proj-view-tab__data-sources"> <div class="tab-title" data-testid="proj-view-tab__data-sources">
<GeneralIcon icon="database" /> <GeneralIcon icon="database" />
@ -180,7 +180,7 @@ watch(
</div> </div>
</template> </template>
<DashboardSettingsDataSources v-model:state="baseSettingsState" /> <DashboardSettingsDataSources v-model:state="baseSettingsState" />
</a-tab-pane> </a-tab-pane>-->
</a-tabs> </a-tabs>
</div> </div>
</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"> <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 flex-wrap justify-between items-center gap-2">
<div class="flex gap-3"> <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') }} {{ $t('objects.viewType.form') }} {{ $t('objects.fields') }}
</div> </div>
<NcBadge color="border-gray-200"> <NcBadge color="border-gray-200">
@ -1633,7 +1633,7 @@ useEventListener(
<LazyCellRichText <LazyCellRichText
v-if="!isLocked && isEditable" v-if="!isLocked && isEditable"
v-model:value="formViewData.success_msg" v-model:value="formViewData.success_msg"
class="nc-form-after-submit-msg" class="nc-form-after-submit-msg editable"
is-form-field is-form-field
:hidden-bubble-menu-options="hiddenBubbleMenuOptions" :hidden-bubble-menu-options="hiddenBubbleMenuOptions"
data-testid="nc-form-after-submit-msg" data-testid="nc-form-after-submit-msg"
@ -1833,6 +1833,13 @@ useEventListener(
.form-meta-input { .form-meta-input {
.nc-textarea-rich-editor { .nc-textarea-rich-editor {
@apply pl-3 pr-4 !rounded-lg !text-sm border-1 border-gray-200 focus-within:border-brand-500; @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 { &.nc-form-input-label .nc-textarea-rich-editor {
@ -1848,6 +1855,14 @@ useEventListener(
.nc-form-after-submit-msg { .nc-form-after-submit-msg {
.nc-textarea-rich-editor { .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; @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 { .ProseMirror {
min-height: 5rem; min-height: 5rem;
max-height: 7.5rem !important; max-height: 7.5rem !important;

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

@ -22,9 +22,12 @@ const { allowCSVDownload } = useSharedView()
<template> <template>
<div <div
v-if="!isMobileMode" v-if="!isMobileMode || isCalendar"
ref="containerRef" 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"> <template v-if="isViewsLoading">
<a-skeleton-input :active="true" class="!w-44 !h-4 ml-2 !rounded overflow-hidden" /> <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, 'w-full': isMobileMode,
}" }"
/> />
<div v-if="isCalendar && isMobileMode" class="flex-1 pointer-events-none" />
<LazySmartsheetToolbarCalendarMode v-if="isCalendar && !isTab" :tab="isTab" /> <LazySmartsheetToolbarCalendarMode v-if="isCalendar && !isTab" :tab="isTab" />
<LazySmartsheetToolbarFieldsMenu v-if="isCalendar" :show-system-fields="false" /> <LazySmartsheetToolbarFieldsMenu v-if="isCalendar && !isMobileMode" :show-system-fields="false" />
<LazySmartsheetToolbarColumnFilterMenu v-if="isCalendar" /> <LazySmartsheetToolbarColumnFilterMenu v-if="isCalendar && !isMobileMode" />
</template> </template>
</div> </div>
</template> </template>

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

@ -889,7 +889,7 @@ watch(
:class="{ :class="{
'!border-brand-500': hour.isSame(selectedTime), '!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" data-testid="nc-calendar-day-hour"
@click="selectHour(hour)" @click="selectHour(hour)"
@dblclick="newRecord(hour)" @dblclick="newRecord(hour)"
@ -999,7 +999,7 @@ watch(
:data-testid="`nc-calendar-day-record-${record.row[displayField!.title!]}`" :data-testid="`nc-calendar-day-record-${record.row[displayField!.title!]}`"
:data-unique-id="record.rowMeta.id" :data-unique-id="record.rowMeta.id"
:style="record.rowMeta.style" :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)" @mousedown="dragStart($event, record)"
@mouseleave="hoverRecord = null" @mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id as string" @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), '!text-gray-400': !isDayInPagedMonth(day),
'!bg-gray-50': day.get('day') === 0 || day.get('day') === 6, '!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" data-testid="nc-calendar-month-day"
@click="selectDate(day)" @click="selectDate(day)"
@dblclick="addRecord(day)" @dblclick="addRecord(day)"
@ -812,7 +812,7 @@ const addRecord = (date: dayjs.Dayjs) => {
...record.rowMeta.style, ...record.rowMeta.style,
zIndex: record.rowMeta.id === draggingId ? 100 : 0, 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" @mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id" @mouseover="hoverRecord = record.rowMeta.id"
@mousedown.stop="dragStart($event, record)" @mousedown.stop="dragStart($event, record)"

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

@ -315,25 +315,29 @@ onClickOutside(searchRef, toggleSearch)
<template> <template>
<NcTooltip <NcTooltip
:class="{ :class="{
'!right-26 top-[-36px]': showSideMenu && isMobileMode,
'right-2': !showSideMenu, 'right-2': !showSideMenu,
'right-74': 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> <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" /> <component :is="iconMap.sidebar" class="h-4 w-4 text-gray-600 transition-all" />
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
<div <div
:class="{ :class="{
'!min-w-[100svw]': props.visible && isMobileMode,
'!w-0 hidden': !props.visible, '!w-0 hidden': !props.visible,
'nc-calendar-side-menu-open block !min-w-[288px]': 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" class="h-full relative border-l-1 border-gray-200 transition-all"
data-testid="nc-calendar-side-menu" data-testid="nc-calendar-side-menu"
> >
<div class="flex flex-col"> <div class="flex min-w-[288px] flex-col">
<NcDateWeekSelector <NcDateWeekSelector
v-if="activeCalendarView === ('day' as const)" v-if="activeCalendarView === ('day' as const)"
v-model:active-dates="activeDates" 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> <template>
<div class="flex relative flex-col prevent-select" data-testid="nc-calendar-week-view" @drop="dropEvent"> <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 <div
v-for="(date, weekIndex) in weekDates" v-for="(date, weekIndex) in weekDates"
:key="weekIndex" :key="weekIndex"
:class="{ :class="{
'!border-brand-500 !border-b-gray-200': dayjs(date).isSame(selectedDate, 'day'), '!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)" @click="selectDate(date)"
@dblclick="addRecord(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" data-testid="nc-calendar-week-view"
@drop="dropEvent" @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 <div
v-for="date in datesHours" v-for="date in datesHours"
:key="date[0].toISOString()" :key="date[0].toISOString()"
:class="{ :class="{
'text-brand-500': date[0].isSame(dayjs(), 'date'), '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') }} {{ dayjs(date[0]).format('DD ddd') }}
</div> </div>
@ -907,7 +907,7 @@ watch(
'border-1 !border-brand-500 bg-gray-50': hour.isSame(selectedTime, 'hour'), 'border-1 !border-brand-500 bg-gray-50': hour.isSame(selectedTime, 'hour'),
'!bg-gray-50': hour.get('day') === 0 || hour.get('day') === 6, '!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" data-testid="nc-calendar-week-hour"
@dblclick="addRecord(hour)" @dblclick="addRecord(hour)"
@click=" @click="
@ -944,7 +944,7 @@ watch(
:data-testid="`nc-calendar-week-record-${record.row[displayField!.title!]}`" :data-testid="`nc-calendar-week-record-${record.row[displayField!.title!]}`"
:data-unique-id="record.rowMeta!.id" :data-unique-id="record.rowMeta!.id"
:style="record.rowMeta!.style " :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)" @mousedown.stop="dragStart($event, record)"
@mouseleave="hoverRecord = null" @mouseleave="hoverRecord = null"
@mouseover="hoverRecord = record.rowMeta.id" @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 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 = () => { const handleResize = () => {
if (width.value < 1608) { if (width.value > 1250) {
size.value = 'small' size.value = 'medium'
} else if (width.value < 2000) { cols.value = 4
} else if (width.value > 850) {
size.value = 'medium' 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 { } else {
size.value = 'large' size.value = 'medium'
cols.value = 1
} }
} }
@ -40,15 +50,19 @@ watch(width, handleResize)
</script> </script>
<template> <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 <div
:class="{ :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" data-testid="nc-calendar-year-view"
> >
<NcDateWeekSelector <LazySmartsheetCalendarYearViewMonth
v-for="(_, index) in months" v-for="(_, index) in months"
:key="index" :key="index"
v-model:active-dates="activeDates" v-model:active-dates="activeDates"
@ -57,7 +71,6 @@ watch(width, handleResize)
:size="size" :size="size"
class="nc-year-view-calendar" class="nc-year-view-calendar"
data-testid="nc-calendar-year-view-month-selector" data-testid="nc-calendar-year-view-month-selector"
disable-pagination
@dbl-click="changeView" @dbl-click="changeView"
/> />
</div> </div>
@ -67,7 +80,7 @@ watch(width, handleResize)
<style lang="scss" scoped> <style lang="scss" scoped>
.nc-year-view-calendar { .nc-year-view-calendar {
:deep(.nc-date-week-header) { :deep(.nc-date-week-header) {
@apply border-gray-200; @apply border-gray-200 h-8 py-2;
} }
} }
</style> </style>

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

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

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

@ -102,16 +102,16 @@ const sortOrder: Record<string, number> = {
const suggestionsList = computed(() => { const suggestionsList = computed(() => {
const unsupportedFnList = sqlUi.value.getUnsupportedFnList() const unsupportedFnList = sqlUi.value.getUnsupportedFnList()
return [ return (
...availableFunctions [
.filter((fn: string) => !unsupportedFnList.includes(fn)) ...availableFunctions.map((fn: string) => ({
.map((fn: string) => ({
text: `${fn}()`, text: `${fn}()`,
type: 'function', type: 'function',
description: formulas[fn].description, description: formulas[fn].description,
syntax: formulas[fn].syntax, syntax: formulas[fn].syntax,
examples: formulas[fn].examples, examples: formulas[fn].examples,
docsUrl: formulas[fn].docsUrl, docsUrl: formulas[fn].docsUrl,
unsupported: unsupportedFnList.includes(fn),
})), })),
...supportedColumns.value ...supportedColumns.value
.filter((c) => { .filter((c) => {
@ -132,6 +132,17 @@ const suggestionsList = computed(() => {
type: '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 // set default suggestion list
@ -214,6 +225,7 @@ function handleInput() {
function selectText() { function selectText() {
if (suggestion.value && selected.value > -1 && selected.value < suggestionsList.value.length) { if (suggestion.value && selected.value > -1 && selected.value < suggestionsList.value.length) {
if (selected.value < suggestedFormulas.value.length) { if (selected.value < suggestedFormulas.value.length) {
if (suggestedFormulas.value[selected.value].unsupported) return
appendText(suggestedFormulas.value[selected.value]) appendText(suggestedFormulas.value[selected.value])
} else { } else {
appendText(variableList.value[selected.value + suggestedFormulas.value.length]) appendText(variableList.value[selected.value + suggestedFormulas.value.length])
@ -276,7 +288,7 @@ onMounted(() => {
<template> <template>
<div class="formula-wrapper relative"> <div class="formula-wrapper relative">
<div <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" 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"> <div class="pr-3">
@ -357,20 +369,22 @@ onMounted(() => {
class="cursor-pointer !overflow-hidden hover:bg-gray-50" class="cursor-pointer !overflow-hidden hover:bg-gray-50"
:class="{ :class="{
'!bg-gray-100': selected === index, '!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" @mouseenter="suggestionPreviewed = item"
> >
<a-list-item-meta> <a-list-item-meta>
<template #title> <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.function" v-if="item.type === 'function'" class="text-lg" />
<component :is="iconMap.calculator" v-if="item.type === 'op'" 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" /> <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>
<div v-if="item.unsupported" class="ml-5 text-gray-400 text-xs">{{ $t('msg.formulaNotSupported') }}</div>
</template> </template>
</a-list-item-meta> </a-list-item-meta>
</a-list-item> </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 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="flex-1 max-w-[calc(100%_-_40px)]">
<div class="font-medium text-gray-800">{{ $t('labels.limitOptions') }}</div> <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"> <div v-if="activeField.meta.isLimitOption" class="mt-3">
<LazySmartsheetFormLimitOptions <LazySmartsheetFormLimitOptions
v-model:model-value="activeField.meta.limitOptions" v-model:model-value="activeField.meta.limitOptions"
@ -95,7 +95,7 @@ const columnSupportsScanning = (elementType: UITypes) =>
v-if="isSelectTypeCol(activeField.uidt)" v-if="isSelectTypeCol(activeField.uidt)"
class="nc-form-field-appearance-settings p-4 flex flex-col gap-4 border-b border-gray-200" 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"> <div class="flex flex-col gap-6">
<!-- Select type field Options Layout --> <!-- Select type field Options Layout -->
<div> <div>

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

@ -2447,7 +2447,7 @@ onKeyStroke('ArrowDown', onDown)
:deep(.nc-cell-icon), :deep(.nc-cell-icon),
:deep(.nc-virtual-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 <GeneralIcon
v-if="isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit')" v-if="isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit')"
icon="arrowDown" 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="{ :class="{
visible: editColumnDropdown || isDropDownOpen, visible: editColumnDropdown || isDropDownOpen,
invisible: !(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: case RelationTypes.BELONGS_TO:
return { icon: iconMap.bt_solid } return { icon: iconMap.bt_solid }
case RelationTypes.ONE_TO_ONE: case RelationTypes.ONE_TO_ONE:
return { icon: iconMap.oneToOneSolid, color: 'text-blue-500' } return { icon: iconMap.oneToOneSolid, color: 'text-purple-500' }
} }
break break
case UITypes.SpecificDBType: case UITypes.SpecificDBType:
@ -36,6 +36,8 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
return { icon: iconMap.cellLookup, color: 'text-orange-500' } return { icon: iconMap.cellLookup, color: 'text-orange-500' }
case RelationTypes.BELONGS_TO: case RelationTypes.BELONGS_TO:
return { icon: iconMap.cellLookup, color: 'text-blue-500' } 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' } return { icon: iconMap.cellLookup, color: 'text-grey' }
case UITypes.Rollup: case UITypes.Rollup:
@ -46,6 +48,8 @@ const renderIcon = (column: ColumnType, relationColumn?: ColumnType) => {
return { icon: iconMap.cellRollup, color: 'text-orange-500' } return { icon: iconMap.cellRollup, color: 'text-orange-500' }
case RelationTypes.BELONGS_TO: case RelationTypes.BELONGS_TO:
return { icon: iconMap.cellRollup, color: 'text-blue-500' } 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' } return { icon: iconMap.cellRollup, color: 'text-grey' }
case UITypes.Count: case UITypes.Count:
@ -76,15 +80,16 @@ export default defineComponent({
const column = computed(() => columnMeta.value ?? injectedColumn.value) const column = computed(() => columnMeta.value ?? injectedColumn.value)
const { metas } = useMetas()
let relationColumn: ColumnType let relationColumn: ColumnType
return () => { return () => {
if (!column.value) return null if (!column.value) return null
if (column && column.value) { if (column && column.value) {
if (isMm(column.value) || isHm(column.value) || isBt(column.value) || isLookup(column.value) || isRollup(column.value)) { if (isLookup(column.value) || isRollup(column.value)) {
const meta = inject(MetaInj, ref()) relationColumn = metas.value?.[column.value.fk_model_id]?.columns?.find(
relationColumn = meta.value?.columns?.find(
(c) => c.id === column.value?.colOptions?.fk_relation_column_id, (c) => c.id === column.value?.colOptions?.fk_relation_column_id,
) as ColumnType ) as ColumnType
} }
@ -92,7 +97,7 @@ export default defineComponent({
const { icon: Icon, color } = renderIcon(column.value, relationColumn) 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 { changeCalendarView, activeCalendarView } = useCalendarViewStoreOrThrow()
const isTab = computed(() => props.tab)
const highlightStyle = ref({ left: '0px' }) const highlightStyle = ref({ left: '0px' })
const setActiveCalendarMode = (mode: 'day' | 'week' | 'month' | 'year', event: MouseEvent) => { const setActiveCalendarMode = (mode: 'day' | 'week' | 'month' | 'year', event: MouseEvent) => {
@ -22,15 +24,19 @@ const updateHighlightPosition = () => {
}) })
} }
onMounted(() => {
updateHighlightPosition()
})
watch(activeCalendarView, () => { watch(activeCalendarView, () => {
if (!props.tab) return if (!isTab.value) return
updateHighlightPosition() updateHighlightPosition()
}) })
</script> </script>
<template> <template>
<div <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" 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" data-testid="nc-calendar-view-mode"
> >
@ -47,12 +53,12 @@ watch(activeCalendarView, () => {
</div> </div>
</div> </div>
<NcSelect v-else v-model:value="activeCalendarView" class="!w-22" data-testid="nc-calendar-view-mode" size="small"> <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-20"> <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"> <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> <template #title>
<span class="capitalize"> <span class="capitalize min-w-21">
{{ option }} {{ option }}
</span> </span>
</template> </template>
@ -81,6 +87,11 @@ watch(activeCalendarView, () => {
@apply !text-[13px]; @apply !text-[13px];
} }
} }
.nc-select.ant-select {
.ant-select-selector {
@apply !px-3;
}
}
.tab { .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; @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> <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' import type { SelectProps } from 'ant-design-vue'
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
@ -119,10 +119,15 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
<template #overlay> <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 v-if="calendarRangeDropdown" class="w-98 space-y-6 rounded-2xl p-6" data-testid="nc-calendar-range-menu" @click.stop>
<div> <div>
<div class="flex justify-between"> <div class="flex mb-3 justify-between">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<component :is="iconMap.calendar" class="text-maroon-500 w-5 h-5" /> <GeneralViewIcon
<span class="font-bold"> {{ `${$t('activity.calendar')} ${$t('activity.viewSettings')}` }}</span> :meta="{
type: ViewTypes.CALENDAR,
}"
class="w-6 h-6"
/>
<span class="font-bold text-base"> {{ `${$t('activity.calendar')} ${$t('activity.viewSettings')}` }}</span>
</div> </div>
<a <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 meta = inject(MetaInj, ref())
const { metas } = useMetas()
const localValue = computed({ const localValue = computed({
get: () => modelValue, get: () => modelValue,
set: (val) => emit('update:modelValue', val), set: (val) => emit('update:modelValue', val),
@ -80,6 +82,31 @@ const filterOption = (input: string, option: any) => option.label.toLowerCase()?
if (!localValue.value && allowEmpty !== true) { if (!localValue.value && allowEmpty !== true) {
localValue.value = (options.value?.[0].value as string) || '' 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> </script>
<template> <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"> <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 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"> <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 <NcTooltip
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
class="max-w-[15rem] truncate select-none" 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) provide(RowHeightInj, providedHeightRef)
const relationColumn = computed( const relationColumn = computed(() =>
() => meta.value?.id
meta.value?.columns?.find((c: ColumnType) => c.id === (column.value?.colOptions as LookupType)?.fk_relation_column_id) as ? metas.value[meta.value?.id]?.columns?.find(
| (ColumnType & { (c: ColumnType) => c.id === (column.value?.colOptions as LookupType)?.fk_relation_column_id,
colOptions: LinkToAnotherRecordType | undefined )
}) : undefined,
| undefined,
) )
watch( watch(

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

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

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

@ -168,13 +168,6 @@ export const useCommandPalette = createSharedComposable(() => {
data: { base_id: route.value.params.baseId }, data: { base_id: route.value.params.baseId },
} }
} }
} else {
if (route.value.path.startsWith('/account')) {
if (activeScope.value.scope === 'account_settings') return
activeScope.value = { scope: 'account_settings', data: {} }
loadScope()
} else { } else {
if (activeScope.value.scope === 'root') return if (activeScope.value.scope === 'root') return
@ -182,7 +175,6 @@ export const useCommandPalette = createSharedComposable(() => {
loadScope() loadScope()
} }
}
}, },
{ immediate: true, deep: true }, { 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) { for (const column of visibleColumns.value) {
let rules: RuleObject[] = [ let rules: RuleObject[] = [
{ {
required: isRequired(column, column.required), validator: (_rule: RuleObject, value: any) => {
message: t('msg.error.fieldRequired'), return new Promise((resolve, reject) => {
...(column.uidt === UITypes.Checkbox && isRequired(column, column.required) ? { type: 'enum', enum: [1, true] } : {}), 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()
})
},
}, },
] ]

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

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

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

@ -167,7 +167,7 @@ export const useUndoRedo = createSharedComposable(() => {
useEventListener(document, 'keydown', async (e: KeyboardEvent) => { useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey 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) { if (cmdOrCtrl && !e.altKey) {
switch (e.keyCode) { switch (e.keyCode) {

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

@ -448,6 +448,8 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results" "noResultsMatchedYourSearch": "Your search did not yield any matching results"
}, },
"labels": { "labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"today": "Today", "today": "Today",
"workspace": "Workspace", "workspace": "Workspace",
"txt": "TXT Record value", "txt": "TXT Record value",
@ -1099,6 +1101,7 @@
"searchOptions": "Search options" "searchOptions": "Search options"
}, },
"msg": { "msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.", "controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.", "addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.", "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", "theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty", "parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed", "duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.", "fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Base not accessible", "projectNotAccessible": "Base not accessible",
"copyToClipboardError": "Failed to copy to clipboard", "copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from 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", "theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Los tipos de archivo aceptados son .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "La clave del parámetro no puede estar vacía", "parameterKeyCannotBeEmpty": "La clave del parámetro no puede estar vacía",
"duplicateParameterKeysAreNotAllowed": "No se permiten claves de parámetros duplicadas", "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", "projectNotAccessible": "Proyecto no accesible",
"copyToClipboardError": "Fallo al copiar al portapapeles", "copyToClipboardError": "Fallo al copiar al portapapeles",
"pasteFromClipboardError": "Failed to paste from clipboard", "pasteFromClipboardError": "Failed to paste from clipboard",

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

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

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

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

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

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

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

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

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

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

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

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

38
packages/nc-gui/package.json

@ -38,7 +38,7 @@
"dependencies": { "dependencies": {
"@braks/revue-draggable": "^0.4.3", "@braks/revue-draggable": "^0.4.3",
"@ckpack/vue-color": "^1.5.0", "@ckpack/vue-color": "^1.5.0",
"@iconify/vue": "^4.1.1", "@iconify/vue": "^4.1.2",
"@nuxt/image": "^1.3.0", "@nuxt/image": "^1.3.0",
"@pinia/nuxt": "^0.5.1", "@pinia/nuxt": "^0.5.1",
"@tiptap/extension-link": "2.2.6", "@tiptap/extension-link": "2.2.6",
@ -60,9 +60,9 @@
"crossoriginworker": "^1.1.0", "crossoriginworker": "^1.1.0",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"dayjs": "^1.11.10", "dayjs": "^1.11.11",
"deep-object-diff": "^1.1.9", "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", "file-saver": "^2.0.5",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"httpsnippet": "^2.0.0", "httpsnippet": "^2.0.0",
@ -75,7 +75,7 @@
"marked": "^4.3.0", "marked": "^4.3.0",
"monaco-editor": "^0.45.0", "monaco-editor": "^0.45.0",
"monaco-sql-languages": "^0.11.0", "monaco-sql-languages": "^0.11.0",
"nocodb-sdk": "0.207.1", "nocodb-sdk": "workspace:^",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"parse-github-url": "^1.0.2", "parse-github-url": "^1.0.2",
"pinia": "^2.1.7", "pinia": "^2.1.7",
@ -92,7 +92,7 @@
"validator": "^13.11.0", "validator": "^13.11.0",
"vue-advanced-cropper": "^2.8.8", "vue-advanced-cropper": "^2.8.8",
"vue-barcode-reader": "^1.0.3", "vue-barcode-reader": "^1.0.3",
"vue-chartjs": "^5.3.0", "vue-chartjs": "^5.3.1",
"vue-dompurify-html": "^3.1.2", "vue-dompurify-html": "^3.1.2",
"vue-github-button": "^3.1.0", "vue-github-button": "^3.1.0",
"vue-i18n": "^9.9.1", "vue-i18n": "^9.9.1",
@ -107,32 +107,32 @@
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.26.3", "@antfu/eslint-config": "^0.26.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@iconify-json/ant-design": "^1.1.15", "@iconify-json/ant-design": "^1.1.16",
"@iconify-json/bi": "^1.1.23", "@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/cil": "^1.1.8",
"@iconify-json/clarity": "^1.1.12", "@iconify-json/clarity": "^1.1.12",
"@iconify-json/eva": "^1.1.10", "@iconify-json/eva": "^1.1.10",
"@iconify-json/ic": "^1.1.17", "@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/la": "^1.1.8",
"@iconify-json/logos": "^1.1.42", "@iconify-json/logos": "^1.1.42",
"@iconify-json/lucide": "^1.1.180", "@iconify-json/lucide": "^1.1.187",
"@iconify-json/material-symbols": "^1.1.77", "@iconify-json/material-symbols": "^1.1.80",
"@iconify-json/mdi": "^1.1.66", "@iconify-json/mdi": "^1.1.66",
"@iconify-json/mi": "^1.1.8", "@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/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/system-uicons": "^1.1.12",
"@iconify-json/tabler": "^1.1.109", "@iconify-json/tabler": "^1.1.112",
"@iconify-json/vscode-icons": "^1.1.33", "@iconify-json/vscode-icons": "^1.1.34",
"@intlify/unplugin-vue-i18n": "^0.13.0", "@intlify/unplugin-vue-i18n": "^0.13.0",
"@nuxt/image": "^1.3.0", "@nuxt/image": "^1.3.0",
"@types/d3-scale": "^4.0.8", "@types/d3-scale": "^4.0.8",
"@types/dagre": "^0.7.52", "@types/dagre": "^0.7.52",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/leaflet": "^1.9.9", "@types/leaflet": "^1.9.12",
"@types/leaflet.markercluster": "^1.5.4", "@types/leaflet.markercluster": "^1.5.4",
"@types/papaparse": "^5.3.14", "@types/papaparse": "^5.3.14",
"@types/parse-github-url": "^1.0.3", "@types/parse-github-url": "^1.0.3",
@ -142,12 +142,12 @@
"@types/splitpanes": "^2.2.6", "@types/splitpanes": "^2.2.6",
"@types/tinycolor2": "^1.4.6", "@types/tinycolor2": "^1.4.6",
"@types/turndown": "^5.0.4", "@types/turndown": "^5.0.4",
"@types/validator": "^13.11.9", "@types/validator": "^13.11.10",
"@types/vue-barcode-reader": "^0.0.3", "@types/vue-barcode-reader": "^0.0.3",
"@unocss/nuxt": "^0.58.9", "@unocss/nuxt": "^0.58.9",
"@vitest/ui": "^0.34.7", "@vitest/ui": "^0.34.7",
"@vue/compiler-sfc": "^3.4.21", "@vue/compiler-sfc": "^3.4.27",
"@vue/test-utils": "^2.4.5", "@vue/test-utils": "^2.4.6",
"@vueuse/nuxt": "^10.7.2", "@vueuse/nuxt": "^10.7.2",
"@windicss/plugin-animations": "^1.0.9", "@windicss/plugin-animations": "^1.0.9",
"@windicss/plugin-question-mark": "^0.1.1", "@windicss/plugin-question-mark": "^0.1.1",
@ -156,7 +156,7 @@
"eslint-config-prettier": "^8.10.0", "eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"happy-dom": "^6.0.4", "happy-dom": "^6.0.4",
"nuxt": "^3.10.3", "nuxt": "^3.11.2",
"nuxt-windicss": "^2.6.1", "nuxt-windicss": "^2.6.1",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"sass": "^1.71.1", "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 isMac = () => /Mac/i.test(navigator.platform)
export const isDrawerExist = () => document.querySelector('.ant-drawer-open') export const isDrawerExist = () => document.querySelector('.ant-drawer-open')
export const isDrawerOrModalExist = () => document.querySelector('.ant-modal.active, .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 isExpandedCellInputExist = () => document.querySelector('.expanded-cell-input')
export const cmdKActive = () => document.querySelector('.cmdk-modal-active') 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) => { export const extractFieldValidator = (_validators: Validation[], element: ColumnType) => {
const rules: RuleObject[] = [] const rules: RuleObject[] = []

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

@ -22,7 +22,17 @@ export default defineConfig({
}, },
darkMode: 'class', 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: [ plugins: [
scrollbar, scrollbar,
animations, animations,
@ -101,6 +111,9 @@ export default defineConfig({
primary: 'rgba(var(--color-primary), var(--tw-ring-opacity))', primary: 'rgba(var(--color-primary), var(--tw-ring-opacity))',
accent: 'rgba(var(--color-accent), 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: { colors: {
...windiColors, ...windiColors,
...themeColors, ...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'] keywords : ['NocoDB environment variables', 'NocoDB env variables', 'NocoDB envs', 'NocoDB env']
--- ---
For production use-cases, it is **recommended** to configure For production use cases, it is **recommended** to set at least:
- `NC_DB`,
- `NC_AUTH_JWT_SECRET`, - `NC_DB`
- `NC_PUBLIC_URL`, - `NC_AUTH_JWT_SECRET`
- `NC_PUBLIC_URL`
- `NC_REDIS_URL` - `NC_REDIS_URL`
| Variable | Comments | If absent | | Variable | Description | 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` | 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 | | | `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 | | | `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 in JDBC URL format | | | `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 `DATABASE_URL` and value should be a valid path to file containing JDBC URL format. | | | `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 will be generated | | `NC_AUTH_JWT_SECRET` | JWT secret used for auth and storing other secrets. | A random secret is generated. |
| PORT | For setting app running port | `8080` | | `NC_ADMIN_EMAIL` | Super admin e-mail address. | |
| DB_QUERY_LIMIT_DEFAULT | Pagination limit | 25 | | `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 `$&+,:;=?@#\|'.^*()%!_-"`. | |
| DB_QUERY_LIMIT_GROUP_BY_GROUP | Group per page limit | 10 | | `PORT` | Network port NocoDB runs on. | Defaults to `8080`. |
| DB_QUERY_LIMIT_GROUP_BY_RECORD | Record per group limit | 10 | | `DB_QUERY_LIMIT_DEFAULT` | Pagination limit. | Defaults to `25`. |
| DB_QUERY_LIMIT_MAX | Maximum allowed pagination limit | 1000 | | `DB_QUERY_LIMIT_GROUP_BY_GROUP` | Group per page limit. | Defaults to `10`. |
| DB_QUERY_LIMIT_MIN | Minimum allowed pagination limit | 1 | | `DB_QUERY_LIMIT_GROUP_BY_RECORD` | Record per group limit. | Defaults to `10`. |
| 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. | | `DB_QUERY_LIMIT_MAX` | Maximum allowed pagination limit. | Defaults to `1000`. |
| NC_PUBLIC_URL | Used for sending Email invitations | Best guess from http request params | | `DB_QUERY_LIMIT_MIN` | Minimum allowed pagination limit. | Defaults to `1`. |
| NC_JWT_EXPIRES_IN | JWT token expiry time | `10h` | | `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_CONNECT_TO_EXTERNAL_DB_DISABLED | Disable Project creation with external database | | | `NC_PUBLIC_URL` | Used for sending E-mail invitations. | Best guess from HTTP request params. |
| 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. | | | `NC_JWT_EXPIRES_IN` | JWT token expiry time | Defaults to `10h`. |
| NUXT_PUBLIC_NC_BACKEND_URL | Custom Backend URL | ``http://localhost:8080`` will be used | | `NC_CONNECT_TO_EXTERNAL_DB_DISABLED` | Disable base creation on external databases. | |
| NC_REQUEST_BODY_SIZE | Request body size [limit](https://expressjs.com/en/resources/middleware/body-parser.html#limit) | `1048576` | | `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_EXPORT_MAX_TIMEOUT | After NC_EXPORT_MAX_TIMEOUT, CSV gets downloaded in batches | Default value 5000(in millisecond) will be used | | `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. | |
| NC_DISABLE_TELE | Disable telemetry | | | `NUXT_PUBLIC_NC_BACKEND_URL` | Custom backend URL. | Defaults to `http://localhost:8080`. |
| NC_DASHBOARD_URL | Custom dashboard URL path | `/dashboard` | | `NC_REQUEST_BODY_SIZE` | Request body size [limit](https://expressjs.com/en/resources/middleware/body-parser.html#limit) | Defaults to `1048576`. |
| NC_GOOGLE_CLIENT_ID | Google client ID to enable Google authentication | | | `NC_EXPORT_MAX_TIMEOUT` | After `NC_EXPORT_MAX_TIMEOUT` (in milliseconds), CSV gets downloaded in batches. | Defaults to `5000` (5 seconds). |
| NC_GOOGLE_CLIENT_SECRET | Google client secret to enable Google authentication | | | `NC_DISABLE_TELE` | Disable telemetry. | |
| NC_MIGRATIONS_DISABLED | Disable NocoDB migration | | | `NC_DASHBOARD_URL` | Custom dashboard URL path | Defaults to `/dashboard`. |
| NC_MIN | If set to any non-empty string the default splash screen(initial welcome animation) and matrix screensaver will disable | | | `NC_GOOGLE_CLIENT_ID` | Google client ID to enable Google authentication. | |
| NC_SENTRY_DSN | For Sentry monitoring | | | `NC_GOOGLE_CLIENT_SECRET` | Google client secret to enable Google authentication. | |
| NC_REDIS_URL | Custom Redis URL. Example: `redis://:authpassword@127.0.0.1:6380/4` | Meta data will be stored in memory | | `NC_MIGRATIONS_DISABLED` | Disable NocoDB migrations. | |
| NC_DISABLE_ERR_REPORT | Disable error reporting | | | `NC_MIN` | Disable default splash screen (initial welcome animation) and matrix screensaver. | |
| 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` | | `NC_SENTRY_DSN` | Data Source Name (DSN) for Sentry monitoring. | |
| 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 | | `NC_REDIS_URL` | Redis URL. Example: `redis://:authpassword@127.0.0.1:6380/4` | Meta data is stored in memory. |
| 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 | | `NC_DISABLE_ERR_REPORT` | Disable error reporting. | |
| AWS_BUCKET | For Litestream - S3 bucket | If Litestream is configured and `NC_DB` is not present. SQLite gets backed up to S3 | | `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`. |
| 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` | E-mail sender address for SMTP plugin. | *SMTP plugin is disabled if this variable is not set.* |
| NC_SMTP_FROM | For SMTP plugin - Email sender address | | | `NC_SMTP_HOST` | E-mail server hostname for SMTP plugin. | *SMTP plugin is disabled if this variable is not set.* |
| NC_SMTP_HOST | For SMTP plugin - SMTP host value | | | `NC_SMTP_PORT` | E-mail server network for SMTP plugin. | *SMTP plugin is disabled if this variable is not set.* |
| NC_SMTP_PORT | For SMTP plugin - SMTP port value | | | `NC_SMTP_USERNAME` | Username for authentication in SMTP plugin. | |
| NC_SMTP_USERNAME | For SMTP plugin (Optional) - SMTP username value for authentication | | | `NC_SMTP_PASSWORD` | Password for authentication in SMTP plugin. | |
| NC_SMTP_PASSWORD | For SMTP plugin (Optional) - SMTP password value for authentication | | | `NC_SMTP_SECURE` | Enable secure authentication in SMTP plugin. Set to `true` to enable, any other value is treated as `false`. | |
| NC_SMTP_SECURE | For SMTP plugin (Optional) - To enable secure set value as `true` any other value 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_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` | AWS S3 bucket name for S3 storage plugin. | |
| NC_S3_BUCKET_NAME | For S3 storage plugin - AWS S3 bucket name | | | `NC_S3_REGION` | AWS S3 region for S3 storage plugin. | |
| NC_S3_REGION | For S3 storage plugin - AWS S3 region | | | `NC_S3_ACCESS_KEY` | AWS access key ID for S3 storage plugin. | |
| NC_S3_ACCESS_KEY | For S3 storage plugin - AWS access key credential for accessing resource | | | `NC_S3_ACCESS_SECRET` | AWS access secret for S3 storage plugin. | |
| NC_S3_ACCESS_SECRET | For S3 storage plugin - AWS access secret credential for accessing resource | | | `NC_ATTACHMENT_FIELD_SIZE` | Maximum file size for [attachments](/fields/field-types/custom-types/attachment/) in bytes. | Defaults to `20971520` (20 MiB). |
| 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. | Defaults to `10`. |
| NC_MAX_ATTACHMENTS_ALLOWED | Maximum Number of attachments per cell | | | `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_ADMIN_EMAIL | For updating/creating super admin with provided email and password | | | `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_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 $&+,:;=?@#\|'.^*()%!_-" ) | | | `NC_DISABLE_AUDIT` | Disable audit log. | Defaults to `false`. |
| NODE_OPTIONS | For passing Node.js [options](https://nodejs.org/api/cli.html#node_optionsoptions) to instance | | | `NC_AUTOMATION_LOG_LEVEL` | Possible Values: `OFF`, `ERROR`, `ALL`. See [Webhooks](/automation/webhook/create-webhook#call-log) for details. | Defaults to `OFF`. |
| 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_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_DISABLE_AUDIT | Disable Audit Log | `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`. |
| NC_AUTOMATION_LOG_LEVEL | Possible Values: `OFF`, `ERROR`, `ALL`. See [Webhooks](/automation/webhook/create-webhook#call-log) for details. | `OFF` | | `NODE_OPTIONS` | Node.js [options](https://nodejs.org/api/cli.html#node_optionsoptions) to pass to instance. | |
| 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` | | `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.* |
| 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) | | `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.* |
| 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` | | `LITESTREAM_S3_PATH` | Directory path to use within the Litestream replication object storage bucket. | Defaults to `nocodb`. |
| NC_SANITIZE_COLUMN_NAME | Sanitize the column name during column creation. To enable set value as `true` any other value treated as false. | `true` | | `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 ## Environment variables
In self-hosted version, you can configure the following environment variables to customize the behavior of `Attachment` field. 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_ATTACHMENT_FIELD_SIZE`: Maximum size of attachment files in bytes. Default: `20971520` (20 MiB)
- NC_SECURE_ATTACHMENTS: Allow accessing attachments only through pre-signed URLs. Default: false - `NC_MAX_ATTACHMENTS_ALLOWED`: Maximum number of attachments per cell. Default: `10`
- NC_ATTACHMENT_EXPIRE_SECONDS: Expiry time for pre-signed URLs. Default: 7200 - `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 ## Related articles
- [Attaching a file from mobile](/views/view-types/form#attaching-a-file-from-mobile-device) - [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` ## Accessing `Data Sources`
1. Access Base context menu by clicking on the `Base` name in the left sidebar 1. Access Base context menu by clicking on the `...` in the left sidebar against the base name
2. Click on `Data Sources` tab 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: 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: 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 1. Access the Base context menu by clicking on the `...` in the left sidebar against the base name
2. Select `Data Sources` tab 2. Click on `Settings` tab
3. Click on `+ New Data Source` button 3. In the popup modal, click on `Data Sources` tab
4. On the pop-up modal, provide the following details: 4. Click on `+ New Data Source` button
5. On the input modal, provide the following details:
| Field Name | Description | | 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 | | Database | Name of the database to connect to |
| Schema name | Name of the schema 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` - Click on `SSL & Advanced Parameters`
- Select `SSL Mode` and upload the client certificate, client key, and Root CA files by clicking on the file. - 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.\ 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). 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. Click on the `Test Database Connection` button to verify the connection
6. Wait for the connection to be verified. 7. Wait for the connection to be verified.
- After connection is successful, `Submit` button will be enabled. - After test is successful, `Counnect to Data Source` button will be enabled.
- Click on `Submit` button to save the data source. - 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'] keywords: ['NocoDB data source', 'connect data source', 'external data source', 'PG data source', 'MySQL data source']
--- ---
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
1. Access Base context menu by clicking on the `Base` name in the left sidebar ![sync metadata](/img/v2/data-source/data-source-meta-sync-1.png)
2. Select `Data Sources` tab ![sync metadata](/img/v2/data-source/data-source-meta-sync-2.png)
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
![sync metadata](/img/v2/data-source/data-source-2.png)
![sync metadata](/img/v2/data-source/data-source-meta-sync.png)
After the sync is complete, you can see the updated state in the `Sync State` column. 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. Sync modal also marks `Tables metadata is in Sync` in the header.

67
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 ## 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 :::info
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! 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) ![edit-data-source](/img/v2/data-source/data-source-edit.png)
![edit db config](/img/v2/data-source/edit-base.png)
## Remove data source ## Remove data source
1. Access Base context menu by clicking on the `Base` name in the left sidebar - Access `Data Sources` tab in the `Base Settings`
2. Click on `Data Sources` tab - Click on `Delete` icon listed under `Actions` column for the data source that you wish to remove
3. Click on `Delete` icon listed under `Actions` column for the data source that you wish to Unlink
![datasource unlink](/img/v2/data-source/data-source-unlink.png) ![datasource unlink](/img/v2/data-source/data-source-remove.png)
:::note :::note
Unlinking a data source will not delete the external data source. It will only remove the data source from the current project. 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 ## Data source visibility
1. Access Base context menu by clicking on the `Base` name in the left sidebar - Access `Data Sources` tab in the `Base Settings`
2. Click on `Data Sources` tab - Toggle radio button listed under `Visibility` column for the data source that you wish to hide/un-hide
3. 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 ## UI Access Control
:::info :::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 Access `Data Sources` tab in the `Base Settings` to manage UI access control for the data source.
2. Click on `Data Sources` tab 1. Click on `UI ACL` button listed under `Actions` column for the data source that you wish to manage UI access control for
3. 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.
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. 3. Click on `Save` button to save the changes
5. Click on `Save` button to save the changes
![ui acl](/img/v2/data-source/data-source-3.png) ![ui acl](/img/v2/data-source/data-source-uiacl.png)
![ui acl](/img/v2/data-source/ui-acl.png)
## Audit logs ## Audit logs
1. Access Base context menu by clicking on the `Base` name in the left sidebar Access `Data Sources` tab in the `Base Settings` to access Audit logs for the data source.
2. Click on `Data Sources` tab - Click on `Default` datasource & then
3. Click on `Audit` button listed under `Actions` column for the data source that you wish to access Audit logs for - Access `Audit` tab to view the audit logs.
![audit](/img/v2/data-source/audit.png)
![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 ## 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 ![relations](/img/v2/data-source/data-source-erd.png)
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)
### Junction table names within Relations ### 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": { "dependencies": {
"axios": "^1.6.8", "axios": "^1.6.8",
"jsep": "^1.3.8", "jsep": "^1.3.8",
"dayjs": "^1.11.10" "dayjs": "^1.11.11"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
@ -56,8 +56,8 @@
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"rimraf": "^5.0.5", "rimraf": "^5.0.7",
"tsc-alias": "^1.8.8", "tsc-alias": "^1.8.10",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"ts-jest": "^29.1.2" "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) { if (parsedTree.type === JSEPNode.CALL_EXP) {
const calleeName = parsedTree.callee.name.toUpperCase(); const calleeName = parsedTree.callee.name.toUpperCase();
// validate function name // validate function name
if ( if (!formulas[calleeName]) {
!formulas[calleeName] ||
sqlUI?.getUnsupportedFnList().includes(calleeName)
) {
throw new FormulaError( throw new FormulaError(
FormulaErrorType.INVALID_FUNCTION_NAME, FormulaErrorType.INVALID_FUNCTION_NAME,
{}, {},
`Function ${calleeName} is not available` `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 // validate arguments

45
packages/nocodb/Dockerfile

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

26
packages/nocodb/Dockerfile.local

@ -1,3 +1,5 @@
# syntax=docker/dockerfile:1
########### ###########
# Builder # Builder
########### ###########
@ -11,11 +13,11 @@ RUN apk add --no-cache python3 make g++
RUN corepack enable && corepack prepare pnpm@latest --activate RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy application dependency manifests to the container image. # Copy application dependency manifests to the container image.
COPY ./package.json ./package.json COPY --link ./package.json ./package.json
COPY ./docker/nc-gui/ ./docker/nc-gui/ COPY --link ./docker/nc-gui/ ./docker/nc-gui/
COPY ./docker/main.js ./docker/index.js COPY --link ./docker/main.js ./docker/index.js
COPY ./docker/start-local.sh /usr/src/appEntry/start.sh COPY --link ./docker/start-local.sh /usr/src/appEntry/start.sh
COPY src/public/ ./docker/public/ COPY --link src/public/ ./docker/public/
# for pnpm to generate a flat node_modules without symlinks # for pnpm to generate a flat node_modules without symlinks
# so that modclean could work as expected # so that modclean could work as expected
@ -36,20 +38,20 @@ RUN pnpm install --prod --shamefully-hoist --reporter=silent \
FROM alpine:3.19 FROM alpine:3.19
WORKDIR /usr/src/app WORKDIR /usr/src/app
ENV NC_DOCKER 0.6 ENV NC_DOCKER=0.6 \
ENV NODE_ENV production NC_TOOL_DIR=/usr/app/data/ \
ENV PORT 8080 NODE_ENV=production \
ENV NC_TOOL_DIR=/usr/app/data/ PORT=8080
RUN apk --update --no-cache add \ RUN apk add --update --no-cache \
nodejs \ nodejs \
dumb-init \ dumb-init \
curl \ curl \
jq jq
# Copy production code & main entry file # Copy production code & main entry file
COPY --from=builder /usr/src/app/ /usr/src/app/ COPY --link --from=builder /usr/src/app/ /usr/src/app/
COPY --from=builder /usr/src/appEntry/ /usr/src/appEntry/ COPY --link --from=builder /usr/src/appEntry/ /usr/src/appEntry/
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["/usr/bin/dumb-init", "--"] 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 #!/bin/sh
#sleep 5 if [ ! -d "${NC_TOOL_DIR}" ] ; then
if [ -n "${NC_TOOL_DIR}" ]; then
mkdir -p "$NC_TOOL_DIR" mkdir -p "$NC_TOOL_DIR"
fi 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" ] if [ -f "${NC_TOOL_DIR}noco.db" ] ; then
then
rm "${NC_TOOL_DIR}noco.db" rm "${NC_TOOL_DIR}noco.db"
rm "${NC_TOOL_DIR}noco.db-shm" rm -f "${NC_TOOL_DIR}noco.db-shm"
rm "${NC_TOOL_DIR}noco.db-wal" rm -f "${NC_TOOL_DIR}noco.db-wal"
fi fi
/usr/src/appEntry/litestream restore -o "${NC_TOOL_DIR}noco.db" "s3://$AWS_BUCKET/$AWS_BUCKET_PATH" litestream restore "${NC_TOOL_DIR}noco.db"
if [ ! -f "${NC_TOOL_DIR}noco.db" ]
then if [ ! -f "${NC_TOOL_DIR}noco.db" ] ; then
touch "${NC_TOOL_DIR}noco.db" touch "${NC_TOOL_DIR}noco.db"
fi fi
/usr/src/appEntry/litestream replicate "${NC_TOOL_DIR}noco.db" "s3://$AWS_BUCKET/$AWS_BUCKET_PATH" &
litestream replicate &
fi fi
node docker/main.js node docker/main.js

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

@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
if [ -n "${NC_TOOL_DIR}" ]; then if [ ! -d "${NC_TOOL_DIR}" ]; then
mkdir -p "$NC_TOOL_DIR" mkdir -p "$NC_TOOL_DIR"
fi 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"]

30
packages/nocodb/package.json

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

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

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

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

@ -11,6 +11,7 @@ export enum JobTypes {
UpdateModelStat = 'update-model-stat', UpdateModelStat = 'update-model-stat',
UpdateWsStat = 'update-ws-stats', UpdateWsStat = 'update-ws-stats',
UpdateSrcStat = 'update-source-stat', UpdateSrcStat = 'update-source-stat',
HealthCheck = 'health-check',
} }
export enum JobStatus { 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.meta = stringifyMetaProp(insertObj);
} }
insertObj.order = await ncMeta.metaGetNextOrder(MetaTable.BASES, {
base_id: source.baseId,
});
const { id } = await ncMeta.metaInsert2( const { id } = await ncMeta.metaInsert2(
source.baseId, source.baseId,
null, null,
@ -92,8 +96,6 @@ export default class Source implements SourceType {
`${CacheScope.BASE}:${id}`, `${CacheScope.BASE}:${id}`,
); );
await this.reorderBases(source.baseId);
return returnBase; return returnBase;
} }
@ -101,7 +103,6 @@ export default class Source implements SourceType {
sourceId: string, sourceId: string,
source: SourceType & { source: SourceType & {
baseId: string; baseId: string;
skipReorder?: boolean;
meta?: any; meta?: any;
deleted?: boolean; deleted?: boolean;
fk_sql_executor_id?: string; fk_sql_executor_id?: string;
@ -138,6 +139,36 @@ export default class Source implements SourceType {
updateObj.type = oldBase.type; 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( await ncMeta.metaUpdate(
source.baseId, source.baseId,
null, null,
@ -154,10 +185,6 @@ export default class Source implements SourceType {
// call before reorder to update cache // call before reorder to update cache
const returnBase = await this.get(oldBase.id, false, ncMeta); 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; return returnBase;
} }
@ -288,41 +315,6 @@ export default class Source implements SourceType {
return this.castType(source); 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> { public async getConnectionConfig(): Promise<any> {
const config = this.getConfig(); 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);
});
}
}
}

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

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

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

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { type UserType, ViewTypes } from 'nocodb-sdk'; import { type UserType, ViewTypes } from 'nocodb-sdk';
import { Base } from '~/models'; import { BaseUser } from '~/models';
import { TablesService } from '~/services/tables.service'; import { TablesService } from '~/services/tables.service';
import { deserializeJSON } from '~/utils/serialize'; import { deserializeJSON } from '~/utils/serialize';
@ -20,30 +20,9 @@ export class CommandPaletteService {
async commandPalette(param: { body: any; user: UserType }) { async commandPalette(param: { body: any; user: UserType }) {
const cmdData = []; const cmdData = [];
try { try {
const { scope } = param.body;
if (scope === 'root') {
const bases = await Base.list({ user: param.user });
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 allBases = [];
const bases = await Base.list({ user: param.user }); const bases = await BaseUser.getProjectsList(param.user.id, param);
allBases.push(...bases); allBases.push(...bases);
@ -108,7 +87,6 @@ export class CommandPaletteService {
cmdData.push(...tableList); cmdData.push(...tableList);
cmdData.push(...vwList); cmdData.push(...vwList);
}
} catch (e) { } catch (e) {
console.log(e); console.log(e);
return []; return [];

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

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

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

@ -276,6 +276,7 @@ const rolePermissions:
baseList: true, baseList: true,
testConnection: true, testConnection: true,
isPluginActive: true, isPluginActive: true,
commandPalette: true,
}, },
}, },
[OrgUserRoles.CREATOR]: { [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 // 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 // for example: example.com to be converted as example<span>.<span>com
export const sanitiseEmailContent = (content: string) => { export const sanitiseEmailContent = (content?: string) => {
return content return content
.replace(/[<>&;?#,'"$]+/g, encode) ?.replace(/[<>&;?#,'"$]+/g, encode)
.replace(/\.|\/\/:/g, '<span>$&</span>'); ?.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, id: source.id,
baseId: source.base_id, baseId: source.base_id,
config, config,
skipReorder: true,
}, },
ncMeta, 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": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nestjs/common": "^10.3.7", "@nestjs/common": "^10.3.8",
"@nestjs/core": "^10.3.7", "@nestjs/core": "^10.3.8",
"express": "^4.18.3", "express": "^4.18.3",
"nocodb": "0.204.9" "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 // replace nc-lib-gui by nc-lib-gui-daily if it is nightly build / pr release
const filePaths = [ const filePaths = [
path.join(__dirname, '..', 'packages', 'nocodb', 'Dockerfile'), 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', 'package.json'),
path.join(__dirname, '..', 'packages', 'nocodb', 'src', 'Noco.ts'), path.join(__dirname, '..', 'packages', 'nocodb', 'src', 'Noco.ts'),
path.join(__dirname, '..', 'packages', 'nocodb', 'src', 'nocobuild.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({ hour.click({
force: true, force: true,
position: { position: {
x: 1, x: 0,
y: 1, y: 0,
}, },
}), }),
requestUrlPathToMatch: '/api/v1/db/data/noco', 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.get().click();
await this.rootPage.keyboard.press('Escape'); await this.rootPage.keyboard.press('Escape');
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() { 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(); expect(await this.tab_allTables.isVisible()).toBeTruthy();
if (role.toLowerCase() === 'creator' || role.toLowerCase() === 'owner') { if (role.toLowerCase() === 'creator' || role.toLowerCase() === 'owner') {
await this.tab_dataSources.waitFor({ state: 'visible' });
await this.tab_accessSettings.waitFor({ state: 'visible' }); await this.tab_accessSettings.waitFor({ state: 'visible' });
expect(await this.tab_dataSources.isVisible()).toBeTruthy();
expect(await this.tab_accessSettings.isVisible()).toBeTruthy(); expect(await this.tab_accessSettings.isVisible()).toBeTruthy();
} else { } else {
expect(await this.tab_dataSources.isVisible()).toBeFalsy();
expect(await this.tab_accessSettings.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() { 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 }) { async openErd({ rowIndex }: { rowIndex: number }) {
await this.get().locator('.ds-table-row', { hasText: dataSourceName }).locator('button:has-text("ERD")').click(); 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 } = {}) { async openAcl({ dataSourceName = defaultBaseName }: { dataSourceName?: string } = {}) {
await this.get().locator('.ds-table-row', { hasText: dataSourceName }).locator('button:has-text("UI ACL")').click(); await this.get().locator('.ds-table-row', { hasText: dataSourceName }).locator('button:has-text("UI ACL")').click();
} }
async openMetaSync({ dataSourceName = defaultBaseName }: { dataSourceName?: string } = {}) { async openMetaSync({ rowIndex }: { rowIndex: number }) {
await this.get() // 0th offset for header
.locator('.ds-table-row', { hasText: dataSourceName }) const row = this.get()
.locator('button:has-text("Sync Metadata")') .locator('.ds-table-row')
.click(); .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.waitForTableOptions({ title });
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator('.nc-tbl-context-menu').click(); 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({ await this.waitForResponse({
uiAction: async () => { uiAction: async () => {
@ -205,7 +205,7 @@ export class TreeViewPage extends BasePage {
await this.waitForTableOptions({ title }); await this.waitForTableOptions({ title });
await this.get().locator(`.nc-base-tree-tbl-${tableTitle}`).locator('.nc-tbl-context-menu').click(); 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('[placeholder="Enter table name"]').fill(newTitle);
await this.dashboard.get().locator('button:has-text("Rename Table")').click(); 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 () => { const openProjectErd = async () => {
await dashboard.baseView.tab_dataSources.click(); await dashboard.treeView.baseSettings({ title: context.base.title });
await dashboard.baseView.dataSources.openERD({ rowIndex: 0 }); 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) => { const openErdOfATable = async (tableName: string) => {

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

Loading…
Cancel
Save