Browse Source

Merge branch 'develop' into enhancement/rollup

pull/6853/head
աɨռɢӄաօռɢ 12 months ago
parent
commit
077d9f84ae
  1. 7
      packages/nc-gui/assets/style.scss
  2. 1
      packages/nc-gui/components.d.ts
  3. 1
      packages/nc-gui/components/account/UsersModal.vue
  4. 1
      packages/nc-gui/components/api-client/Headers.vue
  5. 3
      packages/nc-gui/components/cell/DatePicker.vue
  6. 2
      packages/nc-gui/components/cell/DateTimePicker.vue
  7. 2
      packages/nc-gui/components/cell/GeoData.vue
  8. 9
      packages/nc-gui/components/cell/Json.vue
  9. 87
      packages/nc-gui/components/cell/Percent.vue
  10. 25
      packages/nc-gui/components/cell/TextArea.vue
  11. 2
      packages/nc-gui/components/cell/TimePicker.vue
  12. 2
      packages/nc-gui/components/cell/YearPicker.vue
  13. 22
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  14. 29
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  15. 284
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  16. 37
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  17. 8
      packages/nc-gui/components/dashboard/settings/BaseAudit.vue
  18. 17
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  19. 2
      packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue
  20. 2
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  21. 2
      packages/nc-gui/components/erd/TableNode.vue
  22. 106
      packages/nc-gui/components/general/JoinCloud.vue
  23. 2
      packages/nc-gui/components/general/language/index.vue
  24. 2
      packages/nc-gui/components/nc/Dropdown.vue
  25. 14
      packages/nc-gui/components/nc/Modal.vue
  26. 10
      packages/nc-gui/components/nc/Select.vue
  27. 11
      packages/nc-gui/components/nc/Tooltip.vue
  28. 2
      packages/nc-gui/components/project/AccessSettings.vue
  29. 54
      packages/nc-gui/components/project/View.vue
  30. 1
      packages/nc-gui/components/smartsheet/Cell.vue
  31. 13
      packages/nc-gui/components/smartsheet/Toolbar.vue
  32. 2
      packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue
  33. 5
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  34. 8
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  35. 51
      packages/nc-gui/components/smartsheet/column/PercentOptions.vue
  36. 47
      packages/nc-gui/components/smartsheet/column/PercentOptionsLegacy.vue
  37. 2
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  38. 29
      packages/nc-gui/components/smartsheet/details/Fields.vue
  39. 12
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  40. 4
      packages/nc-gui/components/smartsheet/grid/Table.vue
  41. 17
      packages/nc-gui/components/smartsheet/header/Cell.vue
  42. 2
      packages/nc-gui/components/smartsheet/header/Menu.vue
  43. 11
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  44. 11
      packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue
  45. 13
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  46. 43
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  47. 20
      packages/nc-gui/components/smartsheet/toolbar/OpenedViewAction.vue
  48. 2
      packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue
  49. 5
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  50. 2
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  51. 12
      packages/nc-gui/components/template/Editor.vue
  52. 1
      packages/nc-gui/composables/useAttachment.ts
  53. 10
      packages/nc-gui/composables/useColumnCreateStore.ts
  54. 4
      packages/nc-gui/composables/useViewColumns.ts
  55. 2
      packages/nc-gui/helpers/parsers/CSVTemplateAdapter.ts
  56. 5
      packages/nc-gui/helpers/parsers/ExcelTemplateAdapter.ts
  57. 2
      packages/nc-gui/helpers/parsers/JSONTemplateAdapter.ts
  58. 1
      packages/nc-gui/lang/ar.json
  59. 1
      packages/nc-gui/lang/bn_IN.json
  60. 1
      packages/nc-gui/lang/cs.json
  61. 1
      packages/nc-gui/lang/da.json
  62. 1
      packages/nc-gui/lang/de.json
  63. 5
      packages/nc-gui/lang/en.json
  64. 1
      packages/nc-gui/lang/es.json
  65. 1
      packages/nc-gui/lang/eu.json
  66. 1
      packages/nc-gui/lang/fa.json
  67. 1
      packages/nc-gui/lang/fi.json
  68. 1
      packages/nc-gui/lang/fr.json
  69. 1
      packages/nc-gui/lang/he.json
  70. 1
      packages/nc-gui/lang/hi.json
  71. 1
      packages/nc-gui/lang/hr.json
  72. 1
      packages/nc-gui/lang/id.json
  73. 1
      packages/nc-gui/lang/it.json
  74. 53
      packages/nc-gui/lang/ja.json
  75. 1
      packages/nc-gui/lang/ko.json
  76. 1
      packages/nc-gui/lang/lv.json
  77. 1
      packages/nc-gui/lang/nl.json
  78. 1
      packages/nc-gui/lang/no.json
  79. 1
      packages/nc-gui/lang/pl.json
  80. 1
      packages/nc-gui/lang/pt.json
  81. 1
      packages/nc-gui/lang/pt_BR.json
  82. 1
      packages/nc-gui/lang/ru.json
  83. 1
      packages/nc-gui/lang/sk.json
  84. 1
      packages/nc-gui/lang/sl.json
  85. 1
      packages/nc-gui/lang/sv.json
  86. 1
      packages/nc-gui/lang/th.json
  87. 1
      packages/nc-gui/lang/tr.json
  88. 1
      packages/nc-gui/lang/uk.json
  89. 1
      packages/nc-gui/lang/vi.json
  90. 755
      packages/nc-gui/lang/zh-Hans.json
  91. 1
      packages/nc-gui/lang/zh-Hant.json
  92. 20
      packages/nc-gui/package.json
  93. 2
      packages/nc-gui/pages/signin.vue
  94. 2
      packages/nc-gui/store/bases.ts
  95. 18
      packages/nc-gui/utils/validation.ts
  96. 2
      packages/nc-lib-gui/package.json
  97. 378
      packages/noco-docs/docs/070.fields/040.field-types/060.formula/020.numeric-functions.md
  98. 228
      packages/noco-docs/docs/070.fields/040.field-types/060.formula/030.string-functions.md
  99. 101
      packages/noco-docs/docs/070.fields/040.field-types/060.formula/040.date-functions.md
  100. 76
      packages/noco-docs/docs/070.fields/040.field-types/060.formula/050.conditional-expressions.md
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -225,6 +225,10 @@ a {
@apply !rounded-md; @apply !rounded-md;
} }
} }
// select dropdown border style
.ant-select-dropdown {
@apply border-1 border-gray-200
}
// menu item styling // menu item styling
.nc-menu-item { .nc-menu-item {
@ -427,6 +431,9 @@ a {
.ant-dropdown-menu-submenu { .ant-dropdown-menu-submenu {
@apply !py-0; @apply !py-0;
&.ant-dropdown-menu-submenu-popup{
@apply border-1 border-gray-200
}
.ant-dropdown-menu, .ant-dropdown-menu,
.ant-menu { .ant-menu {
@apply m-0 p-0; @apply m-0 p-0;

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

@ -49,6 +49,7 @@ declare module '@vue/runtime-core' {
AModal: typeof import('ant-design-vue/es')['Modal'] AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination'] APagination: typeof import('ant-design-vue/es')['Pagination']
APopover: typeof import('ant-design-vue/es')['Popover'] APopover: typeof import('ant-design-vue/es')['Popover']
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadio: typeof import('ant-design-vue/es')['Radio'] ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARate: typeof import('ant-design-vue/es')['Rate'] ARate: typeof import('ant-design-vue/es')['Rate']

1
packages/nc-gui/components/account/UsersModal.vue

@ -192,6 +192,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<div class="flex flex-col w-2/4"> <div class="flex flex-col w-2/4">
<a-form-item name="role" :rules="[{ required: true, message: $t('msg.roleRequired') }]"> <a-form-item name="role" :rules="[{ required: true, message: $t('msg.roleRequired') }]">
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('labels.selectUserRole') }}</div> <div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('labels.selectUserRole') }}</div>
<a-select v-model:value="usersData.role" class="nc-user-roles" dropdown-class-name="nc-dropdown-user-role"> <a-select v-model:value="usersData.role" class="nc-user-roles" dropdown-class-name="nc-dropdown-user-role">
<a-select-option <a-select-option
class="nc-role-option" class="nc-role-option"

1
packages/nc-gui/components/api-client/Headers.vue

@ -91,6 +91,7 @@ const filterOption = (input: string, option: Option) => option.value.toUpperCase
:options="headerList" :options="headerList"
:placeholder="$t('placeholder.key')" :placeholder="$t('placeholder.key')"
:filter-option="filterOption" :filter-option="filterOption"
dropdown-class-name="border-1 border-gray-200"
/> />
</a-form-item> </a-form-item>
</td> </td>

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

@ -22,6 +22,7 @@ interface Props {
} }
const { modelValue, isPk } = defineProps<Props>() const { modelValue, isPk } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const { t } = useI18n() const { t } = useI18n()
@ -225,7 +226,7 @@ const clickHandler = () => {
:placeholder="placeholder" :placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk" :allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true" :input-read-only="true"
:dropdown-class-name="`${randomClass} nc-picker-date ${open ? 'active' : ''}`" :dropdown-class-name="`${randomClass} nc-picker-date children:border-1 children:border-gray-200 ${open ? 'active' : ''} `"
:open="isOpen" :open="isOpen"
@click="clickHandler" @click="clickHandler"
@update:open="updateOpen" @update:open="updateOpen"

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

@ -273,7 +273,7 @@ const isColDisabled = computed(() => {
:placeholder="placeholder" :placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk" :allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true" :input-read-only="true"
:dropdown-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''}`" :dropdown-class-name="`${randomClass} nc-picker-datetime children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
:open="isOpen" :open="isOpen"
@click="clickHandler" @click="clickHandler"
@ok="open = !open" @ok="open = !open"

2
packages/nc-gui/components/cell/GeoData.vue

@ -100,7 +100,7 @@ const openInOSM = () => {
</div> </div>
<div v-else data-testid="nc-geo-data-lat-long-set">{{ latLongStr }}</div> <div v-else data-testid="nc-geo-data-lat-long-set">{{ latLongStr }}</div>
<template #overlay> <template #overlay>
<a-form :model="formState" class="flex flex-col w-max-64" @finish="handleFinish"> <a-form :model="formState" class="flex flex-col w-max-64 border-1 border-gray-200" @finish="handleFinish">
<a-form-item> <a-form-item>
<div class="flex mt-4 items-center mx-2"> <div class="flex mt-4 items-center mx-2">
<div class="mr-2">{{ $t('labels.lat') }}:</div> <div class="mr-2">{{ $t('labels.lat') }}:</div>

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

@ -150,7 +150,14 @@ watch(isExpanded, () => {
</script> </script>
<template> <template>
<component :is="isExpanded ? NcModal : 'div'" v-model:visible="isExpanded" :closable="false" centered :footer="null"> <component
:is="isExpanded ? NcModal : 'div'"
v-model:visible="isExpanded"
:closable="false"
centered
:footer="null"
:wrap-class-name="isExpanded ? '!z-1051' : null"
>
<div v-if="editEnabled && !readonly" class="flex flex-col w-full" @mousedown.stop @mouseup.stop @click.stop> <div v-if="editEnabled && !readonly" class="flex flex-col w-full" @mousedown.stop @mouseup.stop @click.stop>
<div class="flex flex-row justify-between pt-1 pb-2 nc-json-action" @mousedown.stop> <div class="flex flex-row justify-between pt-1 pb-2 nc-json-action" @mousedown.stop>
<a-button type="text" size="small" @click="isExpanded = !isExpanded"> <a-button type="text" size="small" @click="isExpanded = !isExpanded">

87
packages/nc-gui/components/cell/Percent.vue

@ -12,6 +12,8 @@ const emits = defineEmits(['update:modelValue'])
const { showNull } = useGlobal() const { showNull } = useGlobal()
const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj) const editEnabled = inject(EditModeInj)
const isEditColumn = inject(EditColumnInj, ref(false)) const isEditColumn = inject(EditColumnInj, ref(false))
@ -32,26 +34,73 @@ const vModel = computed({
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLInputElement)?.focus()
const cellFocused = ref(false)
const expandedEditEnabled = ref(false)
const percentMeta = computed(() => {
return {
is_progress: false,
...parseProp(column.value?.meta),
}
})
const onBlur = () => {
if (editEnabled) {
editEnabled.value = false
}
cellFocused.value = false
expandedEditEnabled.value = false
}
const onFocus = () => {
cellFocused.value = true
}
const onMouseover = () => {
expandedEditEnabled.value = true
}
const onMouseleave = () => {
if (!cellFocused.value) {
expandedEditEnabled.value = false
}
}
</script> </script>
<template> <template>
<input <div class="nc-filter-value-select w-full" @mouseover="onMouseover" @mouseleave="onMouseleave">
v-if="editEnabled" <input
:ref="focus" v-if="(!isExpandedFormOpen && editEnabled) || (isExpandedFormOpen && expandedEditEnabled)"
v-model="vModel" :ref="focus"
class="w-full !text-sm !border-none !outline-none focus:ring-0 text-base p-1" v-model="vModel"
:class="{ '!px-2': editEnabled }" class="w-full !text-sm !border-none !outline-none focus:ring-0 text-base p-1"
type="number" :class="{ '!px-2': editEnabled }"
:placeholder="isEditColumn ? $t('labels.optional') : ''" type="number"
@blur="editEnabled = false" :placeholder="isEditColumn ? $t('labels.optional') : ''"
@keydown.down.stop @blur="onBlur"
@keydown.left.stop @focus="onFocus"
@keydown.right.stop @keydown.down.stop
@keydown.up.stop @keydown.left.stop
@keydown.delete.stop @keydown.right.stop
@selectstart.capture.stop @keydown.up.stop
@mousedown.stop @keydown.delete.stop
/> @selectstart.capture.stop
<span v-else-if="vModel === null && showNull" class="nc-null capitalize">{{ $t('general.null') }}</span> @mousedown.stop
<span v-else>{{ vModel }}</span> />
<span v-else-if="vModel === null && showNull" class="nc-null capitalize">{{ $t('general.null') }}</span>
<div v-else-if="percentMeta.is_progress === true && vModel !== null && vModel !== undefined" class="px-2">
<a-progress
:percent="Number(parseFloat(vModel.toString()).toFixed(2))"
size="small"
status="normal"
stroke-color="#3366FF"
trail-color="#E5E5E5"
:show-info="false"
/>
</div>
<!-- nbsp to keep height even if vModel is zero length -->
<span v-else>{{ vModel }}&nbsp;</span>
</div>
</template> </template>

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

@ -1,16 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { import {
ActiveCellInj, ColumnInj,
EditColumnInj, EditColumnInj,
EditModeInj, EditModeInj,
IsExpandedFormOpenInj, IsExpandedFormOpenInj,
IsFormInj,
ReadonlyInj, ReadonlyInj,
RowHeightInj, RowHeightInj,
computed,
iconMap, iconMap,
inject, inject,
onClickOutside,
ref,
useGlobal,
useVModel, useVModel,
watch,
} from '#imports' } from '#imports'
const props = defineProps<{ const props = defineProps<{
@ -61,9 +66,8 @@ const height = computed(() => {
const isVisible = ref(false) const isVisible = ref(false)
const inputWrapperRef = ref<HTMLElement | null>(null) const inputWrapperRef = ref<HTMLElement | null>(null)
const inputRef = ref<HTMLTextAreaElement | null>(null)
const active = inject(ActiveCellInj, ref(false)) const inputRef = ref<HTMLTextAreaElement | null>(null)
const readOnly = inject(ReadonlyInj) const readOnly = inject(ReadonlyInj)
@ -171,7 +175,7 @@ watch(editEnabled, () => {
:overlay-class-name="isVisible ? 'nc-textarea-dropdown-active' : undefined" :overlay-class-name="isVisible ? 'nc-textarea-dropdown-active' : undefined"
> >
<div <div
class="flex flex-row pt-0.5 w-full" class="flex flex-row pt-0.5 w-full rich-wrapper"
:class="{ :class="{
'min-h-10': rowHeight !== 1, 'min-h-10': rowHeight !== 1,
'min-h-6.5': rowHeight === 1, 'min-h-6.5': rowHeight === 1,
@ -235,7 +239,7 @@ watch(editEnabled, () => {
<NcTooltip <NcTooltip
v-if="!isVisible" v-if="!isVisible"
placement="bottom" placement="bottom"
class="!absolute right-0 bottom-1 !hidden nc-text-area-expand-btn" class="!absolute right-0 bottom-1 nc-text-area-expand-btn"
:class="{ 'right-0 bottom-1': editEnabled, '!bottom-0': !isRichMode }" :class="{ 'right-0 bottom-1': editEnabled, '!bottom-0': !isRichMode }"
> >
<template #title>{{ $t('title.expand') }}</template> <template #title>{{ $t('title.expand') }}</template>
@ -292,8 +296,13 @@ watch(editEnabled, () => {
textarea:focus { textarea:focus {
box-shadow: none; box-shadow: none;
} }
:deep(.nc-text-area-expand-btn) { :deep(.nc-text-area-expand-btn) {
@apply !block; @apply !hidden;
}
.rich-wrapper:hover,
.rich-wrapper:active {
:deep(.nc-text-area-expand-btn) {
@apply !block cursor-pointer;
}
} }
</style> </style>

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

@ -136,7 +136,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:allow-clear="!readOnly && !localState && !isPk" :allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true" :input-read-only="true"
:open="isOpen" :open="isOpen"
:popup-class-name="`${randomClass} nc-picker-time ${open ? 'active' : ''}`" :popup-class-name="`${randomClass} nc-picker-time children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
@click="open = (active || editable) && !open" @click="open = (active || editable) && !open"
@ok="open = !open" @ok="open = !open"
> >

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

@ -121,7 +121,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:allow-clear="(!readOnly && !localState && !isPk) || isEditColumn" :allow-clear="(!readOnly && !localState && !isPk) || isEditColumn"
:input-read-only="true" :input-read-only="true"
:open="isOpen" :open="isOpen"
:dropdown-class-name="`${randomClass} nc-picker-year ${open ? 'active' : ''}`" :dropdown-class-name="`${randomClass} nc-picker-year children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
@click="open = (active || editable) && !open" @click="open = (active || editable) && !open"
@change="open = (active || editable) && !open" @change="open = (active || editable) && !open"
@ok="open = !open" @ok="open = !open"

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

@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import GithubButton from 'vue-github-button'
import { import {
computed, computed,
message, message,
@ -79,7 +78,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="flex w-full flex-col p-1 border-t-1 border-gray-200 gap-y-2"> <div class="flex w-full flex-col p-1 border-t-1 border-gray-200 gap-y-1">
<NcDropdown v-model:visible="isMenuOpen" placement="topLeft" overlay-class-name="!min-w-64"> <NcDropdown v-model:visible="isMenuOpen" placement="topLeft" overlay-class-name="!min-w-64">
<div <div
class="flex flex-row py-2 px-3 gap-x-2 items-center hover:bg-gray-200 rounded-lg cursor-pointer h-10" class="flex flex-row py-2 px-3 gap-x-2 items-center hover:bg-gray-200 rounded-lg cursor-pointer h-10"
@ -205,22 +204,9 @@ onMounted(() => {
</NcDropdown> </NcDropdown>
<template v-if="isMobileMode"></template> <template v-if="isMobileMode"></template>
<div v-else-if="appInfo.ee" class="text-gray-500 text-xs pl-3">© 2023 NocoDB. Inc</div> <div v-else-if="appInfo.ee" class="text-gray-500 text-xs pl-3 mt-1">© 2023 NocoDB. Inc</div>
<div v-else-if="isMounted" class="flex flex-row justify-between pt-1 truncate"> <div v-else class="flex flex-row w-full justify-between pt-0.5 truncate">
<div class="flex flex-wrap mb-1"> <GeneralJoinCloud />
<GithubButton
class="px-2 mb-1"
href="https://github.com/nocodb/nocodb"
data-icon="octicon-star"
data-show-count="true"
data-size="large"
>
Star
</GithubButton>
<div>
<GeneralJoinCloud class="color-transition px-2 text-gray-500 cursor-pointer select-none hover:text-accent" />
</div>
</div>
</div> </div>
</div> </div>
</template> </template>

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

@ -77,7 +77,7 @@ const tempTitle = ref('')
const activeBaseId = ref('') const activeBaseId = ref('')
const isErdModalOpen = ref<boolean>(false) const isErdModalOpen = ref<Boolean>(false)
const { t } = useI18n() const { t } = useI18n()
@ -116,7 +116,7 @@ const showBaseOption = computed(() => {
return ['airtableImport', 'csvImport', 'jsonImport', 'excelImport'].some((permission) => isUIAllowed(permission)) return ['airtableImport', 'csvImport', 'jsonImport', 'excelImport'].some((permission) => isUIAllowed(permission))
}) })
function enableEditMode() { const enableEditMode = () => {
editMode.value = true editMode.value = true
tempTitle.value = base.value.title! tempTitle.value = base.value.title!
nextTick(() => { nextTick(() => {
@ -126,7 +126,7 @@ function enableEditMode() {
}) })
} }
async function updateProjectTitle() { const updateProjectTitle = async () => {
if (!tempTitle.value) return if (!tempTitle.value) return
try { try {
@ -146,7 +146,7 @@ async function updateProjectTitle() {
const { copy } = useCopy(true) const { copy } = useCopy(true)
async function copyProjectInfo() { const copyProjectInfo = async () => {
try { try {
if ( if (
await copy( await copy(
@ -168,7 +168,7 @@ defineExpose({
enableEditMode, enableEditMode,
}) })
async function setIcon(icon: string, base: BaseType) { const setIcon = async (icon: string, base: BaseType) => {
try { try {
const meta = { const meta = {
...((base.meta as object) || {}), ...((base.meta as object) || {}),
@ -249,7 +249,7 @@ async function addNewProjectChildEntity() {
} }
} }
async function onProjectClick(base: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) { const onProjectClick = async (base: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) => {
if (!base) { if (!base) {
return return
} }
@ -348,17 +348,17 @@ onKeyStroke('Escape', () => {
const isDuplicateDlgOpen = ref(false) const isDuplicateDlgOpen = ref(false)
const selectedProjectToDuplicate = ref() const selectedProjectToDuplicate = ref()
function duplicateProject(base: BaseType) { const duplicateProject = (base: BaseType) => {
selectedProjectToDuplicate.value = base selectedProjectToDuplicate.value = base
isDuplicateDlgOpen.value = true isDuplicateDlgOpen.value = true
} }
function tableDelete() { const tableDelete = () => {
isTableDeleteDialogVisible.value = true isTableDeleteDialogVisible.value = true
$e('c:table:delete') $e('c:table:delete')
} }
function projectDelete() { const projectDelete = () => {
isProjectDeleteDialogVisible.value = true isProjectDeleteDialogVisible.value = true
$e('c:project:delete') $e('c:project:delete')
} }
@ -424,15 +424,18 @@ function projectDelete() {
@keyup.esc="updateProjectTitle" @keyup.esc="updateProjectTitle"
@blur="updateProjectTitle" @blur="updateProjectTitle"
/> />
<span <NcTooltip
v-else v-else
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none" class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen }" :class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen }"
@click="onProjectClick(base)" show-on-truncate-only
> >
{{ base.title }} <template #title>{{ base.title }}</template>
</span> <span @click="onProjectClick(base)">
{{ base.title }}
</span>
</NcTooltip>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(base)"></div> <div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(base)"></div>
<NcDropdown v-if="!isSharedBase" v-model:visible="isOptionsOpen" :trigger="['click']"> <NcDropdown v-if="!isSharedBase" v-model:visible="isOptionsOpen" :trigger="['click']">

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

@ -149,96 +149,94 @@ const isTableOpened = computed(() => {
:class="[`nc-base-tree-tbl nc-base-tree-tbl-${table.title}`]" :class="[`nc-base-tree-tbl nc-base-tree-tbl-${table.title}`]"
:data-active="openedTableId === table.id" :data-active="openedTableId === table.id"
> >
<GeneralTooltip <div
class="nc-tree-item-inner nc-sidebar-node pl-11 pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200" v-e="['a:table:open']"
class="table-context flex items-center gap-1 h-full nc-tree-item-inner nc-sidebar-node pl-11 pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200"
:class="{ :class="{
'hover:bg-gray-200': openedTableId !== table.id, 'hover:bg-gray-200': openedTableId !== table.id,
'pl-12 xs:(pl-14)': sourceIndex !== 0, 'pl-12 xs:(pl-14)': sourceIndex !== 0,
'pl-6.5': sourceIndex === 0, 'pl-6.5': sourceIndex === 0,
'!bg-primary-selected': isTableOpened, '!bg-primary-selected': isTableOpened,
}" }"
modifier-key="Alt" :data-testid="`nc-tbl-side-node-${table.title}`"
@contextmenu="setMenuContext('table', table)"
@click="onOpenTable"
> >
<template #title>{{ table.table_name }}</template> <div class="flex flex-row h-full items-center">
<div <NcButton
v-e="['a:table:open']" v-e="['c:table:toggle-expand']"
class="table-context flex items-center gap-1 h-full" type="text"
:data-testid="`nc-tbl-side-node-${table.title}`" size="xxsmall"
@contextmenu="setMenuContext('table', table)" class="nc-sidebar-node-btn nc-sidebar-expand"
@click="onOpenTable" @click.stop="onExpand"
> >
<div class="flex flex-row h-full items-center"> <GeneralLoader
<NcButton v-if="table.isViewsLoading"
v-e="['c:table:toggle-expand']" class="flex w-4 h-4 !text-gray-600 !mt-0.75"
type="text" :class="{
size="xxsmall" '!visible': !isExpanded,
class="nc-sidebar-node-btn nc-sidebar-expand" }"
@click.stop="onExpand" />
<GeneralIcon
v-else
icon="triangleFill"
class="nc-sidebar-source-node-btns group-hover:visible invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 !text-gray-600 rotate-90"
:class="{ '!rotate-180': isExpanded }"
/>
</NcButton>
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<div
v-e="['c:table:emoji-picker']"
class="flex items-center nc-table-icon"
:class="{
'pointer-events-none': !canUserEditEmote,
}"
@click.stop
> >
<GeneralLoader <LazyGeneralEmojiPicker
v-if="table.isViewsLoading" :key="table.meta?.icon"
class="flex w-4 h-4 !text-gray-600 !mt-0.75" :emoji="table.meta?.icon"
:class="{ size="small"
'!visible': !isExpanded, :readonly="!canUserEditEmote || isMobileMode"
}" @emoji-selected="setIcon($event, table)"
/>
<GeneralIcon
v-else
icon="triangleFill"
class="nc-sidebar-source-node-btns group-hover:visible invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 !text-gray-600 rotate-90"
:class="{ '!rotate-180': isExpanded }"
/>
</NcButton>
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<div
v-e="['c:table:emoji-picker']"
class="flex items-center nc-table-icon"
:class="{
'pointer-events-none': !canUserEditEmote,
}"
@click.stop
> >
<LazyGeneralEmojiPicker <template #default>
:key="table.meta?.icon" <NcTooltip class="flex" placement="topLeft" hide-on-click :disabled="!canUserEditEmote">
:emoji="table.meta?.icon" <template #title>
size="small" {{ $t('general.changeIcon') }}
:readonly="!canUserEditEmote || isMobileMode" </template>
@emoji-selected="setIcon($event, table)"
> <component
<template #default> :is="iconMap.table"
<NcTooltip class="flex" placement="topLeft" hide-on-click :disabled="!canUserEditEmote"> v-if="table.type === 'table'"
<template #title> class="flex w-5 !text-gray-500 text-sm"
{{ $t('general.changeIcon') }} :class="{
</template> 'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id,
<component }"
:is="iconMap.table" />
v-if="table.type === 'table'"
class="flex w-5 !text-gray-500 text-sm" <MdiEye
:class="{ v-else
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }), class="flex w-5 !text-gray-500 text-sm"
'!text-black': openedTableId === table.id, :class="{
}" 'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
/> '!text-black': openedTableId === table.id,
}"
<MdiEye />
v-else </NcTooltip>
class="flex w-5 !text-gray-500 text-sm" </template>
:class="{ </LazyGeneralEmojiPicker>
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id,
}"
/>
</NcTooltip>
</template>
</LazyGeneralEmojiPicker>
</div>
</div> </div>
</div> </div>
</div>
<NcTooltip
class="nc-tbl-title nc-sidebar-node-title text-ellipsis w-full overflow-hidden select-none"
show-on-truncate-only
>
<template #title>{{ table.title }}</template>
<span <span
class="nc-tbl-title nc-sidebar-node-title text-ellipsis overflow-hidden select-none"
:class="{ :class="{
'text-black !font-medium': isTableOpened, 'text-black !font-medium': isTableOpened,
}" }"
@ -247,73 +245,73 @@ const isTableOpened = computed(() => {
> >
{{ table.title }} {{ table.title }}
</span> </span>
<div class="flex flex-grow h-full"></div> </NcTooltip>
<div class="flex flex-row items-center"> <div class="flex flex-grow h-full"></div>
<div <div class="flex flex-row items-center">
v-if=" <div
!isSharedBase && v-if="
(isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole })) !isSharedBase && (isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
" "
v-e="['c:table:option']" v-e="['c:table:option']"
> >
<NcDropdown :trigger="['click']" class="nc-sidebar-node-btn" @click.stop> <NcDropdown :trigger="['click']" class="nc-sidebar-node-btn" @click.stop>
<MdiDotsHorizontal <MdiDotsHorizontal
data-testid="nc-sidebar-table-context-menu" data-testid="nc-sidebar-table-context-menu"
class="min-w-5.75 min-h-5.75 mt-0.2 mr-0.25 px-0.5 !text-gray-600 transition-opacity opacity-0 group-hover:opacity-100 nc-tbl-context-menu outline-0 rounded-md hover:(bg-gray-500 bg-opacity-15 !text-black)" class="min-w-5.75 min-h-5.75 mt-0.2 mr-0.25 px-0.5 !text-gray-600 transition-opacity opacity-0 group-hover:opacity-100 nc-tbl-context-menu outline-0 rounded-md hover:(bg-gray-500 bg-opacity-15 !text-black)"
/> />
<template #overlay> <template #overlay>
<NcMenu> <NcMenu>
<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}`"
@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">
<GeneralIcon icon="edit" class="text-gray-700" /> <GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('general.rename') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem <NcMenuItem
v-if=" v-if="
isUIAllowed('tableDuplicate') && isUIAllowed('tableDuplicate') &&
base.sources?.[sourceIndex] && base.sources?.[sourceIndex] &&
(base.sources[sourceIndex].is_meta || base.sources[sourceIndex].is_local) (base.sources[sourceIndex].is_meta || base.sources[sourceIndex].is_local)
" "
:data-testid="`sidebar-table-duplicate-${table.title}`" :data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)" @click="duplicateTable(table)"
> >
<div v-e="['c:table:duplicate']" class="flex gap-2 items-center"> <div v-e="['c:table:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" /> <GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('general.duplicate') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
<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"
@click="isTableDeleteDialogVisible = true" @click="isTableDeleteDialogVisible = true"
> >
<div v-e="['c:table:delete']" class="flex gap-2 items-center"> <div v-e="['c:table:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" /> <GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('general.delete') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>
</div>
</div> </div>
</div> </div>
<DlgTableDelete </div>
v-if="table.id && base?.id" <DlgTableDelete
v-model:visible="isTableDeleteDialogVisible" v-if="table.id && base?.id"
:table-id="table.id" v-model:visible="isTableDeleteDialogVisible"
:base-id="base.id" :table-id="table.id"
/> :base-id="base.id"
</GeneralTooltip> />
<DashboardTreeViewViewsList v-if="isExpanded" :table-id="table.id" :base-id="base.id" /> <DashboardTreeViewViewsList v-if="isExpanded" :table-id="table.id" :base-id="base.id" />
</div> </div>
</template> </template>

37
packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue

@ -128,6 +128,16 @@ onKeyStroke('Enter', (event) => {
} }
}) })
const onRenameMenuClick = () => {
if (isMobileMode.value || !isUIAllowed('viewCreateOrEdit')) return
if (!isEditing.value) {
isEditing.value = true
_title.value = vModel.value.title
$e('c:view:rename', { view: vModel.value?.type })
}
}
const focusInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus() const focusInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
/** Rename a view */ /** Rename a view */
@ -232,19 +242,18 @@ watch(isDropdownOpen, async () => {
@blur="onRename" @blur="onRename"
@keydown.stop="onKeyDown($event)" @keydown.stop="onKeyDown($event)"
/> />
<NcTooltip v-else class="nc-sidebar-node-title text-ellipsis overflow-hidden select-none w-full" show-on-truncate-only>
<div <template #title> {{ vModel.alias || vModel.title }}</template>
v-else <div
class="nc-sidebar-node-title text-ellipsis overflow-hidden select-none w-full" data-testid="sidebar-view-title"
data-testid="sidebar-view-title" :class="{
:class="{ 'font-medium': activeView?.id === vModel.id,
'font-medium': activeView?.id === vModel.id, }"
}" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" >
> {{ vModel.alias || vModel.title }}
{{ vModel.alias || vModel.title }} </div>
</div> </NcTooltip>
<div class="flex-1" /> <div class="flex-1" />
<template v-if="!isEditing && !isLocked && isUIAllowed('viewCreateOrEdit')"> <template v-if="!isEditing && !isLocked && isUIAllowed('viewCreateOrEdit')">
@ -269,7 +278,7 @@ watch(isDropdownOpen, async () => {
:table="table" :table="table"
in-sidebar in-sidebar
@close-modal="isDropdownOpen = false" @close-modal="isDropdownOpen = false"
@rename="onRename" @rename="onRenameMenuClick"
@delete="onDelete" @delete="onDelete"
/> />
</template> </template>

8
packages/nc-gui/components/dashboard/settings/BaseAudit.vue

@ -135,13 +135,14 @@ const columns = [
v-model:page-size="currentLimit" v-model:page-size="currentLimit"
:total="+totalRows" :total="+totalRows"
show-less-items show-less-items
class="pagination"
@change="loadAudits" @change="loadAudits"
/> />
</div> </div>
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss" scoped>
.nc-audit-table pre { .nc-audit-table pre {
display: table; display: table;
table-layout: fixed; table-layout: fixed;
@ -150,4 +151,9 @@ const columns = [
font-size: unset; font-size: unset;
font-family: unset; font-family: unset;
} }
.pagination {
.ant-select-dropdown {
@apply !border-1 !border-gray-200;
}
}
</style> </style>

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

@ -147,7 +147,10 @@ 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 w-[900px]">
<span class="mb-4 first-letter:capital font-bold"> UI ACL : {{ base.title }} </span> <NcTooltip class="mb-4 first-letter:capital font-bold max-w-100 truncate" show-on-truncate-only>
<template #title>{{ base.title }}</template>
<span> UI ACL : {{ base.title }} </span>
</NcTooltip>
<div class="flex flex-row items-center w-full mb-4 gap-2 justify-between"> <div class="flex flex-row items-center w-full mb-4 gap-2 justify-between">
<a-input v-model:value="searchInput" :placeholder="$t('placeholder.searchModels')" class="nc-acl-search !w-[400px]"> <a-input v-model:value="searchInput" :placeholder="$t('placeholder.searchModels')" class="nc-acl-search !w-[400px]">
<template #prefix> <template #prefix>
@ -208,9 +211,10 @@ const toggleSelectAll = (role: Role) => {
<div class="min-w-5 flex items-center justify-center"> <div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="{ meta: record.table_meta, type: record.ptype }" class="text-gray-500" /> <GeneralTableIcon :meta="{ meta: record.table_meta, type: record.ptype }" class="text-gray-500" />
</div> </div>
<GeneralTruncateText> <NcTooltip class="overflow-ellipsis min-w-0 shrink-1 truncate" show-on-truncate-only>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ record._ptn }}</span> <template #title>{{ record._ptn }}</template>
</GeneralTruncateText> <span>{{ record._ptn }}</span>
</NcTooltip>
</div> </div>
</div> </div>
@ -219,7 +223,10 @@ const toggleSelectAll = (role: Role) => {
<div class="min-w-5 flex items-center justify-center"> <div class="min-w-5 flex items-center justify-center">
<GeneralViewIcon :meta="record" class="text-gray-500"></GeneralViewIcon> <GeneralViewIcon :meta="record" class="text-gray-500"></GeneralViewIcon>
</div> </div>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ record.title }}</span> <NcTooltip class="overflow-ellipsis min-w-0 shrink-1 truncate" show-on-truncate-only>
<template #title>{{ record.title }}</template>
<span>{{ record.title }}</span>
</NcTooltip>
</div> </div>
</div> </div>

2
packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue

@ -148,7 +148,7 @@ const onRoleToggle = async () => {
<div class="flex flex-col py-2 px-3 gap-2 w-full" data-testid="nc-share-base-sub-modal"> <div class="flex flex-col py-2 px-3 gap-2 w-full" data-testid="nc-share-base-sub-modal">
<div class="flex flex-col w-full p-3 border-1 border-gray-100 rounded-md"> <div class="flex flex-col w-full p-3 border-1 border-gray-100 rounded-md">
<div class="flex flex-row w-full justify-between"> <div class="flex flex-row w-full justify-between">
<div class="text-black font-medium">{{ $t('activity.enablePublicAccess') }}</div> <div class="text-gray-900 font-medium">{{ $t('activity.enablePublicAccess') }}</div>
<a-switch <a-switch
v-e="['c:share:base:enable:toggle']" v-e="['c:share:base:enable:toggle']"
:checked="isSharedBaseEnabled" :checked="isSharedBaseEnabled"

2
packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue

@ -277,7 +277,7 @@ const isPublicShared = computed(() => {
<div class="flex flex-col py-2 px-3 mb-1"> <div class="flex flex-col py-2 px-3 mb-1">
<div class="flex flex-col w-full mt-2.5 px-3 py-2.5 border-gray-200 border-1 rounded-md gap-y-2"> <div class="flex flex-col w-full mt-2.5 px-3 py-2.5 border-gray-200 border-1 rounded-md gap-y-2">
<div class="flex flex-row w-full justify-between py-0.5"> <div class="flex flex-row w-full justify-between py-0.5">
<div class="flex" :style="{ fontWeight: 500 }">{{ $t('activity.enabledPublicViewing') }}</div> <div class="text-gray-900 font-medium">{{ $t('activity.enabledPublicViewing') }}</div>
<a-switch <a-switch
v-e="['c:share:view:enable:toggle']" v-e="['c:share:view:enable:toggle']"
data-testid="share-view-toggle" data-testid="share-view-toggle"

2
packages/nc-gui/components/erd/TableNode.vue

@ -54,7 +54,7 @@ watch(
</template> </template>
<div <div
class="relative h-full flex flex-col justify-center bg-white min-w-16 min-h-8 rounded-lg nc-erd-table-node" class="relative h-full max-w-76 flex flex-col justify-center bg-white min-w-16 min-h-8 rounded-lg nc-erd-table-node"
:class="[ :class="[
`nc-erd-table-node-${table.table_name}`, `nc-erd-table-node-${table.table_name}`,
showSkeleton ? 'cursor-pointer items-center min-h-200px min-w-300px' : '', showSkeleton ? 'cursor-pointer items-center min-h-200px min-w-300px' : '',

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

@ -1,23 +1,91 @@
<script lang="ts" setup> <script lang="ts" setup></script>
import { iconMap } from '#imports'
</script>
<template> <template>
<a <div class="flex flex-row items-center w-full bg-white rounded-lg border-1 border-brand-500 shadow-sm mb-0.5 overflow-hidden">
v-e="['c:navbar:join-cloud']" <a
class="flex !no-underline" v-e="['c:navbar:join-cloud']"
href="https://app.nocodb.com/#/signin?utm_source=OSS&utm_medium=OSS&utm_campaign=OSS&utm_content=OSS" class="flex flex-grow !no-underline items-center justify-center border-r-1 h-full hover:bg-gray-100"
> href="https://app.nocodb.com/#/signin?utm_source=OSS&utm_medium=OSS&utm_campaign=OSS&utm_content=OSS"
<div
class="flex justify-center items-center rounded-l-[3px] w-full cursor-pointer px-2 py-1 !text-current !no-underline text-primary border-1 border-[#cdd1d6] bg-[#EFF2F6] hover:bg-[#e9ebef] m-0"
> >
<component :is="iconMap.cloud" class="mt-[1px] text-black font-bold" /> <div class="px-1 text-gray-500 prose-sm" style="line-height: 1.3125rem">Try NocoDB Cloud</div>
<div class="px-1 text-xs font-bold text-gray-800">{{ $t('general.join') }}</div> </a>
</div> <a-tooltip overlay-class-name="nc-join-cloud-tooltip">
<div <template #title>
class="group flex justify-center items-center rounded-r-[3px] w-full cursor-pointer px-1 py-1 text-primary border-r-1 border-b-1 border-t-1 border-[#cdd1d6] m-0" <div class="w-72 bg-transparent overflow-hidden rounded-3xl shadow border border-zinc-100">
> <div class="p-6 bg-white flex-col justify-start items-center gap-4 inline-flex pb-7 w-full">
<div class="px-1 text-xs font-semibold group-hover:text-[#0a69da] text-gray-900">NocoDB Cloud</div> <div class="self-stretch justify-start items-center gap-3 inline-flex">
</div> <div class="text-slate-800 text-lg font-semibold leading-9">NocoDB Cloud</div>
</a> <div class="px-2 py-1 bg-brand-50 rounded-lg justify-center items-center gap-2 flex">
<div class="text-brand-500 text-sm font-medium leading-tight">Usage based</div>
</div>
</div>
<div class="self-stretch justify-start items-center gap-2 inline-flex">
<div class="text-gray-500 text-base font-bold line-through leading-normal">
$ 99
<span class="font-thin text-gray-500"> Onwards </span>
</div>
<div class="text-neutral-900 text-4xl font-bold leading-10">Free</div>
</div>
<div class="self-stretch text-gray-500 text-base leading-normal">/ month / workspace</div>
<a href="https://app.nocodb.com/#/signin" target="_blank" class="!no-underline">
<NcButton class="text-gray-700 text-base font-semibold leading-tight py-4 w-full">Start for Free</NcButton>
</a>
<div class="self-stretch text-center text-gray-500 text-xs font-medium leading-none mb-4">
No credit card required
</div>
<div class="flex flex-col items-start w-full">
<div class="self-stretch text-gray-500 text-base font-semibold leading-tight mb-2">Includes</div>
<div class="self-stretch justify-between items-center inline-flex">
<div class="justify-end items-center gap-3 flex">
<GeneralIcon icon="check" class="text-brand-500" />
<div class="text-brand-500 text-lg font-bold leading-normal">20 Users</div>
</div>
</div>
</div>
<div class="self-stretch justify-between items-center inline-flex">
<div class="justify-start items-center gap-3 flex">
<GeneralIcon icon="check" class="text-brand-500" />
<div class="text-slate-800 text-sm font-semibold leading-tight">300k rows / workspace</div>
</div>
</div>
<div class="self-stretch justify-between items-center inline-flex">
<div class="justify-start items-center gap-3 flex">
<GeneralIcon icon="check" class="text-brand-500" />
<div class="text-slate-800 text-sm font-semibold leading-tight">25 GB+ storage</div>
</div>
</div>
<div class="self-stretch justify-between items-center inline-flex">
<div class="justify-start items-center gap-3 flex">
<GeneralIcon icon="check" class="text-brand-500" />
<div class="text-slate-800 text-sm font-semibold leading-tight">APIs : 10+ requests / second</div>
</div>
</div>
<div class="self-stretch justify-start items-center gap-3 inline-flex">
<GeneralIcon icon="check" class="text-brand-500" />
<div class="text-slate-800 text-sm font-semibold leading-tight">Support for External Database</div>
</div>
</div>
</div>
</template>
<NcButton type="text" size="small" class="!rounded-l-none !rounded-r-lg">
<GeneralIcon icon="help" class="!text-lg -mt-0.5 text-gray-700" />
</NcButton>
</a-tooltip>
</div>
</template> </template>
<style lang="scss">
.nc-join-cloud-tooltip {
.ant-tooltip-inner {
@apply !bg-transparent !p-0 !text-gray-700 rounded-3xl;
}
.ant-tooltip-arrow-content {
@apply !bg-white;
}
}
</style>

2
packages/nc-gui/components/general/language/index.vue

@ -9,7 +9,7 @@
</div> </div>
<template #overlay> <template #overlay>
<a-menu class="nc-scrollbar-dark-md min-w-50 max-h-90vh overflow-auto !p-1 m-1 rounded-md"> <a-menu class="nc-scrollbar-dark-md min-w-50 max-h-90vh overflow-auto !p-1 m-1 rounded-md border-1 border-gray-200">
<GeneralLanguageMenu /> <GeneralLanguageMenu />
</a-menu> </a-menu>
</template> </template>

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

@ -25,7 +25,7 @@ const overlayClassName = toRef(props, 'overlayClassName')
const autoClose = computed(() => props.autoClose) const autoClose = computed(() => props.autoClose)
const overlayClassNameComputed = computed(() => { const overlayClassNameComputed = computed(() => {
let className = 'nc-dropdown bg-white rounded-lg border-1 border-gray-100 shadow-lg' let className = 'nc-dropdown bg-white rounded-lg border-1 border-gray-200 shadow-lg'
if (overlayClassName.value) { if (overlayClassName.value) {
className += ` ${overlayClassName.value}` className += ` ${overlayClassName.value}`
} }

14
packages/nc-gui/components/nc/Modal.vue

@ -6,17 +6,19 @@ const props = withDefaults(
size?: 'small' | 'medium' | 'large' size?: 'small' | 'medium' | 'large'
destroyOnClose?: boolean destroyOnClose?: boolean
maskClosable?: boolean maskClosable?: boolean
wrapClassName?: string
}>(), }>(),
{ {
size: 'medium', size: 'medium',
destroyOnClose: true, destroyOnClose: true,
maskClosable: true, maskClosable: true,
wrapClassName: '',
}, },
) )
const emits = defineEmits(['update:visible']) const emits = defineEmits(['update:visible'])
const { width: propWidth, destroyOnClose, maskClosable } = props const { width: propWidth, destroyOnClose, maskClosable, wrapClassName: _wrapClassName } = props
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
@ -64,6 +66,14 @@ const height = computed(() => {
return 'auto' return 'auto'
}) })
const newWrapClassName = computed(() => {
let className = 'nc-modal-wrapper'
if (_wrapClassName) {
className += ` ${_wrapClassName}`
}
return className
})
const visible = useVModel(props, 'visible', emits) const visible = useVModel(props, 'visible', emits)
const slots = useSlots() const slots = useSlots()
@ -76,7 +86,7 @@ const slots = useSlots()
:width="width" :width="width"
:centered="true" :centered="true"
:closable="false" :closable="false"
wrap-class-name="nc-modal-wrapper" :wrap-class-name="newWrapClassName"
:footer="null" :footer="null"
:mask-closable="maskClosable" :mask-closable="maskClosable"
:destroy-on-close="destroyOnClose" :destroy-on-close="destroyOnClose"

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

@ -15,7 +15,13 @@ const emits = defineEmits(['update:value', 'change'])
const placeholder = computed(() => props.placeholder) const placeholder = computed(() => props.placeholder)
const dropdownClassName = computed(() => props.dropdownClassName) const dropdownClassName = computed(() => {
let className = 'nc-select-dropdown'
if (props.dropdownClassName) {
className += ` ${props.dropdownClassName}`
}
return className
})
const showSearch = computed(() => props.showSearch) const showSearch = computed(() => props.showSearch)
@ -37,7 +43,7 @@ const onChange = (value: string) => {
v-model:value="vModel" v-model:value="vModel"
:placeholder="placeholder" :placeholder="placeholder"
class="nc-select" class="nc-select"
:dropdown-class-name="dropdownClassName ? `nc-select-dropdown ${dropdownClassName}` : 'nc-select-dropdown'" :dropdown-class-name="dropdownClassName"
:show-search="showSearch" :show-search="showSearch"
:filter-option="filterOption" :filter-option="filterOption"
:dropdown-match-select-width="dropdownMatchSelectWidth" :dropdown-match-select-width="dropdownMatchSelectWidth"

11
packages/nc-gui/components/nc/Tooltip.vue

@ -11,6 +11,7 @@ interface Props {
// force disable tooltip // force disable tooltip
disabled?: boolean disabled?: boolean
placement?: TooltipPlacement | undefined placement?: TooltipPlacement | undefined
showOnTruncateOnly?: boolean
hideOnClick?: boolean hideOnClick?: boolean
overlayClassName?: string overlayClassName?: string
} }
@ -20,6 +21,7 @@ const props = defineProps<Props>()
const modifierKey = computed(() => props.modifierKey) const modifierKey = computed(() => props.modifierKey)
const tooltipStyle = computed(() => props.tooltipStyle) const tooltipStyle = computed(() => props.tooltipStyle)
const disabled = computed(() => props.disabled) const disabled = computed(() => props.disabled)
const showOnTruncateOnly = computed(() => props.showOnTruncateOnly)
const hideOnClick = computed(() => props.hideOnClick) const hideOnClick = computed(() => props.hideOnClick)
const placement = computed(() => props.placement ?? 'top') const placement = computed(() => props.placement ?? 'top')
@ -65,6 +67,15 @@ onKeyStroke(
) )
watch([isHovering, () => modifierKey.value, () => disabled.value], ([hovering, key, isDisabled]) => { watch([isHovering, () => modifierKey.value, () => disabled.value], ([hovering, key, isDisabled]) => {
if (showOnTruncateOnly?.value) {
const targetElement = el?.value
const isElementTruncated = targetElement && targetElement.scrollWidth > targetElement.clientWidth
if (!isElementTruncated) {
showTooltip.value = false
return
}
}
if (!hovering || isDisabled) { if (!hovering || isDisabled) {
showTooltip.value = false showTooltip.value = false
return return

2
packages/nc-gui/components/project/AccessSettings.vue

@ -177,7 +177,7 @@ onMounted(async () => {
</div> </div>
<div v-else class="nc-collaborators-list mt-6 h-full"> <div v-else class="nc-collaborators-list mt-6 h-full">
<div class="flex flex-col rounded-lg overflow-hidden border-1 max-w-350 max-h-[calc(100%-8rem)]"> <div class="flex flex-col rounded-lg overflow-hidden border-1 max-w-350 max-h-[calc(100%-8rem)]">
<div class="flex flex-row bg-gray-50 min-h-12 items-center"> <div class="flex flex-row bg-gray-50 min-h-12 items-center border-b-1">
<div class="text-gray-700 users-email-grid">{{ $t('objects.users') }}</div> <div class="text-gray-700 users-email-grid">{{ $t('objects.users') }}</div>
<div class="text-gray-700 date-joined-grid">{{ $t('title.dateJoined') }}</div> <div class="text-gray-700 date-joined-grid">{{ $t('title.dateJoined') }}</div>
<div class="text-gray-700 user-access-grid">{{ $t('general.access') }}</div> <div class="text-gray-700 user-access-grid">{{ $t('general.access') }}</div>

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

@ -1,7 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useTitle } from '@vueuse/core' import { useTitle } from '@vueuse/core'
import NcLayout from '~icons/nc-icons/layout' import NcLayout from '~icons/nc-icons/layout'
const { openedProject } = storeToRefs(useBases()) import { isEeUI } from '#imports'
const basesStore = useBases()
const { getProjectUsers } = basesStore
const { openedProject, activeProjectId, baseUserCount } = storeToRefs(basesStore)
const { activeTables } = storeToRefs(useTablesStore()) const { activeTables } = storeToRefs(useTablesStore())
const { activeWorkspace, workspaceUserCount } = storeToRefs(useWorkspace()) const { activeWorkspace, workspaceUserCount } = storeToRefs(useWorkspace())
@ -26,6 +32,23 @@ const { isMobileMode } = useGlobal()
const baseSettingsState = ref('') const baseSettingsState = ref('')
const userCount = isEeUI ? workspaceUserCount : baseUserCount
const updateBaseUserCount = async () => {
try {
const { totalRows } = await getProjectUsers({
baseId: activeProjectId.value!,
page: 1,
searchText: undefined,
limit: 20,
})
baseUserCount.value = totalRows
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
watch( watch(
() => route.value.query?.page, () => route.value.query?.page,
(newVal, oldVal) => { (newVal, oldVal) => {
@ -57,6 +80,16 @@ watch(projectPageTab, () => {
} }
}) })
watch(
() => route.value.params.baseId,
(newVal, oldVal) => {
if (newVal && oldVal === undefined) {
updateBaseUserCount()
}
},
{ immediate: true },
)
watch( watch(
() => openedProject.value?.title, () => openedProject.value?.title,
() => { () => {
@ -68,16 +101,19 @@ watch(
<template> <template>
<div class="h-full nc-base-view"> <div class="h-full nc-base-view">
<div <div
class="flex flex-row pl-2 pr-2 border-b-1 border-gray-200 justify-between w-full" class="flex flex-row pl-2 pr-2 gap-1 border-b-1 border-gray-200 justify-between w-full"
:class="{ 'nc-table-toolbar-mobile': isMobileMode, 'h-[var(--topbar-height)]': !isMobileMode }" :class="{ 'nc-table-toolbar-mobile': isMobileMode, 'h-[var(--topbar-height)]': !isMobileMode }"
> >
<div class="flex flex-row items-center gap-x-3"> <div class="flex flex-row items-center gap-x-3">
<GeneralOpenLeftSidebarBtn /> <GeneralOpenLeftSidebarBtn />
<div class="flex flex-row items-center h-full gap-x-2.5"> <div class="flex flex-row items-center h-full gap-x-2.5">
<GeneralProjectIcon :type="openedProject?.type" /> <GeneralProjectIcon :type="openedProject?.type" />
<div class="flex font-medium text-sm capitalize"> <NcTooltip class="flex font-medium text-sm capitalize truncate max-w-150" show-on-truncate-only>
{{ openedProject?.title }} <template #title> {{ openedProject?.title }}</template>
</div> <span class="truncate">
{{ openedProject?.title }}
</span>
</NcTooltip>
</div> </div>
</div> </div>
<LazyGeneralShareProject /> <LazyGeneralShareProject />
@ -116,14 +152,14 @@ watch(
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" /> <GeneralIcon icon="users" class="!h-3.5 !w-3.5" />
<div>{{ $t('labels.members') }}</div> <div>{{ $t('labels.members') }}</div>
<div <div
v-if="workspaceUserCount" v-if="userCount"
class="tab-info" class="tab-info"
:class="{ :class="{
'bg-primary-selected': projectPageTab === 'data-source', 'bg-primary-selected': projectPageTab === 'collaborator',
'bg-gray-50': projectPageTab !== 'data-source', 'bg-gray-50': projectPageTab !== 'collaborator',
}" }"
> >
{{ workspaceUserCount }} {{ userCount }}
</div> </div>
</div> </div>
</template> </template>

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

@ -8,7 +8,6 @@ import {
EditModeInj, EditModeInj,
IsExpandedFormOpenInj, IsExpandedFormOpenInj,
IsFormInj, IsFormInj,
IsLockedInj,
IsPublicInj, IsPublicInj,
IsSurveyFormInj, IsSurveyFormInj,
NavigateDir, NavigateDir,

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

@ -1,5 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { IsPublicInj, inject, ref, useRoles, useSharedView, useSmartsheetStoreOrThrow, useViewsStore } from '#imports' import {
IsPublicInj,
inject,
ref,
storeToRefs,
useGlobal,
useSharedView,
useSmartsheetStoreOrThrow,
useViewsStore,
} from '#imports'
const { isGrid, isGallery, isKanban, isMap } = useSmartsheetStoreOrThrow() const { isGrid, isGallery, isKanban, isMap } = useSmartsheetStoreOrThrow()
@ -9,8 +18,6 @@ const { isViewsLoading } = storeToRefs(useViewsStore())
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const { isUIAllowed } = useRoles()
const { allowCSVDownload } = useSharedView() const { allowCSVDownload } = useSharedView()
</script> </script>

2
packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue

@ -73,7 +73,7 @@ vModel.value.au = !!vModel.value.au */
</div> </div>
<a-form-item :label="$t('labels.databaseType')" v-bind="validateInfos.dt"> <a-form-item :label="$t('labels.databaseType')" v-bind="validateInfos.dt">
<a-select v-model:value="vModel.dt" dropdown-class-name="nc-dropdown-db-type" @change="onDataTypeChange"> <a-select v-model:value="vModel.dt" dropdown-class-name="nc-dropdown-db-type " @change="onDataTypeChange">
<a-select-option v-for="type in dataTypes" :key="type" :value="type"> <a-select-option v-for="type in dataTypes" :key="type" :value="type">
{{ type }} {{ type }}
</a-select-option> </a-select-option>

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

@ -223,7 +223,7 @@ if (props.fromTableExplorer) {
'!w-146': isTextArea(formState) && formState.meta.richMode, '!w-146': isTextArea(formState) && formState.meta.richMode,
'!w-[600px]': formState.uidt === UITypes.Formula && !props.embedMode, '!w-[600px]': formState.uidt === UITypes.Formula && !props.embedMode,
'!w-[500px]': formState.uidt === UITypes.Attachment && !props.embedMode && !appInfo.ee, '!w-[500px]': formState.uidt === UITypes.Attachment && !props.embedMode && !appInfo.ee,
'shadow-lg border-1 border-gray-100 shadow-gray-300 rounded-xl p-6': !embedMode, 'shadow-lg border-1 border-gray-200 shadow-gray-300 rounded-xl p-6': !embedMode,
}" }"
@keydown="handleEscape" @keydown="handleEscape"
@click.stop @click.stop
@ -271,7 +271,7 @@ if (props.fromTableExplorer) {
show-search show-search
class="nc-column-type-input !rounded" class="nc-column-type-input !rounded"
:disabled="isKanban || readOnly" :disabled="isKanban || readOnly"
dropdown-class-name="nc-dropdown-column-type " dropdown-class-name="nc-dropdown-column-type border-1 border-gray-200"
@change="onUidtOrIdTypeChange" @change="onUidtOrIdTypeChange"
@dblclick="showDeprecated = !showDeprecated" @dblclick="showDeprecated = !showDeprecated"
> >
@ -312,6 +312,7 @@ if (props.fromTableExplorer) {
v-model:value="formState" v-model:value="formState"
/> />
<LazySmartsheetColumnLinkOptions v-if="isEdit && formState.uidt === UITypes.Links" v-model:value="formState" /> <LazySmartsheetColumnLinkOptions v-if="isEdit && formState.uidt === UITypes.Links" v-model:value="formState" />
<LazySmartsheetColumnPercentOptions v-if="formState.uidt === UITypes.Percent" v-model:value="formState" />
<LazySmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" /> <LazySmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<SmartsheetColumnSelectOptions <SmartsheetColumnSelectOptions
v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect" v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect"

8
packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue

@ -78,12 +78,14 @@ const isLinks = computed(() => vModel.value.uidt === UITypes.Links)
@change="onDataTypeChange" @change="onDataTypeChange"
> >
<a-select-option v-for="table of refTables" :key="table.title" :value="table.id"> <a-select-option v-for="table of refTables" :key="table.title" :value="table.id">
<div class="flex items-center gap-2"> <div class="flex w-full items-center gap-2">
<div class="min-w-5 flex items-center justify-center"> <div class="min-w-5 flex items-center justify-center">
<GeneralTableIcon :meta="table" class="text-gray-500" /> <GeneralTableIcon :meta="table" class="text-gray-500" />
</div> </div>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ table.title }}</span> <template #title>{{ table.title }}</template>
<span>{{ table.title }}</span>
</NcTooltip>
</div> </div>
</a-select-option> </a-select-option>
</a-select> </a-select>

51
packages/nc-gui/components/smartsheet/column/PercentOptions.vue

@ -1,47 +1,36 @@
<!-- File not in use for now -->
<script setup lang="ts"> <script setup lang="ts">
import { precisions, useVModel } from '#imports' import { useVModel } from '#imports'
const props = defineProps<{ const props = defineProps<{
value: any value: any
isEdit: boolean
}>() }>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
if (!vModel.value.meta) vModel.value.meta = {} const validators = {}
if (!vModel.value.meta?.negative) vModel.value.meta.negative = false
if (!vModel.value.meta?.default) vModel.value.meta.default = null const { setAdditionalValidations } = useColumnCreateStoreOrThrow()
if (!vModel.value.meta?.precision) vModel.value.meta.precision = precisions[0].id
setAdditionalValidations({
...validators,
})
// set default value
vModel.value.meta = {
is_progress: false,
...vModel.value.meta,
}
</script> </script>
<template> <template>
<div class="flex flex-col mt-2 gap-2"> <div class="flex flex-col">
<div class="flex flex-row space-x-2"> <div>
<a-form-item class="flex w-1/2" :label="$t('placeholder.precision')"> <a-checkbox v-if="vModel.meta" v-model:checked="vModel.meta.is_progress" class="ml-1 mb-1">
<a-select v-model:value="vModel.meta.precision" dropdown-class-name="nc-dropdown-precision"> <span class="text-[10px] text-gray-600">Display as progress</span>
<a-select-option v-for="(precision, i) of precisions" :key="i" :value="precision.id"> </a-checkbox>
<div class="flex flex-row items-center">
<div class="text-xs">
{{ precision.title }}
</div>
</div>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('labels.defaultNumberPercent')">
<a-input v-model:value="vModel.meta.default" :name="$t('labels.default')" type="number" />
</a-form-item>
</div>
<div class="flex flex-row mt-2">
<a-form-item>
<div class="flex flex-row space-x-2 items-center">
<a-switch v-model:checked="vModel.meta.negative" :name="$t('labels.negative')" />
<div class="text-xs">{{ $t('placeholder.allowNegativeNumbers') }}</div>
</div>
</a-form-item>
</div> </div>
</div> </div>
</template> </template>

47
packages/nc-gui/components/smartsheet/column/PercentOptionsLegacy.vue

@ -0,0 +1,47 @@
<!-- File not in use for now -->
<script setup lang="ts">
import { precisions, useVModel } from '#imports'
const props = defineProps<{
value: any
}>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
if (!vModel.value.meta) vModel.value.meta = {}
if (!vModel.value.meta?.negative) vModel.value.meta.negative = false
if (!vModel.value.meta?.default) vModel.value.meta.default = null
if (!vModel.value.meta?.precision) vModel.value.meta.precision = precisions[0].id
</script>
<template>
<div class="flex flex-col mt-2 gap-2">
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" :label="$t('placeholder.precision')">
<a-select v-model:value="vModel.meta.precision" dropdown-class-name="nc-dropdown-precision">
<a-select-option v-for="(precision, i) of precisions" :key="i" :value="precision.id">
<div class="flex flex-row items-center">
<div class="text-xs">
{{ precision.title }}
</div>
</div>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('labels.defaultNumberPercent')">
<a-input v-model:value="vModel.meta.default" :name="$t('labels.default')" type="number" />
</a-form-item>
</div>
<div class="flex flex-row mt-2">
<a-form-item>
<div class="flex flex-row space-x-2 items-center">
<a-switch v-model:checked="vModel.meta.negative" :name="$t('labels.negative')" />
<div class="text-xs">{{ $t('placeholder.allowNegativeNumbers') }}</div>
</div>
</a-form-item>
</div>
</div>
</template>

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

@ -339,7 +339,7 @@ const loadListData = async ($state: any) => {
<a-dropdown <a-dropdown
v-model:visible="colorMenus[index]" v-model:visible="colorMenus[index]"
:trigger="['click']" :trigger="['click']"
overlay-class-name="nc-dropdown-select-color-options" overlay-class-name="nc-dropdown-select-color-options rounded-md overflow-hidden border-1 border-gray-200 "
> >
<template #overlay> <template #overlay>
<LazyGeneralColorPicker <LazyGeneralColorPicker

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

@ -769,8 +769,8 @@ const onFieldOptionUpdate = () => {
</NcTooltip> </NcTooltip>
</div> </div>
</div> </div>
<div class="flex flex-row rounded-lg border-1 border-gray-200"> <div class="flex flex-row rounded-lg border-1 overflow-clip border-gray-200">
<div ref="fieldsListWrapperDomRef" class="nc-scrollbar-md !overflow-auto w-full flex-grow-1 nc-fields-height"> <div ref="fieldsListWrapperDomRef" class="nc-scrollbar-md !overflow-auto flex-1 flex-grow-1 nc-fields-height">
<Draggable v-model="fields" :disabled="isLocked" item-key="id" @change="onMove($event)"> <Draggable v-model="fields" :disabled="isLocked" item-key="id" @change="onMove($event)">
<template #item="{ element: field }"> <template #item="{ element: field }">
<div <div
@ -814,14 +814,18 @@ const onFieldOptionUpdate = () => {
'text-brand-500': compareCols(field, activeField), 'text-brand-500': compareCols(field, activeField),
}" }"
/> />
<span <NcTooltip
:class="{ :class="{
'text-brand-500': compareCols(field, activeField), 'text-brand-500': compareCols(field, activeField),
}" }"
class="truncate max-w-64" class="truncate flex-1"
show-on-truncate-only
> >
{{ fieldState(field)?.title || field.title }} <template #title> {{ fieldState(field)?.title || field.title }} </template>
</span> <span>
{{ fieldState(field)?.title || field.title }}
</span>
</NcTooltip>
</div> </div>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<div class="flex items-center"> <div class="flex items-center">
@ -970,13 +974,18 @@ const onFieldOptionUpdate = () => {
'text-brand-500': compareCols(displayColumn, activeField), 'text-brand-500': compareCols(displayColumn, activeField),
}" }"
/> />
<span <NcTooltip
class="truncate flex-1"
:class="{ :class="{
'text-brand-500': compareCols(displayColumn, activeField), 'text-brand-500': compareCols(displayColumn, activeField),
}" }"
show-on-truncate-only
> >
{{ fieldState(displayColumn)?.title || displayColumn.title }} <template #title> {{ fieldState(displayColumn)?.title || displayColumn.title }} </template>
</span> <span>
{{ fieldState(displayColumn)?.title || displayColumn.title }}
</span>
</NcTooltip>
</div> </div>
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<div class="flex items-center"> <div class="flex items-center">
@ -1072,7 +1081,7 @@ const onFieldOptionUpdate = () => {
</Draggable> </Draggable>
</div> </div>
<Transition v-if="!changingField" name="slide-fade"> <Transition v-if="!changingField" name="slide-fade">
<div class="border-gray-200 border-l-1 rounded-r-xl h-[calc(100vh-(var(--topbar-height)*3.85))]"> <div class="border-gray-200 border-l-1 nc-scrollbar-md nc-fields-height !overflow-y-auto">
<SmartsheetColumnEditOrAddProvider <SmartsheetColumnEditOrAddProvider
v-if="activeField" v-if="activeField"
class="p-4 w-[25rem]" class="p-4 w-[25rem]"

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

@ -645,11 +645,11 @@ export default {
<template v-if="isLoading"> <template v-if="isLoading">
<div <div
v-if="isMobileMode" v-if="isMobileMode"
class="!h-8.5 !xs:h-12 !xs:bg-white !sm:mr-21 !w-60 mt-0.75 !rounded-lg !overflow-hidden" class="!h-8.5 !xs:h-12 !xs:bg-white sm:mr-21 w-60 mt-0.75 !rounded-lg !overflow-hidden"
></div> ></div>
<a-skeleton-input <a-skeleton-input
v-else v-else
class="!h-8.5 !xs:h-9.5 !xs:bg-white !sm:mr-21 !w-60 mt-0.75 !rounded-lg !overflow-hidden" class="!h-8.5 !xs:h-9.5 !xs:bg-white sm:mr-21 !w-60 mt-0.75 !rounded-lg !overflow-hidden"
active active
size="small" size="small"
/> />
@ -658,7 +658,7 @@ export default {
<SmartsheetDivDataCell <SmartsheetDivDataCell
v-if="col.title" v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)" :ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="bg-white rounded-lg !w-[20rem] !xs:w-full border-1 border-gray-200 overflow-hidden px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative" class="bg-white rounded-lg w-80 xs:w-full border-1 border-gray-200 overflow-hidden px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
:class="{ :class="{
'!bg-gray-50 !px-0 !select-text': isReadOnlyVirtualCell(col), '!bg-gray-50 !px-0 !select-text': isReadOnlyVirtualCell(col),
}" }"
@ -720,11 +720,11 @@ export default {
<template v-if="isLoading"> <template v-if="isLoading">
<div <div
v-if="isMobileMode" v-if="isMobileMode"
class="!h-8.5 !xs:h-9.5 !xs:bg-white !sm:mr-21 !w-60 mt-0.75 !rounded-lg !overflow-hidden" class="!h-8.5 !xs:h-9.5 !xs:bg-white sm:mr-21 w-60 mt-0.75 !rounded-lg !overflow-hidden"
></div> ></div>
<a-skeleton-input <a-skeleton-input
v-else v-else
class="!h-8.5 !xs:h-12 !xs:bg-white !sm:mr-21 !w-60 mt-0.75 !rounded-lg !overflow-hidden" class="!h-8.5 !xs:h-12 !xs:bg-white sm:mr-21 w-60 mt-0.75 !rounded-lg !overflow-hidden"
active active
size="small" size="small"
/> />
@ -733,7 +733,7 @@ export default {
<LazySmartsheetDivDataCell <LazySmartsheetDivDataCell
v-if="col.title" v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)" :ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="!bg-white rounded-lg !w-[20rem] border-1 overflow-hidden border-gray-200 px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative" class="bg-white rounded-lg w-80 border-1 overflow-hidden border-gray-200 px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
> >
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="isVirtualCol(col)" v-if="isVirtualCol(col)"

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

@ -1804,7 +1804,7 @@ onKeyStroke('ArrowDown', onDown)
<template #overlay> <template #overlay>
<div class="relative overflow-visible min-h-17 w-10"> <div class="relative overflow-visible min-h-17 w-10">
<div <div
class="absolute -top-19 flex flex-col h-34.5 w-70 bg-white rounded-lg justify-start overflow-hidden" class="absolute -top-19 flex flex-col h-34.5 w-70 bg-white rounded-lg border-1 border-gray-200 justify-start overflow-hidden"
style="box-shadow: 0px 4px 6px -2px rgba(0, 0, 0, 0.06), 0px -12px 16px -4px rgba(0, 0, 0, 0.1)" style="box-shadow: 0px 4px 6px -2px rgba(0, 0, 0, 0.06), 0px -12px 16px -4px rgba(0, 0, 0, 0.1)"
:class="{ :class="{
'-left-44': !isAddNewRecordGridMode, '-left-44': !isAddNewRecordGridMode,
@ -1847,7 +1847,7 @@ onKeyStroke('ArrowDown', onDown)
</div> </div>
</template> </template>
<template #icon> <template #icon>
<component :is="iconMap.arrowUp" class="text-gray-600 h-4" /> <component :is="iconMap.arrowUp" class="text-gray-600 h-4 w-4" />
</template> </template>
</a-dropdown-button> </a-dropdown-button>
</div> </div>

17
packages/nc-gui/components/smartsheet/header/Cell.vue

@ -90,18 +90,23 @@ const onClick = (e: Event) => {
'self-start': isForm || isSurveyForm, 'self-start': isForm || isSurveyForm,
}" }"
/> />
<div <NcTooltip
v-if="column" v-if="column"
class="name pl-1"
:class="{ :class="{
'cursor-pointer pt-0.25': !isForm && isUIAllowed('fieldEdit') && !hideMenu && !isExpandedForm, 'cursor-pointer pt-0.25': !isForm && isUIAllowed('fieldEdit') && !hideMenu && !isExpandedForm,
'cursor-default': isForm || !isUIAllowed('fieldEdit') || hideMenu, 'cursor-default': isForm || !isUIAllowed('fieldEdit') || hideMenu,
'!truncate': !isForm, 'truncate': !isForm,
}" }"
:data-test-id="column.title" class="name pl-1"
placement="bottom"
show-on-truncate-only
> >
{{ column.title }} <template #title> {{ column.title }} </template>
</div>
<span :data-test-id="column.title">
{{ column.title }}
</span>
</NcTooltip>
<span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span> <span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span>

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

@ -291,7 +291,7 @@ const onInsertAfter = () => {
<GeneralIcon icon="arrowDown" class="text-grey h-full text-grey nc-ui-dt-dropdown cursor-pointer outline-0 mr-2" /> <GeneralIcon icon="arrowDown" class="text-grey h-full text-grey nc-ui-dt-dropdown cursor-pointer outline-0 mr-2" />
</div> </div>
<template #overlay> <template #overlay>
<a-menu class="shadow bg-white nc-column-options"> <a-menu class="shadow bg-white border-1 border-gray-200 nc-column-options">
<a-menu-item @click="onEditPress"> <a-menu-item @click="onEditPress">
<div class="nc-column-edit nc-header-menu-item"> <div class="nc-column-edit nc-header-menu-item">
<component :is="iconMap.edit" class="text-gray-700 mx-0.65 my-0.75" /> <component :is="iconMap.edit" class="text-gray-700 mx-0.65 my-0.75" />

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

@ -92,6 +92,7 @@ const tooltipMsg = computed(() => {
if (!column.value) { if (!column.value) {
return '' return ''
} }
if (isHm(column.value)) { if (isHm(column.value)) {
return `'${tableTile.value}' ${t('labels.hasMany')} '${relatedTableTitle.value}'` return `'${tableTile.value}' ${t('labels.hasMany')} '${relatedTableTitle.value}'`
} else if (isMm(column.value)) { } else if (isMm(column.value)) {
@ -110,7 +111,7 @@ const tooltipMsg = computed(() => {
} else if (isRollup(column.value)) { } else if (isRollup(column.value)) {
return `'${childColumn.value.title}' of '${relatedTableTitle.value}' (${childColumn.value.uidt})` return `'${childColumn.value.title}' of '${relatedTableTitle.value}' (${childColumn.value.uidt})`
} }
return '' return column?.value?.title || ''
}) })
const columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null) const columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null)
@ -153,14 +154,14 @@ const openDropDown = (e: Event) => {
> >
<LazySmartsheetHeaderVirtualCellIcon v-if="column && !props.hideIcon" /> <LazySmartsheetHeaderVirtualCellIcon v-if="column && !props.hideIcon" />
<a-tooltip placement="bottom"> <NcTooltip placement="bottom" class="truncate name pl-1" show-on-truncate-only>
<template v-if="!isForm && !isExpandedForm" #title> <template #title>
{{ tooltipMsg }} {{ tooltipMsg }}
</template> </template>
<span class="name truncate pl-1" :class="{ truncate: !isForm }" :data-test-id="column.title"> <span :data-test-id="column.title">
{{ column.title }} {{ column.title }}
</span> </span>
</a-tooltip> </NcTooltip>
<span v-if="isVirtualColRequired(column, meta?.columns || []) || required" class="text-red-500">&nbsp;*</span> <span v-if="isVirtualColRequired(column, meta?.columns || []) || required" class="text-red-500">&nbsp;*</span>

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

@ -91,7 +91,7 @@ const onArrowUp = () => {
<div class="flex pb-3 px-4 border-b-1 border-gray-100"> <div class="flex pb-3 px-4 border-b-1 border-gray-100">
<input ref="inputRef" v-model="search" class="w-full focus:outline-none" :placeholder="$t('msg.selectFieldToSort')" /> <input ref="inputRef" v-model="search" class="w-full focus:outline-none" :placeholder="$t('msg.selectFieldToSort')" />
</div> </div>
<div class="flex-col w-full max-h-100 nc-scrollbar-md !overflow-y-auto"> <div class="flex-col w-full max-h-100 max-w-76 nc-scrollbar-md !overflow-y-auto">
<div v-if="!options.length" class="flex text-gray-500 px-4 py-2.25">{{ $t('general.empty') }}</div> <div v-if="!options.length" class="flex text-gray-500 px-4 py-2.25">{{ $t('general.empty') }}</div>
<div <div
v-for="(option, index) in options" v-for="(option, index) in options"
@ -104,9 +104,12 @@ const onArrowUp = () => {
@click="onClick(option)" @click="onClick(option)"
> >
<SmartsheetHeaderIcon :column="option" /> <SmartsheetHeaderIcon :column="option" />
<div> <NcTooltip class="truncate" show-on-truncate-only>
{{ option.title }} <template #title> {{ option.title }}</template>
</div> <template #default>
{{ option.title }}
</template>
</NcTooltip>
</div> </div>
</div> </div>
</div> </div>

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

@ -82,13 +82,16 @@ 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 gap-2 items-center items-center h-full"> <div class="flex gap-2 items-center items-center h-full">
<component :is="option.icon" class="min-w-5 !mx-0" /> <component :is="option.icon" class="min-w-5 !mx-0" />
<NcTooltip
<div
class="min-w-0 text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
class="max-w-[15rem] truncate select-none"
show-on-truncate-only
> >
{{ option.label }} <template #title> {{ option.label }}</template>
</div> <template #default>
{{ option.label }}
</template>
</NcTooltip>
</div> </div>
</a-select-option> </a-select-option>
</NcSelect> </NcSelect>

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

@ -371,7 +371,7 @@ useMenuCloseOnEsc(open)
<component :is="iconMap.drag" class="cursor-move !h-3.75 text-gray-600 mr-1" /> <component :is="iconMap.drag" class="cursor-move !h-3.75 text-gray-600 mr-1" />
<div <div
v-e="['a:fields:show-hide']" v-e="['a:fields:show-hide']"
class="flex flex-row items-center justify-between w-full cursor-pointer ml-1" class="flex flex-row items-center w-full truncate cursor-pointer ml-1"
@click=" @click="
() => { () => {
field.show = !field.show field.show = !field.show
@ -379,15 +379,13 @@ useMenuCloseOnEsc(open)
} }
" "
> >
<div class="flex items-center -ml-0.75"> <component :is="getIcon(metaColumnById[field.fk_column_id])" />
<component :is="getIcon(metaColumnById[field.fk_column_id])" /> <NcTooltip show-on-truncate-only class="flex-1 px-1 truncate">
<NcTooltip :disabled="field.title.length < 30"> <template #title>
<template #title> {{ field.title }}
{{ field.title }} </template>
</template> <template #default>{{ field.title }}</template>
<span class="mx-0.65 break-all line-clamp-1">{{ field.title }}</span> </NcTooltip>
</NcTooltip>
</div>
<NcSwitch v-e="['a:fields:show-hide']" :checked="field.show" :disabled="field.isViewEssentialField" /> <NcSwitch v-e="['a:fields:show-hide']" :checked="field.show" :disabled="field.isViewEssentialField" />
</div> </div>
@ -399,7 +397,7 @@ useMenuCloseOnEsc(open)
<div <div
v-if="gridDisplayValueField && filteredFieldList[0].title.toLowerCase().includes(filterQuery.toLowerCase())" v-if="gridDisplayValueField && filteredFieldList[0].title.toLowerCase().includes(filterQuery.toLowerCase())"
:key="`pv-${gridDisplayValueField.id}`" :key="`pv-${gridDisplayValueField.id}`"
class="pl-7.5 pr-2.1 py-2 flex flex-row items-center border-1 border-gray-200" class="pl-7.4 pr-2 py-2 flex flex-row items-center border-1 border-gray-200"
:class="{ :class="{
'rounded-t-lg': filteredFieldList.length > 1, 'rounded-t-lg': filteredFieldList.length > 1,
'rounded-lg': filteredFieldList.length === 1, 'rounded-lg': filteredFieldList.length === 1,
@ -407,22 +405,13 @@ useMenuCloseOnEsc(open)
:data-testid="`nc-fields-menu-${gridDisplayValueField.title}`" :data-testid="`nc-fields-menu-${gridDisplayValueField.title}`"
@click.stop @click.stop
> >
<div class="flex flex-row items-center justify-between w-full"> <component :is="getIcon(metaColumnById[filteredFieldList[0].fk_column_id as string])" />
<div class="flex items"> <NcTooltip show-on-truncate-only class="px-1 flex-1 truncate">
<a-tooltip placement="bottom"> <template #title>{{ filteredFieldList[0].title }}</template>
<template #title> <template #default>{{ filteredFieldList[0].title }}</template>
<span class="text-sm">$t('title.displayValue') </span> </NcTooltip>
</template>
</a-tooltip> <NcSwitch v-e="['a:fields:show-hide']" :checked="true" :disabled="true" />
<div class="flex items-center">
<component :is="getIcon(metaColumnById[filteredFieldList[0].fk_column_id as string])" />
<span>{{ filteredFieldList[0].title }}</span>
</div>
</div>
<NcSwitch v-e="['a:fields:show-hide']" :checked="true" :disabled="true" />
</div>
</div> </div>
</template> </template>
</Draggable> </Draggable>

20
packages/nc-gui/components/smartsheet/toolbar/OpenedViewAction.vue

@ -173,7 +173,7 @@ function openDeleteDialog() {
> >
<div <div
v-e="['c:breadcrumb:view-actions']" v-e="['c:breadcrumb:view-actions']"
class="truncate nc-active-view-title !hover:(bg-gray-100 text-gray-800) ml-0.25 pl-1 pr-0.25 rounded-md py-1 cursor-pointer" class="truncate nc-active-view-title flex gap-0.5 items-center !hover:(bg-gray-100 text-gray-800) ml-1 pl-1 pr-0.25 rounded-md py-1 cursor-pointer"
:class="{ :class="{
'max-w-2/5': !isSharedBase && !isMobileMode && activeView?.is_default, 'max-w-2/5': !isSharedBase && !isMobileMode && activeView?.is_default,
'max-w-3/5': !isSharedBase && !isMobileMode && !activeView?.is_default, 'max-w-3/5': !isSharedBase && !isMobileMode && !activeView?.is_default,
@ -182,14 +182,16 @@ function openDeleteDialog() {
'text-gray-800 font-medium': !activeView?.is_default, 'text-gray-800 font-medium': !activeView?.is_default,
}" }"
> >
<span <NcTooltip class="truncate xs:pl-1.25 flex-1 text-inherit" show-on-truncate-only>
class="truncate xs:pl-1.25 text-inherit" <template #title>{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }} </template>
:class="{ <span
'max-w-28/100': !isMobileMode, :class="{
}" 'max-w-28/100': !isMobileMode,
> }"
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }} >
</span> {{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</span>
</NcTooltip>
<GeneralIcon icon="arrowDown" class="ml-1" /> <GeneralIcon icon="arrowDown" class="ml-1" />
</div> </div>
<template #overlay> <template #overlay>

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

@ -65,7 +65,7 @@ useMenuCloseOnEsc(open)
</div> </div>
<template #overlay> <template #overlay>
<div <div
class="w-full bg-white shadow-lg menu-filter-dropdown border-1 border-gray-50 rounded-md overflow-hidden" class="w-full bg-white shadow-lg menu-filter-dropdown border-1 border-gray-200 rounded-md overflow-hidden"
data-testid="nc-height-menu" data-testid="nc-height-menu"
> >
<div class="flex flex-col w-full text-sm" @click.stop> <div class="flex flex-col w-full text-sm" @click.stop>

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

@ -114,7 +114,10 @@ watch(columns, () => {
<a-select-option v-for="op of columns" :key="op.value" v-e="['c:search:field:select']" :value="op.value"> <a-select-option v-for="op of columns" :key="op.value" v-e="['c:search:field:select']" :value="op.value">
<div class="text-[0.75rem] flex items-center -ml-1 gap-2"> <div class="text-[0.75rem] flex items-center -ml-1 gap-2">
<SmartsheetHeaderIcon class="text-sm" :column="op.column" /> <SmartsheetHeaderIcon class="text-sm" :column="op.column" />
{{ op.label }} <NcTooltip class="truncate" placement="top" show-on-truncate-only>
<template #title>{{ op.label }}</template>
<template #default>{{ op.label }}</template>
</NcTooltip>
</div> </div>
</a-select-option> </a-select-option>
</a-select> </a-select>

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

@ -138,7 +138,7 @@ onMounted(() => {
<template v-for="(sort, i) of sorts" :key="i"> <template v-for="(sort, i) of sorts" :key="i">
<SmartsheetToolbarFieldListAutoCompleteDropdown <SmartsheetToolbarFieldListAutoCompleteDropdown
v-model="sort.fk_column_id" v-model="sort.fk_column_id"
class="flex caption nc-sort-field-select min-w-40 flex-grow" class="flex caption nc-sort-field-select w-44 flex-grow"
:columns="columns" :columns="columns"
is-sort is-sort
@click.stop @click.stop

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

@ -131,10 +131,7 @@ const validators = computed(() =>
hasSelectColumn.value[tableIdx] = false hasSelectColumn.value[tableIdx] = false
table.columns?.forEach((column, columnIdx) => { table.columns?.forEach((column, columnIdx) => {
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [ acc[`tables.${tableIdx}.columns.${columnIdx}.title`] = [fieldRequiredValidator(), fieldLengthValidator()]
fieldRequiredValidator(),
fieldLengthValidator(base.value?.sources?.[0].type || ClientType.MYSQL),
]
acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()] acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()]
if (isSelect(column)) { if (isSelect(column)) {
hasSelectColumn.value[tableIdx] = true hasSelectColumn.value[tableIdx] = true
@ -252,6 +249,7 @@ function deleteTableColumn(tableIdx: number, columnKey: number) {
function addNewColumnRow(tableIdx: number, uidt: string) { function addNewColumnRow(tableIdx: number, uidt: string) {
data.tables[tableIdx].columns.push({ data.tables[tableIdx].columns.push({
key: data.tables[tableIdx].columns.length, key: data.tables[tableIdx].columns.length,
title: `title${data.tables[tableIdx].columns.length + 1}`,
column_name: `title${data.tables[tableIdx].columns.length + 1}`, column_name: `title${data.tables[tableIdx].columns.length + 1}`,
uidt, uidt,
}) })
@ -275,7 +273,7 @@ function remapColNames(batchData: any[], columns: ColumnType[]) {
// then only col.column_name exists in data, else col.ref_column_name // then only col.column_name exists in data, else col.ref_column_name
// for csv, col.column_name always exists in data // for csv, col.column_name always exists in data
// since it streams the data in getData() with the updated col.column_name // since it streams the data in getData() with the updated col.column_name
const key = col.column_name in data ? col.column_name : col.ref_column_name const key = col.title in data ? col.title : col.ref_column_name
let d = data[key] let d = data[key]
if (col.uidt === UITypes.Date && d) { if (col.uidt === UITypes.Date && d) {
let dateFormat let dateFormat
@ -295,7 +293,7 @@ function remapColNames(batchData: any[], columns: ColumnType[]) {
} }
return { return {
...aggObj, ...aggObj,
[col.column_name]: d, [col.title]: d,
} }
}, {}), }, {}),
) )
@ -901,7 +899,7 @@ watch(modelRef, async () => {
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'column_name'"> <template v-if="column.key === 'column_name'">
<a-form-item v-bind="validateInfos[`tables.${tableIdx}.columns.${record.key}.${column.key}`]"> <a-form-item v-bind="validateInfos[`tables.${tableIdx}.columns.${record.key}.${column.key}`]">
<a-input :ref="(el: HTMLInputElement) => (inputRefs[record.key] = el)" v-model:value="record.column_name" /> <a-input :ref="(el: HTMLInputElement) => (inputRefs[record.key] = el)" v-model:value="record.title" />
</a-form-item> </a-form-item>
</template> </template>

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

@ -6,6 +6,7 @@ const useAttachment = () => {
const getPossibleAttachmentSrc = (item: Record<string, any>) => { const getPossibleAttachmentSrc = (item: Record<string, any>) => {
const res: string[] = [] const res: string[] = []
if (item?.data) res.push(item.data) if (item?.data) res.push(item.data)
if (item?.file) res.push(window.URL.createObjectURL(item.file))
if (item?.signedPath) res.push(`${appInfo.value.ncSiteUrl}/${item.signedPath}`) if (item?.signedPath) res.push(`${appInfo.value.ncSiteUrl}/${item.signedPath}`)
if (item?.signedUrl) res.push(item.signedUrl) if (item?.signedUrl) res.push(item.signedUrl)
if (item?.path) res.push(`${appInfo.value.ncSiteUrl}/${item.path}`) if (item?.path) res.push(`${appInfo.value.ncSiteUrl}/${item.path}`)

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

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

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

@ -275,9 +275,9 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
} }
// reload view columns when active view changes // reload view columns when active view changes
// or when columns count changes(delete/add) // or when columns changes(delete/add)
watch( watch(
[() => view?.value?.id, () => meta.value?.columns?.length], [() => view?.value?.id, () => meta.value?.columns],
async ([newViewId]) => { async ([newViewId]) => {
// reload only if view belongs to current table // reload only if view belongs to current table
if (newViewId && view.value?.fk_model_id === meta.value?.id) { if (newViewId && view.value?.fk_model_id === meta.value?.id) {

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

@ -58,6 +58,7 @@ export default class CSVTemplateAdapter {
this.tables[tableIdx] = [] this.tables[tableIdx] = []
for (const [columnIdx, columnName] of columnNames.entries()) { for (const [columnIdx, columnName] of columnNames.entries()) {
const title = ((columnNameRowExist && columnName.toString().trim()) || `Field ${columnIdx + 1}`).trim()
let cn: string = ((columnNameRowExist && columnName.toString().trim()) || `field_${columnIdx + 1}`) let cn: string = ((columnNameRowExist && columnName.toString().trim()) || `field_${columnIdx + 1}`)
.replace(/[` ~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/g, '_') .replace(/[` ~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/g, '_')
.trim() .trim()
@ -70,6 +71,7 @@ export default class CSVTemplateAdapter {
this.distinctValues[columnIdx] = new Set<string>() this.distinctValues[columnIdx] = new Set<string>()
this.columnValues[columnIdx] = [] this.columnValues[columnIdx] = []
tableObj.columns.push({ tableObj.columns.push({
title,
column_name: cn, column_name: cn,
ref_column_name: cn, ref_column_name: cn,
meta: {}, meta: {},

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

@ -113,6 +113,10 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
) )
for (let col = 0; col < rows[0].length; col++) { for (let col = 0; col < rows[0].length; col++) {
const title = (
(this.config.firstRowAsHeaders && rows[0] && rows[0][col] && rows[0][col].toString().trim()) ||
`Field ${col + 1}`
).trim()
let cn: string = ( let cn: string = (
(this.config.firstRowAsHeaders && rows[0] && rows[0][col] && rows[0][col].toString().trim()) || (this.config.firstRowAsHeaders && rows[0] && rows[0][col] && rows[0][col].toString().trim()) ||
`field_${col + 1}` `field_${col + 1}`
@ -126,6 +130,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
columnNamePrefixRef[cn] = 0 columnNamePrefixRef[cn] = 0
const column: Record<string, any> = { const column: Record<string, any> = {
title,
column_name: cn, column_name: cn,
ref_column_name: cn, ref_column_name: cn,
meta: {}, meta: {},

2
packages/nc-gui/helpers/parsers/JSONTemplateAdapter.ts

@ -90,8 +90,10 @@ export default class JSONTemplateAdapter extends TemplateGenerator {
columns.push(...normalizedNestedColumns) columns.push(...normalizedNestedColumns)
} }
} else { } else {
const title = path.join(' ').trim()
const cn = path.join('_').replace(/\W/g, '_').trim() const cn = path.join('_').replace(/\W/g, '_').trim()
const column: Record<string, any> = { const column: Record<string, any> = {
title,
column_name: cn, column_name: cn,
ref_column_name: cn, ref_column_name: cn,
uidt: UITypes.SingleLineText, uidt: UITypes.SingleLineText,

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

@ -581,6 +581,7 @@
"childTable": "جدول فرعي", "childTable": "جدول فرعي",
"childColumn": "عمود فرعي", "childColumn": "عمود فرعي",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record", "linkToAnotherRecord": "Link to another record",
"links": "Links", "links": "Links",
"onUpdate": "عند التحديث", "onUpdate": "عند التحديث",

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

@ -581,6 +581,7 @@
"childTable": "Child table", "childTable": "Child table",
"childColumn": "Child column", "childColumn": "Child column",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record", "linkToAnotherRecord": "Link to another record",
"links": "Links", "links": "Links",
"onUpdate": "আপড", "onUpdate": "আপড",

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

@ -581,6 +581,7 @@
"childTable": "Podřízená tabulka", "childTable": "Podřízená tabulka",
"childColumn": "Podřízený sloupec", "childColumn": "Podřízený sloupec",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Odkaz na jiný záznam", "linkToAnotherRecord": "Odkaz na jiný záznam",
"links": "Links", "links": "Links",
"onUpdate": "Při aktualizaci", "onUpdate": "Při aktualizaci",

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

@ -581,6 +581,7 @@
"childTable": "Undertabel", "childTable": "Undertabel",
"childColumn": "Underkolonner", "childColumn": "Underkolonner",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link til en anden post", "linkToAnotherRecord": "Link til en anden post",
"links": "Links", "links": "Links",
"onUpdate": "På opdatering", "onUpdate": "På opdatering",

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

@ -581,6 +581,7 @@
"childTable": "Child-Tabelle", "childTable": "Child-Tabelle",
"childColumn": "Child-Spalte", "childColumn": "Child-Spalte",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link zu einem anderen Datensatz", "linkToAnotherRecord": "Link zu einem anderen Datensatz",
"links": "Links", "links": "Links",
"onUpdate": "Update", "onUpdate": "Update",

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

@ -581,6 +581,7 @@
"childTable": "Child table", "childTable": "Child table",
"childColumn": "Child field", "childColumn": "Child field",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record", "linkToAnotherRecord": "Link to another record",
"links": "Links", "links": "Links",
"onUpdate": "On Update", "onUpdate": "On Update",
@ -665,7 +666,7 @@
"enablePublicAccess": "Enable Public Access", "enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?", "doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access", "editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing", "enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password", "restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access", "manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download", "allowDownload": "Allow Download",
@ -718,7 +719,7 @@
"groupBy": "Group By", "groupBy": "Group By",
"addSubGroup": "Add subgroup", "addSubGroup": "Add subgroup",
"shareBase": { "shareBase": {
"label": "Share base", "label": "Share Base",
"disable": "Disable shared base", "disable": "Disable shared base",
"enable": "Anyone with the link", "enable": "Anyone with the link",
"link": "Shared base link" "link": "Shared base link"

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

@ -581,6 +581,7 @@
"childTable": "Tabla hija", "childTable": "Tabla hija",
"childColumn": "Columna hija", "childColumn": "Columna hija",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Enlace a otro registro", "linkToAnotherRecord": "Enlace a otro registro",
"links": "Links", "links": "Links",
"onUpdate": "En actualización", "onUpdate": "En actualización",

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

@ -581,6 +581,7 @@
"childTable": "Child table", "childTable": "Child table",
"childColumn": "Child column", "childColumn": "Child column",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record", "linkToAnotherRecord": "Link to another record",
"links": "Links", "links": "Links",
"onUpdate": "On Update", "onUpdate": "On Update",

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

@ -581,6 +581,7 @@
"childTable": "جدول فرزند", "childTable": "جدول فرزند",
"childColumn": "ستون فرزند", "childColumn": "ستون فرزند",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "لینک به رکورد دیگر", "linkToAnotherRecord": "لینک به رکورد دیگر",
"links": "Links", "links": "Links",
"onUpdate": "هنگام به روز رسانی", "onUpdate": "هنگام به روز رسانی",

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

@ -581,6 +581,7 @@
"childTable": "Lapsipöytä", "childTable": "Lapsipöytä",
"childColumn": "Lapsipylväs", "childColumn": "Lapsipylväs",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Linkki toiseen tietueeseen", "linkToAnotherRecord": "Linkki toiseen tietueeseen",
"links": "Links", "links": "Links",
"onUpdate": "Päivitys", "onUpdate": "Päivitys",

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

@ -581,6 +581,7 @@
"childTable": "Table enfant", "childTable": "Table enfant",
"childColumn": "Colonne enfant", "childColumn": "Colonne enfant",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Lien vers un autre enregistrement", "linkToAnotherRecord": "Lien vers un autre enregistrement",
"links": "Links", "links": "Links",
"onUpdate": "Mise à jour en cours", "onUpdate": "Mise à jour en cours",

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

@ -581,6 +581,7 @@
"childTable": "טבלת ילדים", "childTable": "טבלת ילדים",
"childColumn": "טור ילדים", "childColumn": "טור ילדים",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record", "linkToAnotherRecord": "Link to another record",
"links": "Links", "links": "Links",
"onUpdate": "על עדכון", "onUpdate": "על עדכון",

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

@ -581,6 +581,7 @@
"childTable": "Child table", "childTable": "Child table",
"childColumn": "Child column", "childColumn": "Child column",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record", "linkToAnotherRecord": "Link to another record",
"links": "Links", "links": "Links",
"onUpdate": "On Update", "onUpdate": "On Update",

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

@ -581,6 +581,7 @@
"childTable": "Dječji stol", "childTable": "Dječji stol",
"childColumn": "Dječji stupac", "childColumn": "Dječji stupac",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Poveži s drugim zapisom", "linkToAnotherRecord": "Poveži s drugim zapisom",
"links": "Links", "links": "Links",
"onUpdate": "Na ažuriranje", "onUpdate": "Na ažuriranje",

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

@ -581,6 +581,7 @@
"childTable": "Tabel Anak", "childTable": "Tabel Anak",
"childColumn": "Kolom anak.", "childColumn": "Kolom anak.",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Tautan ke catatan lain", "linkToAnotherRecord": "Tautan ke catatan lain",
"links": "Links", "links": "Links",
"onUpdate": "Pada pembaruan", "onUpdate": "Pada pembaruan",

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

@ -581,6 +581,7 @@
"childTable": "Sottotabella", "childTable": "Sottotabella",
"childColumn": "Sottocolonna", "childColumn": "Sottocolonna",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Collegamento a un altro record", "linkToAnotherRecord": "Collegamento a un altro record",
"links": "Links", "links": "Links",
"onUpdate": "All'aggiornamento", "onUpdate": "All'aggiornamento",

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

@ -1,32 +1,32 @@
{ {
"dashboards": { "dashboards": {
"create_new_dashboard_project": "Create New Interface", "create_new_dashboard_project": "インターフェースを作成",
"connect_data_sources": "Connect data sources", "connect_data_sources": "データソースに接続",
"alert": "Alert", "alert": "アラート",
"alert-message": "No databases have been connected. Connect database bases to build interfaces. Skip this step and add databases from the base home page later.", "alert-message": "データベースが接続されていません。DBベースを接続してインターフェースを構成してください。スキップして後でベースのホームページを追加することもできます。",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "Select Database Bases that you want to link to this Interface.", "select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "このインターフェースに接続するプロジェクトを選択します。",
"create_interface": "Create interface", "create_interface": "インターフェースを作成",
"project_name": "Base Name", "project_name": "プロジェクト名",
"connect": "Connect", "connect": "接続",
"buttonActionTypes": { "buttonActionTypes": {
"open_external_url": "Open external link", "open_external_url": "外部リンクを開く",
"delete_record": "Delete record", "delete_record": "レコードを削除",
"update_record": "Update record", "update_record": "レコードを更新",
"open_layout": "Open layout" "open_layout": "レイアウトを開く"
}, },
"widgets": { "widgets": {
"static_text": "Text", "static_text": "テキスト",
"chart": "Chart", "chart": "チャート",
"table": "Table", "table": "",
"image": "Image", "image": "Image",
"map": "Map", "map": "マップ",
"button": "Button", "button": "ボタン",
"number": "Number", "number": "ナンバー",
"bar_chart": "Bar Chart", "bar_chart": "棒グラフ",
"line_chart": "Line Chart", "line_chart": "折れ線グラフ",
"area_chart": "Area Chart", "area_chart": "面グラフ",
"pie_chart": "Pie Chart", "pie_chart": "円グラフ",
"donut_chart": "Donut Chart", "donut_chart": "ドーナツグラフ",
"scatter_plot": "Scatter Plot", "scatter_plot": "Scatter Plot",
"bubble_chart": "Bubble Chart", "bubble_chart": "Bubble Chart",
"radar_chart": "Radar Chart", "radar_chart": "Radar Chart",
@ -39,7 +39,7 @@
} }
}, },
"general": { "general": {
"quit": "Quit", "quit": "終了",
"home": "ホーム", "home": "ホーム",
"load": "読み込み", "load": "読み込み",
"open": "開く", "open": "開く",
@ -47,7 +47,7 @@
"yes": "はい", "yes": "はい",
"no": "いいえ", "no": "いいえ",
"ok": "OK", "ok": "OK",
"back": "Back", "back": "戻る",
"and": "と", "and": "と",
"or": "または", "or": "または",
"add": "追加", "add": "追加",
@ -78,7 +78,7 @@
"quote": "Quote", "quote": "Quote",
"submit": "送信", "submit": "送信",
"create": "作成", "create": "作成",
"createEntity": "Create {entity}", "createEntity": "{entity}を作成",
"creating": "Creating", "creating": "Creating",
"creatingEntity": "Creating {entity}", "creatingEntity": "Creating {entity}",
"details": "Details", "details": "Details",
@ -581,6 +581,7 @@
"childTable": "子テーブル", "childTable": "子テーブル",
"childColumn": "子カラム", "childColumn": "子カラム",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "他のレコードへのリンク", "linkToAnotherRecord": "他のレコードへのリンク",
"links": "Links", "links": "Links",
"onUpdate": "更新中", "onUpdate": "更新中",

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

@ -581,6 +581,7 @@
"childTable": "자식 테이블", "childTable": "자식 테이블",
"childColumn": "자식 컬럼", "childColumn": "자식 컬럼",
"childField": "자식 필드", "childField": "자식 필드",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "다른 레코드에 링크", "linkToAnotherRecord": "다른 레코드에 링크",
"links": "링크", "links": "링크",
"onUpdate": "업데이트 시 ", "onUpdate": "업데이트 시 ",

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

@ -581,6 +581,7 @@
"childTable": "Apakštabula", "childTable": "Apakštabula",
"childColumn": "Apakškolonna", "childColumn": "Apakškolonna",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Saite uz citu ierakstu", "linkToAnotherRecord": "Saite uz citu ierakstu",
"links": "Links", "links": "Links",
"onUpdate": "Atjaunojot", "onUpdate": "Atjaunojot",

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

@ -581,6 +581,7 @@
"childTable": "Child Tabel", "childTable": "Child Tabel",
"childColumn": "Child Kolom", "childColumn": "Child Kolom",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link naar een ander record", "linkToAnotherRecord": "Link naar een ander record",
"links": "Links", "links": "Links",
"onUpdate": "Bij update", "onUpdate": "Bij update",

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

@ -581,6 +581,7 @@
"childTable": "Barnbord", "childTable": "Barnbord",
"childColumn": "Barn kolonne", "childColumn": "Barn kolonne",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record", "linkToAnotherRecord": "Link to another record",
"links": "Links", "links": "Links",
"onUpdate": "På oppdatering", "onUpdate": "På oppdatering",

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

@ -581,6 +581,7 @@
"childTable": "Tabele podrzędne", "childTable": "Tabele podrzędne",
"childColumn": "Kolumna podrzędna", "childColumn": "Kolumna podrzędna",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link do innego rekordu", "linkToAnotherRecord": "Link do innego rekordu",
"links": "Links", "links": "Links",
"onUpdate": "Przy aktualizacji", "onUpdate": "Przy aktualizacji",

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

@ -581,6 +581,7 @@
"childTable": "Mesa de criança", "childTable": "Mesa de criança",
"childColumn": "Coluna de criança", "childColumn": "Coluna de criança",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link para outro registo", "linkToAnotherRecord": "Link para outro registo",
"links": "Links", "links": "Links",
"onUpdate": "Na atualização", "onUpdate": "Na atualização",

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

@ -581,6 +581,7 @@
"childTable": "Tabela filha", "childTable": "Tabela filha",
"childColumn": "Coluna de criança", "childColumn": "Coluna de criança",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link para outro registo", "linkToAnotherRecord": "Link para outro registo",
"links": "Links", "links": "Links",
"onUpdate": "Ao atualizar", "onUpdate": "Ao atualizar",

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

@ -581,6 +581,7 @@
"childTable": "Дочерняя таблица", "childTable": "Дочерняя таблица",
"childColumn": "Дочерний столбец", "childColumn": "Дочерний столбец",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Ссылка на другую запись", "linkToAnotherRecord": "Ссылка на другую запись",
"links": "Links", "links": "Links",
"onUpdate": "При обновлении", "onUpdate": "При обновлении",

1
packages/nc-gui/lang/sk.json

@ -581,6 +581,7 @@
"childTable": "Detský stôl", "childTable": "Detský stôl",
"childColumn": "Detský stĺpec", "childColumn": "Detský stĺpec",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Prepojenie na iný záznam", "linkToAnotherRecord": "Prepojenie na iný záznam",
"links": "Links", "links": "Links",
"onUpdate": "O aktualizácii", "onUpdate": "O aktualizácii",

1
packages/nc-gui/lang/sl.json

@ -581,6 +581,7 @@
"childTable": "Otroška miza", "childTable": "Otroška miza",
"childColumn": "Otroška stolpec", "childColumn": "Otroška stolpec",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Povezava na drug zapis", "linkToAnotherRecord": "Povezava na drug zapis",
"links": "Links", "links": "Links",
"onUpdate": "Na posodobitvi", "onUpdate": "Na posodobitvi",

1
packages/nc-gui/lang/sv.json

@ -581,6 +581,7 @@
"childTable": "Barnbord", "childTable": "Barnbord",
"childColumn": "Barnkolonn", "childColumn": "Barnkolonn",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Länk till en annan post", "linkToAnotherRecord": "Länk till en annan post",
"links": "Links", "links": "Links",
"onUpdate": "Vid uppdatering", "onUpdate": "Vid uppdatering",

1
packages/nc-gui/lang/th.json

@ -581,6 +581,7 @@
"childTable": "ตารางลก", "childTable": "ตารางลก",
"childColumn": "คอลมนเดก", "childColumn": "คอลมนเดก",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "เชอมไปยงตารางอน", "linkToAnotherRecord": "เชอมไปยงตารางอน",
"links": "Links", "links": "Links",
"onUpdate": "เมออปเดต", "onUpdate": "เมออปเดต",

1
packages/nc-gui/lang/tr.json

@ -581,6 +581,7 @@
"childTable": "Alt tablo", "childTable": "Alt tablo",
"childColumn": "Alt sütun", "childColumn": "Alt sütun",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Başka bir kayda bağlantı", "linkToAnotherRecord": "Başka bir kayda bağlantı",
"links": "Links", "links": "Links",
"onUpdate": "Güncellenince", "onUpdate": "Güncellenince",

1
packages/nc-gui/lang/uk.json

@ -581,6 +581,7 @@
"childTable": "Дочірня таблиця", "childTable": "Дочірня таблиця",
"childColumn": "Дочірній стовпець", "childColumn": "Дочірній стовпець",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Посилання на інший запис", "linkToAnotherRecord": "Посилання на інший запис",
"links": "Links", "links": "Links",
"onUpdate": "При оновленні", "onUpdate": "При оновленні",

1
packages/nc-gui/lang/vi.json

@ -581,6 +581,7 @@
"childTable": "Bảng con", "childTable": "Bảng con",
"childColumn": "Cột trẻ con.", "childColumn": "Cột trẻ con.",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record", "linkToAnotherRecord": "Link to another record",
"links": "Links", "links": "Links",
"onUpdate": "Trên bản cập nhật", "onUpdate": "Trên bản cập nhật",

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

File diff suppressed because it is too large Load Diff

1
packages/nc-gui/lang/zh-Hant.json

@ -581,6 +581,7 @@
"childTable": "子表", "childTable": "子表",
"childColumn": "子欄", "childColumn": "子欄",
"childField": "Child field", "childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "連結到另一個紀錄", "linkToAnotherRecord": "連結到另一個紀錄",
"links": "Links", "links": "Links",
"onUpdate": "更新時", "onUpdate": "更新時",

20
packages/nc-gui/package.json

@ -73,7 +73,7 @@
"rfdc": "^1.3.0", "rfdc": "^1.3.0",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"socket.io-client": "^4.7.2", "socket.io-client": "^4.7.2",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.1",
"splitpanes": "^3.1.5", "splitpanes": "^3.1.5",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
"unique-names-generator": "^4.7.1", "unique-names-generator": "^4.7.1",
@ -93,10 +93,10 @@
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
"@tiptap/extension-link": "2.0.4", "@tiptap/extension-link": "2.0.4",
"@tiptap/extension-task-list": "2.0.4", "@tiptap/extension-task-list": "2.0.4",
"@tiptap/extension-underline": "^2.1.12", "@tiptap/extension-underline": "^2.1.13",
"@tiptap/html": "2.0.4", "@tiptap/html": "2.0.4",
"@tiptap/pm": "^2.1.12", "@tiptap/pm": "^2.1.13",
"@tiptap/starter-kit": "^2.1.12", "@tiptap/starter-kit": "^2.1.13",
"marked": "^4.3.0", "marked": "^4.3.0",
"turndown": "^7.1.2", "turndown": "^7.1.2",
"@tiptap/vue-3": "2.0.4" "@tiptap/vue-3": "2.0.4"
@ -106,7 +106,7 @@
"@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@iconify-json/ant-design": "^1.1.12", "@iconify-json/ant-design": "^1.1.12",
"@iconify-json/bi": "^1.1.21", "@iconify-json/bi": "^1.1.21",
"@iconify-json/carbon": "^1.1.23", "@iconify-json/carbon": "^1.1.24",
"@iconify-json/cil": "^1.1.7", "@iconify-json/cil": "^1.1.7",
"@iconify-json/clarity": "^1.1.11", "@iconify-json/clarity": "^1.1.11",
"@iconify-json/eva": "^1.1.9", "@iconify-json/eva": "^1.1.9",
@ -114,18 +114,18 @@
"@iconify-json/ion": "^1.1.14", "@iconify-json/ion": "^1.1.14",
"@iconify-json/la": "^1.1.7", "@iconify-json/la": "^1.1.7",
"@iconify-json/logos": "^1.1.40", "@iconify-json/logos": "^1.1.40",
"@iconify-json/lucide": "^1.1.143", "@iconify-json/lucide": "^1.1.144",
"@iconify-json/material-symbols": "^1.1.65", "@iconify-json/material-symbols": "^1.1.65",
"@iconify-json/mdi": "^1.1.57", "@iconify-json/mdi": "^1.1.58",
"@iconify-json/mi": "^1.1.7", "@iconify-json/mi": "^1.1.7",
"@iconify-json/ph": "^1.1.8", "@iconify-json/ph": "^1.1.8",
"@iconify-json/ri": "^1.1.14", "@iconify-json/ri": "^1.1.15",
"@iconify-json/simple-icons": "^1.1.81", "@iconify-json/simple-icons": "^1.1.82",
"@iconify-json/system-uicons": "^1.1.11", "@iconify-json/system-uicons": "^1.1.11",
"@iconify-json/tabler": "^1.1.100", "@iconify-json/tabler": "^1.1.100",
"@iconify-json/vscode-icons": "^1.1.31", "@iconify-json/vscode-icons": "^1.1.31",
"@intlify/unplugin-vue-i18n": "^0.12.3", "@intlify/unplugin-vue-i18n": "^0.12.3",
"@nuxt/image-edge": "1.1.0-28346300.6030589", "@nuxt/image-edge": "1.1.0-28355789.b3279fe",
"@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",

2
packages/nc-gui/pages/signin.vue

@ -184,7 +184,7 @@ function navigateForgotPassword() {
</a> </a>
</div> </div>
<div class="text-end prose-sm" v-if="!appInfo.inviteOnlySignup"> <div v-if="!appInfo.inviteOnlySignup" class="text-end prose-sm">
{{ $t('msg.info.signUp.dontHaveAccount') }} {{ $t('msg.info.signUp.dontHaveAccount') }}
<nuxt-link @click="navigateSignUp">{{ $t('general.signUp') }}</nuxt-link> <nuxt-link @click="navigateSignUp">{{ $t('general.signUp') }}</nuxt-link>
</div> </div>

2
packages/nc-gui/store/bases.ts

@ -12,6 +12,7 @@ export const useBases = defineStore('basesStore', () => {
const bases = ref<Map<string, NcProject>>(new Map()) const bases = ref<Map<string, NcProject>>(new Map())
const basesList = computed<NcProject[]>(() => Array.from(bases.value.values()).sort((a, b) => a.updated_at - b.updated_at)) const basesList = computed<NcProject[]>(() => Array.from(bases.value.values()).sort((a, b) => a.updated_at - b.updated_at))
const baseUserCount = ref<number | undefined>(undefined)
const router = useRouter() const router = useRouter()
const route = router.currentRoute const route = router.currentRoute
@ -294,6 +295,7 @@ export const useBases = defineStore('basesStore', () => {
return { return {
bases, bases,
basesList, basesList,
baseUserCount,
loadProjects, loadProjects,
loadProject, loadProject,
getSqlUi, getSqlUi,

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

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

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

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

378
packages/noco-docs/docs/070.fields/040.field-types/060.formula/020.numeric-functions.md

@ -5,34 +5,360 @@ tags: ['Fields', 'Field types', 'Formula']
keywords: ['Fields', 'Field types', 'Formula', 'Create formula field', 'Numeric functions'] keywords: ['Fields', 'Field types', 'Formula', 'Create formula field', 'Numeric functions']
--- ---
This cheat sheet provides a quick reference guide for various mathematical functions commonly used in data analysis and programming. Each function is accompanied by its syntax, a sample usage, and a brief description.
### Numeric functions ------------
| Name | Syntax | Sample | Output |
|-------------|----------------------------|----------------------------------------|-------------------------------------------------------------------------------------------------------------|
| **ABS** | `ABS(value)` | `ABS({field})` | Absolute value of the input parameter |
| **ADD** | `ADD(value1,[value2,...])` | `ADD({field1}, {field2})` | Sum of input parameters |
| **AVG** | `AVG(value1,[value2,...])` | `AVG({field1}, {field2})` | Average of input parameters |
| **CEILING** | `CEILING(value)` | `CEILING({field})` | Rounded next largest integer value of input parameter |
| **EXP** | `EXP(value)` | `EXP({field})` | Exponential value of input parameter (`e^x`) |
| **FLOOR** | `FLOOR(value)` | `FLOOR({field})` | Rounded largest integer less than or equal to input parameter |
| **INT** | `INT(value)` | `INT({field})` | Integer value of input parameter |
| **LOG** | `LOG([base], value)` | `LOG(10, {field})` | Logarithm of input parameter to the base (default = e) specified |
| **MAX** | `MAX(value1,[value2,...])` | `MAX({field1}, {field2}, {field3})` | Maximum value amongst input parameters |
| **MIN** | `MIN(value1,[value2,...])` | `MIN({field1}, {field2}, {field3})` | Minimum value amongst input parameters |
| **MOD** | `MOD(value1, value2)` | `MOD({field}, 2)` | Remainder after integer division of input parameters |
| **POWER** | `POWER(base, exponent)` | `POWER({field}, 3)` | `base` to the `exponent` power, as in `base ^ exponent` |
| **ROUND** | `ROUND(value, precision)` | `ROUND({field}, 3)` | Round input `value` to decimal place specified by `precision` (Nearest integer if `precision` not provided) |
| **SQRT** | `SQRT(value)` | `SQRT({field})` | Square root of the input parameter |
| **COUNT** | `COUNT(value1,[value2,...])`| `COUNT({field1},{field2},{field3})` | Count the number of arguments that are numbers |
| **COUNTA** | `COUNTA(value1,[value2,...])`| `COUNTA({field1},{field2},{field3})`| Counts the number of non-empty arguments |
| **COUNTALL** | `COUNTALL(value1,[value2,...])`|`COUNTALL({field1},{field2},{field3})`| Counts the number of arguments |
| **EVEN** | `EVEN(value)` | `EVEN({field})` | Returns the nearest even integer that is greater than or equal to the specified value |
| **ODD** | `ODD(value)` | `ODD({field})` | Returns the nearest odd integer that is greater than or equal to the specified value |
| **VALUE** | `VALUE(value)` | `VALUE({field})` | Extract the numeric value from a string, if `%` or `-` is present, it will handle it accordingly and return the numeric value|
| **ROUNDDOWN** | `ROUNDDOWN(value, [precision])` | `ROUNDDOWN({field}, 2)` | Round down the value after the decimal point to the number of decimal places given by "precision"(default is 0) |
| **ROUNDUP** | `ROUNDUP(value, [precision])` | `ROUNDUP({field}, 2)` | Round up the value after the decimal point to the number of decimal places given by "precision"(default is 0) |
## ABS
The ABS function returns the distance of the number from zero on the number line, ensuring that the result is non-negative.
#### Syntax
```plaintext
ABS(number)
```
#### Sample
```plaintext
ABS(10.35) => 10.35
ABS(-15) => 15
```
------------
## ADD
The ADD function computes the total of multiple numbers provided as arguments.
#### Syntax
```plaintext
ADD(number1, [number2, ...])
```
#### Sample
```plaintext
ADD(5, 7) => 12
ADD(-10, 15, 20) => 25
```
------------
## AVG
The AVG function calculates the mean of a set of numerical values.
#### Syntax
```plaintext
AVG(number1, [number2, ...])
```
#### Sample
```plaintext
AVG(10, 20, 30) => 20
AVG(-5, 5) => 0
```
------------
## CEILING
The CEILING function rounds a number up to the nearest integer greater than or equal to the input.
#### Syntax
```plaintext
CEILING(number)
```
#### Sample
```plaintext
CEILING(8.75) => 9
CEILING(-15.25) => -15
```
------------
## COUNT
The COUNT function calculates the number of numeric arguments provided.
#### Syntax
```plaintext
COUNT(number1, [number2, ...])
```
#### Sample
```plaintext
COUNT(1, 2, "abc", 3) => 3
COUNT(-5, 0, "$abc", 5) => 3
```
------------
## COUNTA
The COUNTA function counts the number of non-empty arguments provided.
#### Syntax
```plaintext
COUNTA(value1, [value2, ...])
```
#### Sample
```plaintext
COUNTA(1, "", "text") => 2
COUNTA("one", "two", "three") => 3
```
------------
## COUNTALL
The COUNTALL function calculates the total number of arguments, both numeric and non-numeric.
#### Syntax
```plaintext
COUNTALL(value1, [value2, ...])
```
#### Sample
```plaintext
COUNTALL(1, "", "text") => 3
COUNTALL("one", "two", "three") => 3
```
------------
## EVEN
The EVEN function rounds positive values up to the nearest even number and negative values down to the nearest even number.
#### Syntax
```plaintext
EVEN(number)
```
#### Sample
```plaintext
EVEN(7) => 8
EVEN(-5) => -6
```
------------
## EXP
The EXP function returns 'e' raised to the power of a given number.
#### Syntax
```plaintext
EXP(number)
```
#### Sample
```plaintext
EXP(2) => 7.38905609893065
EXP(-1) => 0.36787944117144233
```
------------
## FLOOR
The FLOOR function rounds a number down to the nearest integer.
#### Syntax
```plaintext
FLOOR(number)
```
#### Sample
```plaintext
FLOOR(8.75) => 8
FLOOR(-15.25) => -16
```
------------
## INT
The INT function truncates the decimal part, returning the integer portion of a number.
#### Syntax
```plaintext
INT(number)
```
#### Sample
```plaintext
INT(8.75) => 8
INT(-15.25) => -15
```
------------
## LOG
The LOG function computes the logarithm of a number to a specified base. (default = e).
#### Syntax
```plaintext
LOG([base], number)
```
#### Sample
```plaintext
LOG(10, 100) => 2
LOG(2, 8) => 3
```
------------
## MAX
The MAX function identifies the highest value from a set of numbers.
#### Syntax
```plaintext
MAX(number1, [number2, ...])
```
#### Sample
```plaintext
MAX(5, 10, 3) => 10
MAX(-10, -5, -20) => -5
```
------------
## MIN
The MIN function identifies the lowest value from a set of numbers.
#### Syntax
```plaintext
MIN(number1, [number2, ...])
```
#### Sample
```plaintext
MIN(5, 10, 3) => 3
MIN(-10, -5, -20) => -20
```
------------
## MOD
The MOD function calculates the remainder when dividing (integer division) one number by another.
#### Syntax
```plaintext
MOD(number1, number2)
```
#### Sample
```plaintext
MOD(10, 3) => 1
MOD(-15, 4) => -3
```
------------
## ODD
The ODD function rounds positive values up to the nearest odd number and negative values down to the nearest odd number.
#### Syntax
```plaintext
ODD(number)
```
#### Sample
```plaintext
ODD(6) => 7
ODD(-5.5) => -7
```
------------
## POWER
The POWER function raises a given base to a specified exponent.
#### Syntax
```plaintext
POWER(base, exponent)
```
#### Sample
```plaintext
POWER(2, 3) => 8
POWER(10, -2) => 0.01
```
------------
## ROUND
The ROUND function is used to round a number to a specified number of decimal places (precision). Default value for precision is 0.
#### Syntax
```plaintext
ROUND(number, [precision])
```
#### Sample
```plaintext
ROUND(8.765, 2) => 8.77
ROUND(-15.123, 1) => -15.1
```
------------
## ROUNDDOWN
The ROUNDDOWN function rounds a number down to a specified number of decimal places (precision). Default value for precision is 0.
#### Syntax
```plaintext
ROUNDDOWN(number, [precision])
```
#### Sample
```plaintext
ROUNDDOWN(8.765, 2) => 8.76
ROUNDDOWN(-15.123, 1) => -15.2
```
------------
## ROUNDUP
The ROUNDUP function rounds a number up to a specified number of decimal places (precision). Default value for precision is 0.
#### Syntax
```plaintext
ROUNDUP(number, [precision])
```
#### Sample
```plaintext
ROUNDUP(8.765, 2) => 8.77
ROUNDUP(-15.123, 1) => -15.1
```
------------
## SQRT
The SQRT function calculates the square root of a given number.
#### Syntax
```plaintext
SQRT(number)
```
#### Sample
```plaintext
SQRT(25) => 5
SQRT(2) => 1.4142135623730951
```
------------
## VALUE
The VALUE function is used to extract the numeric value from a string (after handling `%` or `-` accordingly).
#### Syntax
```plaintext
VALUE(text)
```
#### Sample
```plaintext
VALUE("123$") => 123
VALUE("USD -45.67") => -45.67
```
------------
## Related Articles ## Related Articles
- [Numeric and Logical Operators](015.operators.md) - [Numeric and Logical Operators](015.operators.md)

228
packages/noco-docs/docs/070.fields/040.field-types/060.formula/030.string-functions.md

@ -5,28 +5,216 @@ tags: ['Fields', 'Field types', 'Formula']
keywords: ['Fields', 'Field types', 'Formula', 'Create formula field', 'String functions'] keywords: ['Fields', 'Field types', 'Formula', 'Create formula field', 'String functions']
--- ---
This cheat sheet provides a quick reference guide for various string based functions commonly used in data analysis and programming. Each function is accompanied by its syntax, a sample usage, and a brief description.
### String functions ## CONCAT
The CONCAT function concatenates one or more strings into a single string.
| Name | Syntax | Sample | Output |
|-------------|----------------------------------|-------------------------------------|---------------------------------------------------------------------------|
| **CONCAT** | `CONCAT(str1, [str2,...])` | `CONCAT({field1}, ' ', {field2})` | Concatenated string of input parameters |
| **LEFT** | `LEFT(str1, n)` | `LEFT({field}, 3)` | `n` characters from the beginning of input parameter |
| **LEN** | `LEN(str)` | `LEN({field})` | Input parameter character length |
| **LOWER** | `LOWER(str)` | `LOWER({field})` | Lower case converted string of input parameter |
| **MID** | `MID(str, position, [count])` | `MID({field}, 3, 2)` | Alias for `SUBSTR` |
| **REPEAT** | `REPEAT(str, count)` | `REPEAT({field}, 2)` | Specified copies of the input parameter string concatenated together |
| **REPLACE** | `REPLACE(str, srchStr, rplcStr)` | `REPLACE({field}, 'int', 'num')` | String, after replacing all occurrences of `srchStr` with `rplcStr` |
| **RIGHT** | `RIGHT(str, n)` | `RIGHT({field}, 3)` | `n` characters from the end of input parameter |
| **SEARCH** | `SEARCH(str, srchStr)` | `SEARCH({field}, 'str')` | Index of `srchStr` specified if found, 0 otherwise |
| **SUBSTR** | `SUBTR(str, position, [count])` | `SUBSTR({field}, 3, 2)` | Substring of length 'count' of input string, from the postition specified |
| **TRIM** | `TRIM(str)` | `TRIM({field})` | Remove trailing and leading whitespaces from input parameter |
| **UPPER** | `UPPER(str)` | `UPPER({field})` | Upper case converted string of input parameter |
| **URL** | `URL(str)` | `URL({field})` | Convert to a hyperlink if it is a valid URL |
| **REGEX_MATCH** | `REGEX_MATCH(str, pattern)` | `REGEX_MATCH({field}, 'a.*')` | Returns 1 if the input text matches a regular expression or 0 if it does not |
| **REGEX_EXTRACT** | `REGEX_EXTRACT(str, pattern)` | `REGEX_MATCH({field}, 'a.*')` | Returns the first match of a regular expression in a string |
| **REGEX_REPLACE** | `REGEX_REPLACE(str, pattern, replacer)` | `REGEX_MATCH({field}, 'a.*', '---')` | Replaces all matches of a regular expression in a string with a replacement string |
#### Syntax
```plaintext
CONCAT(text, [text,...])
```
#### Sample
```plaintext
CONCAT('John', ' ', 'Doe') => 'John Doe'
```
## LEFT
The LEFT function retrieves the first 'n' characters specified from the beginning of the input string.
#### Syntax
```plaintext
LEFT(text, count)
```
#### Sample
```plaintext
LEFT('123-456-7890', 3) => '123'
```
## LEN
The LEN function calculates and returns the total number of characters present in the provided string.
#### Syntax
```plaintext
LEN(text)
```
#### Sample
```plaintext
LEN('Product Description') => 19
```
## LOWER
The LOWER function transforms all characters in the input string to lowercase
#### Syntax
```plaintext
LOWER(text)
```
#### Sample
```plaintext
LOWER('User INPUT') => 'user input'
```
## MID
The MID function retrieves a substring from the input string starting at the specified position and extending for the specified count of characters.
#### Syntax
```plaintext
MID(text, position, [count])
```
#### Sample
```plaintext
MID('This is a sentence', 5, 3) => 'is '
```
## REGEX_EXTRACT
The REGEX_EXTRACT function searches the input string for the first occurrence of the specified regular expression pattern and returns the matched substring.
#### Syntax
```plaintext
REGEX_EXTRACT(text, pattern)
```
#### Sample
```plaintext
REGEX_EXTRACT('Error: Something went wrong', 'Error: (.*)') => 'Something went wrong'
```
## REGEX_MATCH
The REGEX_MATCH function evaluates whether the input string matches the specified regular expression pattern, returning 1 if there is a match and 0 if there is no match.
#### Syntax
```plaintext
REGEX_MATCH(text, pattern)
```
#### Sample
```plaintext
REGEX_MATCH('123-45-6789', '\d{3}-\d{2}-\d{4}') => 1
```
## REGEX_REPLACE
The REGEX_REPLACE function identifies all occurrences of the specified regular expression pattern in the input string and substitutes them with the provided replacement string.
#### Syntax
```plaintext
REGEX_REPLACE(text, pattern, replacer)
```
#### Sample
```plaintext
REGEX_REPLACE('Replace all bugs', 'bug', 'feature') => 'Replace all features'
```
## REPEAT
The REPEAT function duplicates the provided string the specified number of times, facilitating the creation of repeated patterns or sequences.
#### Syntax
```plaintext
REPEAT(text, count)
```
#### Sample
```plaintext
REPEAT('😃', 3) => '😃😃😃'
```
## REPLACE
The REPLACE function identifies all instances of a particular substring within the given string and substitutes them with another specified substring.
#### Syntax
```plaintext
REPLACE(text, srchStr, rplcStr)
```
#### Sample
```plaintext
REPLACE('Replace old text', 'old', 'new') => 'Replace new text'
```
## RIGHT
The RIGHT function retrieves the last 'n' characters from the end of the input string, allowing you to extract a substring starting from the right.
#### Syntax
```plaintext
RIGHT(text, n)
```
#### Sample
```plaintext
RIGHT('file_name.txt', 3) => 'txt'
```
## SEARCH
The SEARCH function identifies the position of the specified substring within the input string, returning the index if found, and 0 otherwise.
#### Syntax
```plaintext
SEARCH(text, srchStr)
```
#### Sample
```plaintext
SEARCH('user@example.com', '@') => 5
```
## SUBSTR
The SUBSTR function extracts a substring from the input string, starting at the specified position and optionally extending for the specified count of characters.
#### Syntax
```plaintext
SUBSTR(text, position, [count])
```
#### Sample
```plaintext
SUBSTR('Extract this text', 9, 4) => 'this'
```
## TRIM
The TRIM function eliminates any leading or trailing whitespaces from the input string.
#### Syntax
```plaintext
TRIM(text)
```
#### Sample
```plaintext
TRIM(' Trim this ') => 'Trim this'
```
## UPPER
The UPPER function transforms all characters in the input string to uppercase.
#### Syntax
```plaintext
UPPER(text)
```
#### Sample
```plaintext
UPPER('title') => 'TITLE'
```
## URL
The URL function checks if the input string is a valid URL and converts it into a hyperlink
#### Syntax
```plaintext
URL(text)
```
#### Sample
```plaintext
URL('https://www.example.com') => a clickable link for https://www.example.com
```
## Related Articles ## Related Articles

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

@ -5,22 +5,95 @@ tags: ['Fields', 'Field types', 'Formula', 'Date & Time']
keywords: ['Fields', 'Field types', 'Formula', 'Date & Time', 'Create formula field', 'Date functions'] keywords: ['Fields', 'Field types', 'Formula', 'Date & Time', 'Create formula field', 'Date functions']
--- ---
This cheat sheet provides a quick reference guide for various date functions commonly used in data analysis and programming. Each function is accompanied by its syntax, a sample usage, and a brief description.
| Name | Syntax | Sample | Output | Remark | ## DATETIME_DIFF
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| The DATETIME_DIFF function calculates the difference between two dates in various units.
| **NOW** | `NOW()` | `NOW()` | 2022-05-19 17:20:43 | Returns the current time and day |
| | `IF(NOW() < {DATE_COL}`, "true", "false")` | `IF(NOW() < date, "true", "false")` | If current date is less than `{DATE_COL}`, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. | #### Syntax
| **DATEADD** | `DATEADD(date \| datetime, value, ["day" \| "week" \| "month" \| "year"])` | `DATEADD(date, 1, 'day')` | Supposing `{DATE_COL}` is 2022-03-14. The result is 2022-03-15. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'day')` | ```plaintext
| | | `DATEADD(date, 1, 'week')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2022-03-21 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'week')` | DATETIME_DIFF(date1, date2, ["milliseconds" | "ms" | "seconds" | "s" | "minutes" | "m" | "hours" | "h" | "days" | "d" | "weeks" | "w" | "months" | "M" | "quarters" | "Q" | "years" | "y"])
| | | `DATEADD(date, 1, 'month')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2022-04-14 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'month')` | ```
| | | `DATEADD(date, 1, 'year')` | Supposing `{DATE_COL}` is 2022-03-14 03:14. The result is 2023-03-14 03:14. | DateTime fields and negative values are supported. Example: `DATEADD(DATE_TIME_COL, -1, 'year')` |
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than `{DATE_COL}` plus 10 days, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. | #### Sample
| | | `IF(NOW() < DATEADD(date,10,'day'), "true", "false")` | If the current date is less than `{DATE_COL}` plus 10 days, it returns true. Otherwise, it returns false. | DateTime fields and negative values are supported. | ```plaintext
| **DATETIME_DIFF** | `DATETIME_DIFF(date, date, ["milliseconds" \| "ms" \| "seconds" \| "s" \| "minutes" \| "m" \| "hours" \| "h" \| "days" \| "d" \| "weeks" \| "w" \| "months" \| "M" \| "quarters" \| "Q" \| "years" \| "y"])` | `DATETIME_DIFF("2022/10/14", "2022/10/15", "second")` | Supposing `{DATE_COL_1}` is 2017-08-25 and `{DATE_COL_2}` is 2011-08-25. The result is 86400. | Compares two dates and returns the difference in the unit specified. Positive integers indicate the second date being in the past compared to the first and vice versa for negative ones. | DATETIME_DIFF("2022/10/14", "2022/10/15", "seconds") => -86400
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday | ```
| **WEEKDAY** | `WEEKDAY(date, [startDayOfWeek])` | `WEEKDAY(NOW())` | If today is Monday, it returns 0 | Returns the day of the week as an integer between 0 and 6 inclusive starting from Monday by default. You can optionally change the start day of the week by specifying in the second argument |
| | | `WEEKDAY(NOW(), "sunday")` | If today is Monday, it returns 1 | Get the week day of NOW() with the first day set as sunday | #### Remark
This function compares two dates and returns the difference in the specified unit. Positive integers indicate that second date is in the past compared to first, and vice versa for negative values.
---
## DATEADD
The DATEADD function adds a specified value to a date or datetime.
#### Syntax
```plaintext
DATEADD(date | datetime, value, ["day" | "week" | "month" | "year"])
```
#### Sample
```plaintext
DATEADD('2022-03-14', 1, 'day') => 2022-03-15
DATEADD('2022-03-14', 1, 'week') => 2022-03-21
DATEADD('2022-03-14', 1, 'month') => 2022-04-14
DATEADD('2022-03-14', 1, 'year') => 2023-03-14
```
#### Conditional Example
```plaintext
IF(NOW() < DATEADD(date, 10, 'day'), "true", "false") => If the current date is less than the specified date plus 10 days, it returns true. Otherwise, it returns false.
```
#### Remark
This function supports date and datetime fields and can handle negative values.
---
## NOW
The NOW function returns the current time and day.
#### Syntax
```plaintext
NOW()
```
#### Sample
```plaintext
NOW() => 2022-05-19 17:20:43 (current date & time)
```
#### Conditional Example
```plaintext
IF(NOW() < date, "true", "false") => If the current date is less than the specified date, it returns true. Otherwise, it returns false.
```
#### Remark
This function provides the current time and day, supporting datetime fields and negative values.
---
## WEEKDAY
The WEEKDAY function returns the day of the week as an integer.
#### Syntax
```plaintext
WEEKDAY(date, [startDayOfWeek])
```
#### Sample
```plaintext
WEEKDAY(NOW()) => If today is Monday, it returns 0.
WEEKDAY(NOW(), "sunday") => If today is Monday, it returns 1.
```
#### Remark
Returns the day of the week as an integer between 0 and 6 (inclusive), with Monday as the default start day. The start day of the week can be optionally changed by specifying it as the second argument.
---
## Related Articles ## Related Articles
- [Numeric and Logical Operators](015.operators.md) - [Numeric and Logical Operators](015.operators.md)

76
packages/noco-docs/docs/070.fields/040.field-types/060.formula/050.conditional-expressions.md

@ -1,19 +1,79 @@
--- ---
title: 'Conditional expressions' title: 'Conditional Expressions'
description: 'This article explains various conditional expressions that can be used in formula fields.' description: 'This article explains various conditional expressions that can be used in formula fields.'
tags: ['Fields', 'Field types', 'Formula'] tags: ['Fields', 'Field types', 'Formula']
keywords: ['Fields', 'Field types', 'Formula', 'Create formula field', 'Conditional expressions'] keywords: ['Fields', 'Field types', 'Formula', 'Create formula field', 'Conditional expressions']
--- ---
This cheat sheet provides a quick reference guide for various conditional expressions commonly used in data analysis and programming. Each expression is accompanied by its syntax, a sample usage, and a brief description.
### Conditional expressions ## IF
The IF function in programming and spreadsheet formulas provides a way to perform conditional operations. It evaluates a condition and returns a value if the condition is `TRUE`, or another value if the condition is `FALSE`.
| Name | Syntax | Sample | Output | #### Syntax
|------------|------------------------------------------------|------------------------------------------------|-------------------------------------------------------------| ```markdown
| **IF** | `IF(expr, successCase, elseCase)` | `IF({field} > 1, Value1, Value2)` | successCase if `expr` evaluates to TRUE, elseCase otherwise | IF(expr, successCase, elseCase)
| **SWITCH** | `SWITCH(expr, [pattern, value, ..., default])` | `SWITCH({field}, 1, 'One', 2, 'Two', '--')` | Switch case value based on `expr` output | ```
| **AND** | `AND(expr1, [expr2,...])` | `AND({field} > 2, {field} < 10)` | TRUE if all `expr` evaluate to TRUE |
| **OR** | `OR(expr1, [expr2,...])` | `OR({field} > 2, {field} < 10)` | TRUE if at least one `expr` evaluates to TRUE | #### Sample
```markdown
IF({field} > 1, Value1, Value2)
Output
- `Value1` if `{field} > 1` evaluates to TRUE
- `Value2` otherwise
```
## SWITCH
The SWITCH function is a versatile tool for handling multiple cases. It evaluates the given expression (expr) against a series of patterns and returns the corresponding value of the first matching pattern. If none match, it returns the default value.
#### Syntax
```markdown
SWITCH(expr, [pattern, value, ..., default])
```
#### Sample
```markdown
SWITCH({field}, 1, 'One', 2, 'Two', '--')
Output
Switch case value based on the output of `{field}`:
- `'One'` if `{field} = 1`
- `'Two'` if `{field} = 2`
- `'--'` for the default case
```
## AND
The AND function is a logical operator that returns TRUE only if all its conditions are true.
#### Syntax
```markdown
AND(expr1, [expr2,...])
```
#### Sample
```markdown
AND({field} > 2, {field} < 10)
Output
TRUE if both `{field} > 2` and `{field} < 10` evaluate to TRUE
```
## OR
The OR function is another logical operator, returning TRUE if at least one of its conditions is true.
#### Syntax
```markdown
OR(expr1, [expr2,...])
```
#### Sample
```markdown
OR({field} > 2, {field} < 10)
Output
TRUE if at least one of the conditions `{field} > 2` or `{field} < 10` evaluates to TRUE
```
:::tip :::tip
Logical operators, along with Numerical operators can be used to build conditional `expressions`. Logical operators, along with Numerical operators can be used to build conditional `expressions`.

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

Loading…
Cancel
Save