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;
}
}
// select dropdown border style
.ant-select-dropdown {
@apply border-1 border-gray-200
}
// menu item styling
.nc-menu-item {
@ -427,6 +431,9 @@ a {
.ant-dropdown-menu-submenu {
@apply !py-0;
&.ant-dropdown-menu-submenu-popup{
@apply border-1 border-gray-200
}
.ant-dropdown-menu,
.ant-menu {
@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']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopover: typeof import('ant-design-vue/es')['Popover']
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
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">
<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>
<a-select v-model:value="usersData.role" class="nc-user-roles" dropdown-class-name="nc-dropdown-user-role">
<a-select-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"
:placeholder="$t('placeholder.key')"
:filter-option="filterOption"
dropdown-class-name="border-1 border-gray-200"
/>
</a-form-item>
</td>

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

@ -22,6 +22,7 @@ interface Props {
}
const { modelValue, isPk } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { t } = useI18n()
@ -225,7 +226,7 @@ const clickHandler = () => {
:placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk"
: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"
@click="clickHandler"
@update:open="updateOpen"

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

@ -273,7 +273,7 @@ const isColDisabled = computed(() => {
:placeholder="placeholder"
:allow-clear="!readOnly && !localState && !isPk"
: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"
@click="clickHandler"
@ok="open = !open"

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

@ -100,7 +100,7 @@ const openInOSM = () => {
</div>
<div v-else data-testid="nc-geo-data-lat-long-set">{{ latLongStr }}</div>
<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>
<div class="flex mt-4 items-center mx-2">
<div class="mr-2">{{ $t('labels.lat') }}:</div>

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

@ -150,7 +150,14 @@ watch(isExpanded, () => {
</script>
<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 class="flex flex-row justify-between pt-1 pb-2 nc-json-action" @mousedown.stop>
<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 column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)
const isEditColumn = inject(EditColumnInj, ref(false))
@ -32,26 +34,73 @@ const vModel = computed({
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
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>
<template>
<input
v-if="editEnabled"
:ref="focus"
v-model="vModel"
class="w-full !text-sm !border-none !outline-none focus:ring-0 text-base p-1"
:class="{ '!px-2': editEnabled }"
type="number"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
@selectstart.capture.stop
@mousedown.stop
/>
<span v-else-if="vModel === null && showNull" class="nc-null capitalize">{{ $t('general.null') }}</span>
<span v-else>{{ vModel }}</span>
<div class="nc-filter-value-select w-full" @mouseover="onMouseover" @mouseleave="onMouseleave">
<input
v-if="(!isExpandedFormOpen && editEnabled) || (isExpandedFormOpen && expandedEditEnabled)"
:ref="focus"
v-model="vModel"
class="w-full !text-sm !border-none !outline-none focus:ring-0 text-base p-1"
:class="{ '!px-2': editEnabled }"
type="number"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
@blur="onBlur"
@focus="onFocus"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop
@keydown.up.stop
@keydown.delete.stop
@selectstart.capture.stop
@mousedown.stop
/>
<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>

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

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

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

@ -136,7 +136,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
:allow-clear="!readOnly && !localState && !isPk"
:input-read-only="true"
: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"
@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"
:input-read-only="true"
: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"
@change="open = (active || editable) && !open"
@ok="open = !open"

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

@ -1,5 +1,4 @@
<script lang="ts" setup>
import GithubButton from 'vue-github-button'
import {
computed,
message,
@ -79,7 +78,7 @@ onMounted(() => {
</script>
<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">
<div
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>
<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="isMounted" class="flex flex-row justify-between pt-1 truncate">
<div class="flex flex-wrap mb-1">
<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 v-else-if="appInfo.ee" class="text-gray-500 text-xs pl-3 mt-1">© 2023 NocoDB. Inc</div>
<div v-else class="flex flex-row w-full justify-between pt-0.5 truncate">
<GeneralJoinCloud />
</div>
</div>
</template>

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

@ -77,7 +77,7 @@ const tempTitle = ref('')
const activeBaseId = ref('')
const isErdModalOpen = ref<boolean>(false)
const isErdModalOpen = ref<Boolean>(false)
const { t } = useI18n()
@ -116,7 +116,7 @@ const showBaseOption = computed(() => {
return ['airtableImport', 'csvImport', 'jsonImport', 'excelImport'].some((permission) => isUIAllowed(permission))
})
function enableEditMode() {
const enableEditMode = () => {
editMode.value = true
tempTitle.value = base.value.title!
nextTick(() => {
@ -126,7 +126,7 @@ function enableEditMode() {
})
}
async function updateProjectTitle() {
const updateProjectTitle = async () => {
if (!tempTitle.value) return
try {
@ -146,7 +146,7 @@ async function updateProjectTitle() {
const { copy } = useCopy(true)
async function copyProjectInfo() {
const copyProjectInfo = async () => {
try {
if (
await copy(
@ -168,7 +168,7 @@ defineExpose({
enableEditMode,
})
async function setIcon(icon: string, base: BaseType) {
const setIcon = async (icon: string, base: BaseType) => {
try {
const meta = {
...((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) {
return
}
@ -348,17 +348,17 @@ onKeyStroke('Escape', () => {
const isDuplicateDlgOpen = ref(false)
const selectedProjectToDuplicate = ref()
function duplicateProject(base: BaseType) {
const duplicateProject = (base: BaseType) => {
selectedProjectToDuplicate.value = base
isDuplicateDlgOpen.value = true
}
function tableDelete() {
const tableDelete = () => {
isTableDeleteDialogVisible.value = true
$e('c:table:delete')
}
function projectDelete() {
const projectDelete = () => {
isProjectDeleteDialogVisible.value = true
$e('c:project:delete')
}
@ -424,15 +424,18 @@ function projectDelete() {
@keyup.esc="updateProjectTitle"
@blur="updateProjectTitle"
/>
<span
<NcTooltip
v-else
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen }"
@click="onProjectClick(base)"
show-on-truncate-only
>
{{ base.title }}
</span>
<template #title>{{ base.title }}</template>
<span @click="onProjectClick(base)">
{{ base.title }}
</span>
</NcTooltip>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(base)"></div>
<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}`]"
:data-active="openedTableId === table.id"
>
<GeneralTooltip
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"
<div
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="{
'hover:bg-gray-200': openedTableId !== table.id,
'pl-12 xs:(pl-14)': sourceIndex !== 0,
'pl-6.5': sourceIndex === 0,
'!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
v-e="['a:table:open']"
class="table-context flex items-center gap-1 h-full"
:data-testid="`nc-tbl-side-node-${table.title}`"
@contextmenu="setMenuContext('table', table)"
@click="onOpenTable"
>
<div class="flex flex-row h-full items-center">
<NcButton
v-e="['c:table:toggle-expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand"
@click.stop="onExpand"
<div class="flex flex-row h-full items-center">
<NcButton
v-e="['c:table:toggle-expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand"
@click.stop="onExpand"
>
<GeneralLoader
v-if="table.isViewsLoading"
class="flex w-4 h-4 !text-gray-600 !mt-0.75"
:class="{
'!visible': !isExpanded,
}"
/>
<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
v-if="table.isViewsLoading"
class="flex w-4 h-4 !text-gray-600 !mt-0.75"
:class="{
'!visible': !isExpanded,
}"
/>
<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
:key="table.meta?.icon"
:emoji="table.meta?.icon"
size="small"
:readonly="!canUserEditEmote || isMobileMode"
@emoji-selected="setIcon($event, table)"
>
<LazyGeneralEmojiPicker
:key="table.meta?.icon"
:emoji="table.meta?.icon"
size="small"
:readonly="!canUserEditEmote || isMobileMode"
@emoji-selected="setIcon($event, table)"
>
<template #default>
<NcTooltip class="flex" placement="topLeft" hide-on-click :disabled="!canUserEditEmote">
<template #title>
{{ $t('general.changeIcon') }}
</template>
<component
:is="iconMap.table"
v-if="table.type === 'table'"
class="flex w-5 !text-gray-500 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id,
}"
/>
<MdiEye
v-else
class="flex w-5 !text-gray-500 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id,
}"
/>
</NcTooltip>
</template>
</LazyGeneralEmojiPicker>
</div>
<template #default>
<NcTooltip class="flex" placement="topLeft" hide-on-click :disabled="!canUserEditEmote">
<template #title>
{{ $t('general.changeIcon') }}
</template>
<component
:is="iconMap.table"
v-if="table.type === 'table'"
class="flex w-5 !text-gray-500 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id,
}"
/>
<MdiEye
v-else
class="flex w-5 !text-gray-500 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id,
}"
/>
</NcTooltip>
</template>
</LazyGeneralEmojiPicker>
</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
class="nc-tbl-title nc-sidebar-node-title text-ellipsis overflow-hidden select-none"
:class="{
'text-black !font-medium': isTableOpened,
}"
@ -247,73 +245,73 @@ const isTableOpened = computed(() => {
>
{{ table.title }}
</span>
<div class="flex flex-grow h-full"></div>
<div class="flex flex-row items-center">
<div
v-if="
!isSharedBase &&
(isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
"
v-e="['c:table:option']"
>
<NcDropdown :trigger="['click']" class="nc-sidebar-node-btn" @click.stop>
<MdiDotsHorizontal
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)"
/>
<template #overlay>
<NcMenu>
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: baseRole })"
:data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, base.sources[sourceIndex].id)"
>
<div v-e="['c:table:rename']" class="flex gap-2 items-center">
<GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="
isUIAllowed('tableDuplicate') &&
base.sources?.[sourceIndex] &&
(base.sources[sourceIndex].is_meta || base.sources[sourceIndex].is_local)
"
:data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)"
>
<div v-e="['c:table:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: baseRole })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50"
@click="isTableDeleteDialogVisible = true"
>
<div v-e="['c:table:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }}
</div>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</div>
</NcTooltip>
<div class="flex flex-grow h-full"></div>
<div class="flex flex-row items-center">
<div
v-if="
!isSharedBase && (isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
"
v-e="['c:table:option']"
>
<NcDropdown :trigger="['click']" class="nc-sidebar-node-btn" @click.stop>
<MdiDotsHorizontal
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)"
/>
<template #overlay>
<NcMenu>
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: baseRole })"
:data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, base.sources[sourceIndex].id)"
>
<div v-e="['c:table:rename']" class="flex gap-2 items-center">
<GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="
isUIAllowed('tableDuplicate') &&
base.sources?.[sourceIndex] &&
(base.sources[sourceIndex].is_meta || base.sources[sourceIndex].is_local)
"
:data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)"
>
<div v-e="['c:table:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: baseRole })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50"
@click="isTableDeleteDialogVisible = true"
>
<div v-e="['c:table:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }}
</div>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</div>
</div>
<DlgTableDelete
v-if="table.id && base?.id"
v-model:visible="isTableDeleteDialogVisible"
:table-id="table.id"
:base-id="base.id"
/>
</GeneralTooltip>
</div>
<DlgTableDelete
v-if="table.id && base?.id"
v-model:visible="isTableDeleteDialogVisible"
:table-id="table.id"
:base-id="base.id"
/>
<DashboardTreeViewViewsList v-if="isExpanded" :table-id="table.id" :base-id="base.id" />
</div>
</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()
/** Rename a view */
@ -232,19 +242,18 @@ watch(isDropdownOpen, async () => {
@blur="onRename"
@keydown.stop="onKeyDown($event)"
/>
<div
v-else
class="nc-sidebar-node-title text-ellipsis overflow-hidden select-none w-full"
data-testid="sidebar-view-title"
:class="{
'font-medium': activeView?.id === vModel.id,
}"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ vModel.alias || vModel.title }}
</div>
<NcTooltip v-else class="nc-sidebar-node-title text-ellipsis overflow-hidden select-none w-full" show-on-truncate-only>
<template #title> {{ vModel.alias || vModel.title }}</template>
<div
data-testid="sidebar-view-title"
:class="{
'font-medium': activeView?.id === vModel.id,
}"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ vModel.alias || vModel.title }}
</div>
</NcTooltip>
<div class="flex-1" />
<template v-if="!isEditing && !isLocked && isUIAllowed('viewCreateOrEdit')">
@ -269,7 +278,7 @@ watch(isDropdownOpen, async () => {
:table="table"
in-sidebar
@close-modal="isDropdownOpen = false"
@rename="onRename"
@rename="onRenameMenuClick"
@delete="onDelete"
/>
</template>

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

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

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

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

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 w-full p-3 border-1 border-gray-100 rounded-md">
<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
v-e="['c:share:base:enable:toggle']"
: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 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" :style="{ fontWeight: 500 }">{{ $t('activity.enabledPublicViewing') }}</div>
<div class="text-gray-900 font-medium">{{ $t('activity.enabledPublicViewing') }}</div>
<a-switch
v-e="['c:share:view:enable:toggle']"
data-testid="share-view-toggle"

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

@ -54,7 +54,7 @@ watch(
</template>
<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="[
`nc-erd-table-node-${table.table_name}`,
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>
import { iconMap } from '#imports'
</script>
<script lang="ts" setup></script>
<template>
<a
v-e="['c:navbar:join-cloud']"
class="flex !no-underline"
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"
<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">
<a
v-e="['c:navbar:join-cloud']"
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"
>
<component :is="iconMap.cloud" class="mt-[1px] text-black font-bold" />
<div class="px-1 text-xs font-bold text-gray-800">{{ $t('general.join') }}</div>
</div>
<div
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="px-1 text-xs font-semibold group-hover:text-[#0a69da] text-gray-900">NocoDB Cloud</div>
</div>
</a>
<div class="px-1 text-gray-500 prose-sm" style="line-height: 1.3125rem">Try NocoDB Cloud</div>
</a>
<a-tooltip overlay-class-name="nc-join-cloud-tooltip">
<template #title>
<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="self-stretch justify-start items-center gap-3 inline-flex">
<div class="text-slate-800 text-lg font-semibold leading-9">NocoDB Cloud</div>
<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>
<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>
<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 />
</a-menu>
</template>

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

@ -25,7 +25,7 @@ const overlayClassName = toRef(props, 'overlayClassName')
const autoClose = computed(() => props.autoClose)
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) {
className += ` ${overlayClassName.value}`
}

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

@ -6,17 +6,19 @@ const props = withDefaults(
size?: 'small' | 'medium' | 'large'
destroyOnClose?: boolean
maskClosable?: boolean
wrapClassName?: string
}>(),
{
size: 'medium',
destroyOnClose: true,
maskClosable: true,
wrapClassName: '',
},
)
const emits = defineEmits(['update:visible'])
const { width: propWidth, destroyOnClose, maskClosable } = props
const { width: propWidth, destroyOnClose, maskClosable, wrapClassName: _wrapClassName } = props
const { isMobileMode } = useGlobal()
@ -64,6 +66,14 @@ const height = computed(() => {
return 'auto'
})
const newWrapClassName = computed(() => {
let className = 'nc-modal-wrapper'
if (_wrapClassName) {
className += ` ${_wrapClassName}`
}
return className
})
const visible = useVModel(props, 'visible', emits)
const slots = useSlots()
@ -76,7 +86,7 @@ const slots = useSlots()
:width="width"
:centered="true"
:closable="false"
wrap-class-name="nc-modal-wrapper"
:wrap-class-name="newWrapClassName"
:footer="null"
:mask-closable="maskClosable"
: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 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)
@ -37,7 +43,7 @@ const onChange = (value: string) => {
v-model:value="vModel"
:placeholder="placeholder"
class="nc-select"
:dropdown-class-name="dropdownClassName ? `nc-select-dropdown ${dropdownClassName}` : 'nc-select-dropdown'"
:dropdown-class-name="dropdownClassName"
:show-search="showSearch"
:filter-option="filterOption"
:dropdown-match-select-width="dropdownMatchSelectWidth"

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

@ -11,6 +11,7 @@ interface Props {
// force disable tooltip
disabled?: boolean
placement?: TooltipPlacement | undefined
showOnTruncateOnly?: boolean
hideOnClick?: boolean
overlayClassName?: string
}
@ -20,6 +21,7 @@ const props = defineProps<Props>()
const modifierKey = computed(() => props.modifierKey)
const tooltipStyle = computed(() => props.tooltipStyle)
const disabled = computed(() => props.disabled)
const showOnTruncateOnly = computed(() => props.showOnTruncateOnly)
const hideOnClick = computed(() => props.hideOnClick)
const placement = computed(() => props.placement ?? 'top')
@ -65,6 +67,15 @@ onKeyStroke(
)
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) {
showTooltip.value = false
return

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

@ -177,7 +177,7 @@ onMounted(async () => {
</div>
<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-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 date-joined-grid">{{ $t('title.dateJoined') }}</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>
import { useTitle } from '@vueuse/core'
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 { activeWorkspace, workspaceUserCount } = storeToRefs(useWorkspace())
@ -26,6 +32,23 @@ const { isMobileMode } = useGlobal()
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(
() => route.value.query?.page,
(newVal, oldVal) => {
@ -57,6 +80,16 @@ watch(projectPageTab, () => {
}
})
watch(
() => route.value.params.baseId,
(newVal, oldVal) => {
if (newVal && oldVal === undefined) {
updateBaseUserCount()
}
},
{ immediate: true },
)
watch(
() => openedProject.value?.title,
() => {
@ -68,16 +101,19 @@ watch(
<template>
<div class="h-full nc-base-view">
<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 }"
>
<div class="flex flex-row items-center gap-x-3">
<GeneralOpenLeftSidebarBtn />
<div class="flex flex-row items-center h-full gap-x-2.5">
<GeneralProjectIcon :type="openedProject?.type" />
<div class="flex font-medium text-sm capitalize">
{{ openedProject?.title }}
</div>
<NcTooltip class="flex font-medium text-sm capitalize truncate max-w-150" show-on-truncate-only>
<template #title> {{ openedProject?.title }}</template>
<span class="truncate">
{{ openedProject?.title }}
</span>
</NcTooltip>
</div>
</div>
<LazyGeneralShareProject />
@ -116,14 +152,14 @@ watch(
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" />
<div>{{ $t('labels.members') }}</div>
<div
v-if="workspaceUserCount"
v-if="userCount"
class="tab-info"
:class="{
'bg-primary-selected': projectPageTab === 'data-source',
'bg-gray-50': projectPageTab !== 'data-source',
'bg-primary-selected': projectPageTab === 'collaborator',
'bg-gray-50': projectPageTab !== 'collaborator',
}"
>
{{ workspaceUserCount }}
{{ userCount }}
</div>
</div>
</template>

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

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

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

@ -1,5 +1,14 @@
<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()
@ -9,8 +18,6 @@ const { isViewsLoading } = storeToRefs(useViewsStore())
const { isMobileMode } = useGlobal()
const { isUIAllowed } = useRoles()
const { allowCSVDownload } = useSharedView()
</script>

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

@ -73,7 +73,7 @@ vModel.value.au = !!vModel.value.au */
</div>
<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">
{{ type }}
</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-[600px]': formState.uidt === UITypes.Formula && !props.embedMode,
'!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"
@click.stop
@ -271,7 +271,7 @@ if (props.fromTableExplorer) {
show-search
class="nc-column-type-input !rounded"
:disabled="isKanban || readOnly"
dropdown-class-name="nc-dropdown-column-type "
dropdown-class-name="nc-dropdown-column-type border-1 border-gray-200"
@change="onUidtOrIdTypeChange"
@dblclick="showDeprecated = !showDeprecated"
>
@ -312,6 +312,7 @@ if (props.fromTableExplorer) {
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" />
<SmartsheetColumnSelectOptions
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"
>
<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">
<GeneralTableIcon :meta="table" class="text-gray-500" />
</div>
<span class="overflow-ellipsis min-w-0 shrink-1">{{ table.title }}</span>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>{{ table.title }}</template>
<span>{{ table.title }}</span>
</NcTooltip>
</div>
</a-select-option>
</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">
import { precisions, useVModel } from '#imports'
import { useVModel } from '#imports'
const props = defineProps<{
value: any
isEdit: boolean
}>()
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
const validators = {}
const { setAdditionalValidations } = useColumnCreateStoreOrThrow()
setAdditionalValidations({
...validators,
})
// set default value
vModel.value.meta = {
is_progress: false,
...vModel.value.meta,
}
</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 class="flex flex-col">
<div>
<a-checkbox v-if="vModel.meta" v-model:checked="vModel.meta.is_progress" class="ml-1 mb-1">
<span class="text-[10px] text-gray-600">Display as progress</span>
</a-checkbox>
</div>
</div>
</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
v-model:visible="colorMenus[index]"
: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>
<LazyGeneralColorPicker

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

@ -769,8 +769,8 @@ const onFieldOptionUpdate = () => {
</NcTooltip>
</div>
</div>
<div class="flex flex-row rounded-lg border-1 border-gray-200">
<div ref="fieldsListWrapperDomRef" class="nc-scrollbar-md !overflow-auto w-full flex-grow-1 nc-fields-height">
<div class="flex flex-row rounded-lg border-1 overflow-clip border-gray-200">
<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)">
<template #item="{ element: field }">
<div
@ -814,14 +814,18 @@ const onFieldOptionUpdate = () => {
'text-brand-500': compareCols(field, activeField),
}"
/>
<span
<NcTooltip
:class="{
'text-brand-500': compareCols(field, activeField),
}"
class="truncate max-w-64"
class="truncate flex-1"
show-on-truncate-only
>
{{ fieldState(field)?.title || field.title }}
</span>
<template #title> {{ fieldState(field)?.title || field.title }} </template>
<span>
{{ fieldState(field)?.title || field.title }}
</span>
</NcTooltip>
</div>
<div class="flex items-center justify-end gap-1">
<div class="flex items-center">
@ -970,13 +974,18 @@ const onFieldOptionUpdate = () => {
'text-brand-500': compareCols(displayColumn, activeField),
}"
/>
<span
<NcTooltip
class="truncate flex-1"
:class="{
'text-brand-500': compareCols(displayColumn, activeField),
}"
show-on-truncate-only
>
{{ fieldState(displayColumn)?.title || displayColumn.title }}
</span>
<template #title> {{ fieldState(displayColumn)?.title || displayColumn.title }} </template>
<span>
{{ fieldState(displayColumn)?.title || displayColumn.title }}
</span>
</NcTooltip>
</div>
<div class="flex items-center justify-end gap-1">
<div class="flex items-center">
@ -1072,7 +1081,7 @@ const onFieldOptionUpdate = () => {
</Draggable>
</div>
<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
v-if="activeField"
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">
<div
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>
<a-skeleton-input
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
size="small"
/>
@ -658,7 +658,7 @@ export default {
<SmartsheetDivDataCell
v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="bg-white rounded-lg !w-[20rem] !xs:w-full border-1 border-gray-200 overflow-hidden px-1 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="{
'!bg-gray-50 !px-0 !select-text': isReadOnlyVirtualCell(col),
}"
@ -720,11 +720,11 @@ export default {
<template v-if="isLoading">
<div
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>
<a-skeleton-input
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
size="small"
/>
@ -733,7 +733,7 @@ export default {
<LazySmartsheetDivDataCell
v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="!bg-white rounded-lg !w-[20rem] border-1 overflow-hidden border-gray-200 px-1 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
v-if="isVirtualCol(col)"

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

@ -1804,7 +1804,7 @@ onKeyStroke('ArrowDown', onDown)
<template #overlay>
<div class="relative overflow-visible min-h-17 w-10">
<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)"
:class="{
'-left-44': !isAddNewRecordGridMode,
@ -1847,7 +1847,7 @@ onKeyStroke('ArrowDown', onDown)
</div>
</template>
<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>
</a-dropdown-button>
</div>

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

@ -90,18 +90,23 @@ const onClick = (e: Event) => {
'self-start': isForm || isSurveyForm,
}"
/>
<div
<NcTooltip
v-if="column"
class="name pl-1"
:class="{
'cursor-pointer pt-0.25': !isForm && isUIAllowed('fieldEdit') && !hideMenu && !isExpandedForm,
'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 }}
</div>
<template #title> {{ column.title }} </template>
<span :data-test-id="column.title">
{{ column.title }}
</span>
</NcTooltip>
<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" />
</div>
<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">
<div class="nc-column-edit nc-header-menu-item">
<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) {
return ''
}
if (isHm(column.value)) {
return `'${tableTile.value}' ${t('labels.hasMany')} '${relatedTableTitle.value}'`
} else if (isMm(column.value)) {
@ -110,7 +111,7 @@ const tooltipMsg = computed(() => {
} else if (isRollup(column.value)) {
return `'${childColumn.value.title}' of '${relatedTableTitle.value}' (${childColumn.value.uidt})`
}
return ''
return column?.value?.title || ''
})
const columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null)
@ -153,14 +154,14 @@ const openDropDown = (e: Event) => {
>
<LazySmartsheetHeaderVirtualCellIcon v-if="column && !props.hideIcon" />
<a-tooltip placement="bottom">
<template v-if="!isForm && !isExpandedForm" #title>
<NcTooltip placement="bottom" class="truncate name pl-1" show-on-truncate-only>
<template #title>
{{ tooltipMsg }}
</template>
<span class="name truncate pl-1" :class="{ truncate: !isForm }" :data-test-id="column.title">
<span :data-test-id="column.title">
{{ column.title }}
</span>
</a-tooltip>
</NcTooltip>
<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">
<input ref="inputRef" v-model="search" class="w-full focus:outline-none" :placeholder="$t('msg.selectFieldToSort')" />
</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-for="(option, index) in options"
@ -104,9 +104,12 @@ const onArrowUp = () => {
@click="onClick(option)"
>
<SmartsheetHeaderIcon :column="option" />
<div>
{{ option.title }}
</div>
<NcTooltip class="truncate" show-on-truncate-only>
<template #title> {{ option.title }}</template>
<template #default>
{{ option.title }}
</template>
</NcTooltip>
</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">
<div class="flex gap-2 items-center items-center h-full">
<component :is="option.icon" class="min-w-5 !mx-0" />
<div
class="min-w-0 text-ellipsis overflow-hidden select-none"
<NcTooltip
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
class="max-w-[15rem] truncate select-none"
show-on-truncate-only
>
{{ option.label }}
</div>
<template #title> {{ option.label }}</template>
<template #default>
{{ option.label }}
</template>
</NcTooltip>
</div>
</a-select-option>
</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" />
<div
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="
() => {
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])" />
<NcTooltip :disabled="field.title.length < 30">
<template #title>
{{ field.title }}
</template>
<span class="mx-0.65 break-all line-clamp-1">{{ field.title }}</span>
</NcTooltip>
</div>
<component :is="getIcon(metaColumnById[field.fk_column_id])" />
<NcTooltip show-on-truncate-only class="flex-1 px-1 truncate">
<template #title>
{{ field.title }}
</template>
<template #default>{{ field.title }}</template>
</NcTooltip>
<NcSwitch v-e="['a:fields:show-hide']" :checked="field.show" :disabled="field.isViewEssentialField" />
</div>
@ -399,7 +397,7 @@ useMenuCloseOnEsc(open)
<div
v-if="gridDisplayValueField && filteredFieldList[0].title.toLowerCase().includes(filterQuery.toLowerCase())"
: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="{
'rounded-t-lg': filteredFieldList.length > 1,
'rounded-lg': filteredFieldList.length === 1,
@ -407,22 +405,13 @@ useMenuCloseOnEsc(open)
:data-testid="`nc-fields-menu-${gridDisplayValueField.title}`"
@click.stop
>
<div class="flex flex-row items-center justify-between w-full">
<div class="flex items">
<a-tooltip placement="bottom">
<template #title>
<span class="text-sm">$t('title.displayValue') </span>
</template>
</a-tooltip>
<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>
<component :is="getIcon(metaColumnById[filteredFieldList[0].fk_column_id as string])" />
<NcTooltip show-on-truncate-only class="px-1 flex-1 truncate">
<template #title>{{ filteredFieldList[0].title }}</template>
<template #default>{{ filteredFieldList[0].title }}</template>
</NcTooltip>
<NcSwitch v-e="['a:fields:show-hide']" :checked="true" :disabled="true" />
</div>
</template>
</Draggable>

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

@ -173,7 +173,7 @@ function openDeleteDialog() {
>
<div
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="{
'max-w-2/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,
}"
>
<span
class="truncate xs:pl-1.25 text-inherit"
:class="{
'max-w-28/100': !isMobileMode,
}"
>
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</span>
<NcTooltip class="truncate xs:pl-1.25 flex-1 text-inherit" show-on-truncate-only>
<template #title>{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }} </template>
<span
:class="{
'max-w-28/100': !isMobileMode,
}"
>
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</span>
</NcTooltip>
<GeneralIcon icon="arrowDown" class="ml-1" />
</div>
<template #overlay>

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

@ -65,7 +65,7 @@ useMenuCloseOnEsc(open)
</div>
<template #overlay>
<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"
>
<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">
<div class="text-[0.75rem] flex items-center -ml-1 gap-2">
<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>
</a-select-option>
</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">
<SmartsheetToolbarFieldListAutoCompleteDropdown
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"
is-sort
@click.stop

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

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

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

@ -6,6 +6,7 @@ const useAttachment = () => {
const getPossibleAttachmentSrc = (item: Record<string, any>) => {
const res: string[] = []
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?.signedUrl) res.push(item.signedUrl)
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 { bases } = storeToRefs(useBases())
const { $api } = useNuxtApp()
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]),
)
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 additionalValidations = ref<ValidationsObj>({})
@ -136,7 +128,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
})
},
},
fieldLengthValidator(source.value?.type || ClientType.MYSQL),
fieldLengthValidator(),
],
uidt: [
{

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

@ -275,9 +275,9 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
}
// reload view columns when active view changes
// or when columns count changes(delete/add)
// or when columns changes(delete/add)
watch(
[() => view?.value?.id, () => meta.value?.columns?.length],
[() => view?.value?.id, () => meta.value?.columns],
async ([newViewId]) => {
// reload only if view belongs to current table
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] = []
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}`)
.replace(/[` ~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/g, '_')
.trim()
@ -70,6 +71,7 @@ export default class CSVTemplateAdapter {
this.distinctValues[columnIdx] = new Set<string>()
this.columnValues[columnIdx] = []
tableObj.columns.push({
title,
column_name: cn,
ref_column_name: cn,
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++) {
const title = (
(this.config.firstRowAsHeaders && rows[0] && rows[0][col] && rows[0][col].toString().trim()) ||
`Field ${col + 1}`
).trim()
let cn: string = (
(this.config.firstRowAsHeaders && rows[0] && rows[0][col] && rows[0][col].toString().trim()) ||
`field_${col + 1}`
@ -126,6 +130,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
columnNamePrefixRef[cn] = 0
const column: Record<string, any> = {
title,
column_name: cn,
ref_column_name: cn,
meta: {},

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -581,6 +581,7 @@
"childTable": "Bảng con",
"childColumn": "Cột trẻ con.",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record",
"links": "Links",
"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": "子表",
"childColumn": "子欄",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "連結到另一個紀錄",
"links": "Links",
"onUpdate": "更新時",

20
packages/nc-gui/package.json

@ -73,7 +73,7 @@
"rfdc": "^1.3.0",
"showdown": "^2.1.0",
"socket.io-client": "^4.7.2",
"sortablejs": "^1.15.0",
"sortablejs": "^1.15.1",
"splitpanes": "^3.1.5",
"tinycolor2": "^1.4.2",
"unique-names-generator": "^4.7.1",
@ -93,10 +93,10 @@
"xlsx": "^0.18.5",
"@tiptap/extension-link": "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/pm": "^2.1.12",
"@tiptap/starter-kit": "^2.1.12",
"@tiptap/pm": "^2.1.13",
"@tiptap/starter-kit": "^2.1.13",
"marked": "^4.3.0",
"turndown": "^7.1.2",
"@tiptap/vue-3": "2.0.4"
@ -106,7 +106,7 @@
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@iconify-json/ant-design": "^1.1.12",
"@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/clarity": "^1.1.11",
"@iconify-json/eva": "^1.1.9",
@ -114,18 +114,18 @@
"@iconify-json/ion": "^1.1.14",
"@iconify-json/la": "^1.1.7",
"@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/mdi": "^1.1.57",
"@iconify-json/mdi": "^1.1.58",
"@iconify-json/mi": "^1.1.7",
"@iconify-json/ph": "^1.1.8",
"@iconify-json/ri": "^1.1.14",
"@iconify-json/simple-icons": "^1.1.81",
"@iconify-json/ri": "^1.1.15",
"@iconify-json/simple-icons": "^1.1.82",
"@iconify-json/system-uicons": "^1.1.11",
"@iconify-json/tabler": "^1.1.100",
"@iconify-json/vscode-icons": "^1.1.31",
"@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/dagre": "^0.7.52",
"@types/file-saver": "^2.0.7",

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

@ -184,7 +184,7 @@ function navigateForgotPassword() {
</a>
</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') }}
<nuxt-link @click="navigateSignUp">{{ $t('general.signUp') }}</nuxt-link>
</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 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 route = router.currentRoute
@ -294,6 +295,7 @@ export const useBases = defineStore('basesStore', () => {
return {
bases,
basesList,
baseUserCount,
loadProjects,
loadProject,
getSqlUi,

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

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

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

@ -1,6 +1,6 @@
{
"name": "nc-lib-gui",
"version": "0.202.8",
"version": "0.202.9",
"description": "NocoDB GUI",
"author": {
"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']
---
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
- [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']
---
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
| 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 |
## CONCAT
The CONCAT function concatenates one or more strings into a single 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

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']
---
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 |
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **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. |
| **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')` |
| | | `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')` |
| | | `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. |
| | | `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. |
| **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. |
| | | `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 |
## DATETIME_DIFF
The DATETIME_DIFF function calculates the difference between two dates in various units.
#### Syntax
```plaintext
DATETIME_DIFF(date1, date2, ["milliseconds" | "ms" | "seconds" | "s" | "minutes" | "m" | "hours" | "h" | "days" | "d" | "weeks" | "w" | "months" | "M" | "quarters" | "Q" | "years" | "y"])
```
#### Sample
```plaintext
DATETIME_DIFF("2022/10/14", "2022/10/15", "seconds") => -86400
```
#### 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
- [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.'
tags: ['Fields', 'Field types', 'Formula']
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 |
|------------|------------------------------------------------|------------------------------------------------|-------------------------------------------------------------|
| **IF** | `IF(expr, successCase, elseCase)` | `IF({field} > 1, Value1, Value2)` | successCase if `expr` evaluates to TRUE, elseCase otherwise |
| **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 |
#### Syntax
```markdown
IF(expr, successCase, elseCase)
```
#### 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
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