Browse Source

Merge pull request #8024 from nocodb/develop

pull/8025/head 0.205.0
github-actions[bot] 8 months ago committed by GitHub
parent
commit
99294e4342
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      packages/nc-gui/components/account/UsersModal.vue
  2. 3
      packages/nc-gui/components/cell/Currency.vue
  3. 12
      packages/nc-gui/components/cell/Email.vue
  4. 5
      packages/nc-gui/components/cell/MultiSelect.vue
  5. 9
      packages/nc-gui/components/cell/Rating.vue
  6. 107
      packages/nc-gui/components/cell/RichText.vue
  7. 22
      packages/nc-gui/components/cell/RichText/LinkOptions.vue
  8. 311
      packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue
  9. 12
      packages/nc-gui/components/cell/SingleSelect.vue
  10. 34
      packages/nc-gui/components/cell/TextArea.vue
  11. 16
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  12. 52
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  13. 7
      packages/nc-gui/components/dlg/TableCreate.vue
  14. 8
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  15. 6
      packages/nc-gui/components/general/CopyUrl.vue
  16. 2
      packages/nc-gui/components/general/ShareProject.vue
  17. 2
      packages/nc-gui/components/nc/ErrorBoundary.vue
  18. 14
      packages/nc-gui/components/project/ShareBaseDlg.vue
  19. 3
      packages/nc-gui/components/smartsheet/Cell.vue
  20. 1913
      packages/nc-gui/components/smartsheet/Form.vue
  21. 19
      packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue
  22. 53
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  23. 16
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  24. 7
      packages/nc-gui/components/smartsheet/calendar/RecordCard.vue
  25. 15
      packages/nc-gui/components/smartsheet/calendar/SideMenu.vue
  26. 7
      packages/nc-gui/components/smartsheet/calendar/VRecordCard.vue
  27. 15
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  28. 54
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  29. 21
      packages/nc-gui/components/smartsheet/calendar/index.vue
  30. 2
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  31. 2
      packages/nc-gui/components/smartsheet/column/RatingOptions.vue
  32. 2
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  33. 2
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  34. 248
      packages/nc-gui/components/smartsheet/form/FieldMenu.vue
  35. 94
      packages/nc-gui/components/smartsheet/form/Layout.vue
  36. 33
      packages/nc-gui/components/smartsheet/form/LimitOptions.vue
  37. 8
      packages/nc-gui/components/smartsheet/header/DeleteColumnModal.vue
  38. 4
      packages/nc-gui/components/smartsheet/toolbar/CalendarMode.vue
  39. 18
      packages/nc-gui/components/smartsheet/toolbar/CalendarRange.vue
  40. 20
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  41. 6
      packages/nc-gui/components/tabs/Smartsheet.vue
  42. 21
      packages/nc-gui/components/virtual-cell/Lookup.vue
  43. 15
      packages/nc-gui/components/virtual-cell/components/LinkedItems.vue
  44. 1
      packages/nc-gui/components/virtual-cell/components/UnLinkedItems.vue
  45. 13
      packages/nc-gui/components/workspace/InviteSection.vue
  46. 18
      packages/nc-gui/composables/useCalendarViewStore.ts
  47. 2
      packages/nc-gui/composables/useCommandPalette/commands.ts
  48. 7
      packages/nc-gui/composables/useLTARStore.ts
  49. 6
      packages/nc-gui/composables/useLoadingIndicator/index.ts
  50. 7
      packages/nc-gui/composables/useMultiSelect/convertCellData.ts
  51. 11
      packages/nc-gui/composables/useSharedFormViewStore.ts
  52. 3
      packages/nc-gui/composables/useTableNew.ts
  53. 2
      packages/nc-gui/composables/useViewColumns.ts
  54. 10
      packages/nc-gui/helpers/parsers/parserHelpers.ts
  55. 4
      packages/nc-gui/lang/ar.json
  56. 4
      packages/nc-gui/lang/bn_IN.json
  57. 4
      packages/nc-gui/lang/cs.json
  58. 4
      packages/nc-gui/lang/da.json
  59. 4
      packages/nc-gui/lang/de.json
  60. 10
      packages/nc-gui/lang/en.json
  61. 4
      packages/nc-gui/lang/es.json
  62. 4
      packages/nc-gui/lang/eu.json
  63. 4
      packages/nc-gui/lang/fa.json
  64. 4
      packages/nc-gui/lang/fi.json
  65. 4
      packages/nc-gui/lang/fr.json
  66. 4
      packages/nc-gui/lang/he.json
  67. 4
      packages/nc-gui/lang/hi.json
  68. 4
      packages/nc-gui/lang/hr.json
  69. 4
      packages/nc-gui/lang/id.json
  70. 4
      packages/nc-gui/lang/it.json
  71. 4
      packages/nc-gui/lang/ja.json
  72. 4
      packages/nc-gui/lang/ko.json
  73. 4
      packages/nc-gui/lang/lv.json
  74. 4
      packages/nc-gui/lang/nl.json
  75. 4
      packages/nc-gui/lang/no.json
  76. 8
      packages/nc-gui/lang/pl.json
  77. 4
      packages/nc-gui/lang/pt.json
  78. 4
      packages/nc-gui/lang/pt_BR.json
  79. 4
      packages/nc-gui/lang/ru.json
  80. 4
      packages/nc-gui/lang/sk.json
  81. 4
      packages/nc-gui/lang/sl.json
  82. 28
      packages/nc-gui/lang/sv.json
  83. 4
      packages/nc-gui/lang/th.json
  84. 4
      packages/nc-gui/lang/tr.json
  85. 30
      packages/nc-gui/lang/uk.json
  86. 4
      packages/nc-gui/lang/vi.json
  87. 216
      packages/nc-gui/lang/zh-Hans.json
  88. 4
      packages/nc-gui/lang/zh-Hant.json
  89. 17
      packages/nc-gui/lib/enums.ts
  90. 10
      packages/nc-gui/package.json
  91. 18
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
  92. 5
      packages/nc-gui/store/base.ts
  93. 13
      packages/nc-gui/store/sidebar.ts
  94. 12
      packages/nc-gui/utils/baseCreateUtils.ts
  95. 37
      packages/nc-gui/utils/errorUtils.ts
  96. 76
      packages/noco-docs/docs/070.fields/040.field-types/060.formula/040.date-functions.md
  97. 24
      packages/noco-docs/docs/070.fields/040.field-types/060.formula/060.generic-functions.md
  98. 1
      packages/noco-docs/docs/090.views/010.views-overview.md
  99. 5
      packages/noco-docs/docs/090.views/020.create-view.md
  100. 145
      packages/noco-docs/docs/090.views/040.view-types/030.form.md
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -16,6 +16,7 @@ import {
useI18n,
useNuxtApp,
} from '#imports'
import { extractEmail } from '~/helpers/parsers/parserHelpers'
interface Props {
show: boolean
@ -99,6 +100,12 @@ const clickInviteMore = () => {
}
const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
const onPaste = (e: ClipboardEvent) => {
const pastedText = e.clipboardData?.getData('text') ?? ''
usersData.value.emails = extractEmail(pastedText) || pastedText
}
</script>
<template>
@ -189,6 +196,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
size="middle"
validate-trigger="onBlur"
:placeholder="$t('labels.email')"
@paste.prevent="onPaste"
/>
</a-form-item>
</div>

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

@ -112,13 +112,14 @@ onMounted(() => {
</span>
</div>
<input
v-if="!readOnly && editEnabled"
v-if="(!readOnly && editEnabled) || (isForm && !isEditColumn)"
:ref="focus"
v-model="vModel"
type="number"
class="nc-cell-field h-full text-sm border-none rounded-md py-1 outline-none focus:outline-none focus:ring-0"
:class="isForm && !isEditColumn ? 'flex flex-1' : 'w-full'"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:disabled="readOnly"
@blur="onBlur"
@keydown.enter="onKeydownEnter"
@keydown.down.stop

12
packages/nc-gui/components/cell/Email.vue

@ -11,6 +11,7 @@ import {
useI18n,
validateEmail,
} from '#imports'
import { extractEmail } from '~/helpers/parsers/parserHelpers'
interface Props {
modelValue: string | null | undefined
@ -56,6 +57,16 @@ const validEmail = computed(() => vModel.value && validateEmail(vModel.value))
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()
const onPaste = (e: ClipboardEvent) => {
const pastedText = e.clipboardData?.getData('text') ?? ''
if (parseProp(column.value.meta).validate) {
vModel.value = extractEmail(pastedText) || pastedText
} else {
vModel.value = pastedText
}
}
watch(
() => editEnabled.value,
() => {
@ -90,6 +101,7 @@ watch(
@keydown.delete.stop
@selectstart.capture.stop
@mousedown.stop
@paste.prevent="onPaste"
/>
<span v-else-if="vModel === null && showNull" class="nc-cell-field nc-null uppercase">{{ $t('general.null') }}</span>

5
packages/nc-gui/components/cell/MultiSelect.vue

@ -10,6 +10,7 @@ import {
EditColumnInj,
EditModeInj,
IsKanbanInj,
IsSurveyFormInj,
ReadonlyInj,
RowHeightInj,
computed,
@ -64,6 +65,8 @@ const isEditColumn = inject(EditColumnInj, ref(false))
const rowHeight = inject(RowHeightInj, ref(undefined))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const selectedIds = ref<string[]>([])
const aselect = ref<typeof AntSelect>()
@ -386,6 +389,8 @@ const onFocus = () => {
isFocusing.value = false
}, 250)
if (isSurveyForm.value && vModel.value?.length) return
isOpen.value = true
}
</script>

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

@ -73,11 +73,12 @@ watch(rateDomRef, () => {
<template>
<a-rate
ref="rateDomRef"
:key="ratingMeta.icon.full"
v-model:value="vModel"
:disabled="readOnly"
:count="ratingMeta.max"
:class="readOnly ? 'pointer-events-none' : ''"
:style="`color: ${ratingMeta.color}; padding: ${isExpandedFormOpen ? '0px 8px' : '0px 5px'};`"
:style="`color: ${ratingMeta.color}; padding: ${isExpandedFormOpen ? '0px 8px' : '0px 2px'};`"
@keydown="onKeyPress"
>
<template #character>
@ -89,3 +90,9 @@ watch(rateDomRef, () => {
</template>
</a-rate>
</template>
<style scoped lang="scss">
:deep(li:not(:last-child)) {
@apply mr-[1.5px];
}
</style>

107
packages/nc-gui/components/cell/RichText.vue

@ -9,21 +9,31 @@ import Underline from '@tiptap/extension-underline'
import Placeholder from '@tiptap/extension-placeholder'
import { TaskItem } from '@/helpers/dbTiptapExtensions/task-item'
import { Link } from '@/helpers/dbTiptapExtensions/links'
import { IsExpandedFormOpenInj, IsFormInj, IsGridInj, ReadonlyInj, RowHeightInj } from '#imports'
const props = defineProps<{
value?: string | null
readOnly?: boolean
syncValueChange?: boolean
showMenu?: boolean
fullMode?: boolean
isFormField?: boolean
autofocus?: boolean
placeholder?: string
renderAsText?: boolean
}>()
const emits = defineEmits(['update:value'])
import type { RichTextBubbleMenuOptions } from '#imports'
import { IsExpandedFormOpenInj, IsFormInj, IsGridInj, IsSurveyFormInj, ReadonlyInj, RowHeightInj } from '#imports'
const props = withDefaults(
defineProps<{
value?: string | null
readOnly?: boolean
syncValueChange?: boolean
showMenu?: boolean
fullMode?: boolean
isFormField?: boolean
autofocus?: boolean
placeholder?: string
renderAsText?: boolean
hiddenBubbleMenuOptions?: RichTextBubbleMenuOptions[]
}>(),
{
isFormField: false,
hiddenBubbleMenuOptions: () => [],
},
)
const emits = defineEmits(['update:value', 'focus', 'blur'])
const { isFormField, hiddenBubbleMenuOptions } = toRefs(props)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
@ -35,8 +45,12 @@ const isForm = inject(IsFormInj, ref(false))
const isGrid = inject(IsGridInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isFocused = ref(false)
const keys = useMagicKeys()
const turndownService = new TurndownService({})
turndownService.addRule('lineBreak', {
@ -115,7 +129,7 @@ const vModel = useVModel(props, 'value', emits, { defaultValue: '' })
const tiptapExtensions = [
StarterKit.configure({
heading: props.isFormField ? false : undefined,
heading: isFormField.value ? false : undefined,
}),
TaskList,
TaskItem.configure({
@ -136,16 +150,18 @@ const editor = useEditor({
.turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />'))
.replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n')
vModel.value = props.isFormField && markdown === '<br />' ? '' : markdown
vModel.value = isFormField.value && markdown === '<br />' ? '' : markdown
},
editable: !props.readOnly,
autofocus: props.autofocus,
onFocus: () => {
isFocused.value = true
emits('focus')
},
onBlur: (e) => {
if (!(e?.event?.relatedTarget as HTMLElement)?.closest('.bubble-menu, .nc-textarea-rich-editor')) {
isFocused.value = false
emits('blur')
}
},
})
@ -176,13 +192,19 @@ const setEditorContent = (contentMd: any, focusEndOfDoc?: boolean) => {
}, 100)
}
const onFocusWrapper = () => {
if (isForm.value && !isFormField.value && !props.readOnly && !keys.shift.value) {
editor.value?.chain().focus().run()
}
}
if (props.syncValueChange) {
watch([vModel, editor], () => {
setEditorContent(vModel.value)
})
}
if (props.isFormField) {
if (isFormField.value) {
watch([props, editor], () => {
if (props.readOnly) {
editor.value?.setEditable(false)
@ -197,7 +219,7 @@ watch(editorDom, () => {
setEditorContent(vModel.value, true)
if (props.isFormField) return
if ((isForm.value && !isSurveyForm.value) || isFormField.value) return
// Focus editor after editor is mounted
setTimeout(() => {
editor.value?.chain().focus().run()
@ -208,8 +230,10 @@ useEventListener(
editorDom,
'focusout',
(e: FocusEvent) => {
if (!(e?.relatedTarget as HTMLElement)?.closest('.bubble-menu, .nc-textarea-rich-editor')) {
const targetEl = e?.relatedTarget as HTMLElement
if (targetEl?.classList?.contains('tiptap') || !targetEl?.closest('.bubble-menu, .nc-textarea-rich-editor')) {
isFocused.value = false
emits('blur')
}
},
true,
@ -218,15 +242,16 @@ useEventListener(
<template>
<div
class="h-full focus:outline-none"
class="nc-rich-text h-full focus:outline-none"
:class="{
'flex flex-col flex-grow nc-rich-text-full': fullMode,
'nc-rich-text-embed flex flex-col pl-1 w-full': !fullMode,
'readonly': readOnly,
'nc-form-rich-text-field !p-0': isFormField,
'nc-form-rich-text-field !p-0 relative': isFormField,
'nc-rich-text-grid': isGrid,
}"
:tabindex="readOnlyCell || isFormField ? -1 : 0"
@focus="onFocusWrapper"
>
<div v-if="renderAsText" class="truncate">
<span v-if="editor"> {{ editor?.getText() ?? '' }}</span>
@ -234,38 +259,50 @@ useEventListener(
<template v-else>
<div
v-if="showMenu && !readOnly && !isFormField"
class="absolute top-0 right-0.5 xs:hidden"
class="absolute top-0 right-0.5"
:class="{
'max-w-[calc(100%_-_198px)] flex justify-end rounded-tr-2xl overflow-hidden': fullMode,
'flex rounded-tr-2xl overflow-hidden w-full': fullMode || isForm,
'max-w-[calc(100%_-_198px)]': fullMode,
'justify-start left-0.5': isForm,
'justify-end xs:hidden': !isForm,
}"
>
<div class="nc-longtext-scrollbar">
<div class="scrollbar-thin scrollbar-thumb-gray-200 hover:scrollbar-thumb-gray-300 scrollbar-track-transparent">
<CellRichTextSelectedBubbleMenu v-if="editor" :editor="editor" embed-mode :is-form-field="isFormField" />
</div>
</div>
<CellRichTextSelectedBubbleMenuPopup v-if="editor && !isFormField" :editor="editor" />
<CellRichTextSelectedBubbleMenuPopup v-if="editor && !isFormField && !isForm" :editor="editor" />
<CellRichTextLinkOptions v-if="editor" :editor="editor" />
<EditorContent
ref="editorDom"
:editor="editor"
class="flex flex-col nc-textarea-rich-editor w-full"
:class="{
'mt-2.5 flex-grow': fullMode,
'nc-scrollbar-md': !fullMode || (!fullMode && isExpandedFormOpen),
'scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent': !fullMode || (!fullMode && isExpandedFormOpen),
'flex-grow': isExpandedFormOpen,
[`!overflow-hidden children:line-clamp-${rowHeight}`]:
!fullMode && readOnly && rowHeight && !isExpandedFormOpen && !isForm,
}"
@keydown.alt.enter.stop
@keydown.shift.enter.stop
/>
<div v-if="isFormField && !readOnly">
<div v-if="isFormField && !readOnly" class="nc-form-field-bubble-menu-wrapper overflow-hidden">
<div
class="overflow-hidden"
:class="isFocused ? 'max-h-[50px]' : 'max-h-0'"
:style="{
transition: 'max-height 0.2s ease-in-out',
}"
>
<CellRichTextSelectedBubbleMenu v-if="editor" :editor="editor" embed-mode is-form-field />
<CellRichTextSelectedBubbleMenu
v-if="editor"
:editor="editor"
embed-mode
is-form-field
:hidden-options="hiddenBubbleMenuOptions"
/>
</div>
</div>
</template>
@ -343,7 +380,7 @@ useEventListener(
.nc-textarea-rich-editor {
.tiptap p.is-editor-empty:first-child::before {
color: #6a7184;
color: #9aa2af;
content: attr(data-placeholder);
float: left;
height: 0;
@ -429,18 +466,21 @@ useEventListener(
font-weight: 700;
font-size: 1.85rem;
margin-bottom: 0.1rem;
line-height: 36px;
}
h2 {
font-weight: 600;
font-size: 1.55rem;
margin-bottom: 0.1em;
line-height: 30px;
}
h3 {
font-weight: 600;
font-size: 1.15rem;
margin-bottom: 0.1em;
line-height: 24px;
}
blockquote {
@ -462,4 +502,9 @@ useEventListener(
height: fit-content;
}
}
.nc-form-field-bubble-menu-wrapper {
@apply absolute -bottom-9 left-1/2 z-50 rounded-lg;
transform: translateX(-50%);
box-shadow: 0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.1);
}
</style>

22
packages/nc-gui/components/cell/RichText/LinkOptions.vue

@ -158,23 +158,37 @@ const openLink = () => {
window.open(href.value, '_blank', 'noopener,noreferrer')
}
}
const onMountLinkOptions = (e) => {
if (e?.popper?.style) {
e.popper.style.width = '95%'
}
}
</script>
<template>
<BubbleMenu :editor="editor" :tippy-options="{ duration: 100, maxWidth: 600 }" :should-show="(checkLinkMark as any)">
<BubbleMenu
:editor="editor"
:tippy-options="{
duration: 100,
maxWidth: 600,
onMount: onMountLinkOptions,
}"
:should-show="(checkLinkMark as any)"
>
<div
v-if="!justDeleted"
ref="wrapperRef"
class="relative bubble-menu nc-text-area-rich-link-options flex flex-col bg-gray-50 py-1 px-1 rounded-lg"
class="relative bubble-menu nc-text-area-rich-link-options flex flex-col bg-gray-50 py-1 px-1 rounded-lg w-full"
data-testid="nc-text-area-rich-link-options"
@keydown.stop="handleKeyDown"
>
<div class="flex items-center gap-x-1">
<div class="!border-1 !border-gray-200 !py-0.5 bg-gray-100 rounded-md !z-10">
<div class="!border-1 !border-gray-200 !py-0.5 bg-gray-100 rounded-md !z-10 flex-1">
<a-input
ref="inputRef"
v-model:value="href"
class="nc-text-area-rich-link-option-input flex-1 !w-96 !mx-0.5 !px-1.5 !py-0.5 !rounded-md z-10"
class="nc-text-area-rich-link-option-input flex-1 !mx-0.5 !px-1.5 !py-0.5 !rounded-md z-10"
:bordered="false"
placeholder="Enter a link"
@change="onChange"

311
packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue

@ -10,16 +10,22 @@ import MsFormatH3 from '~icons/material-symbols/format-h3'
import TablerBlockQuote from '~icons/tabler/blockquote'
import MsCode from '~icons/material-symbols/code'
import MsFormatQuote from '~icons/material-symbols/format-quote'
import { RichTextBubbleMenuOptions } from '#imports'
interface Props {
editor: Editor
embedMode?: boolean
isFormField?: boolean
hiddenOptions?: RichTextBubbleMenuOptions[]
}
const props = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
embedMode: false,
isFormField: false,
hiddenOptions: () => [],
})
const { editor, embedMode } = toRefs(props)
const { editor, embedMode, isFormField, hiddenOptions } = toRefs(props)
const cmdOrCtrlKey = computed(() => {
return isMac() ? '⌘' : 'CTRL'
@ -33,6 +39,14 @@ const altKey = computed(() => {
return isMac() ? '⌥' : 'Alt'
})
const tooltipPlacement = computed(() => {
if (isFormField.value) return 'bottom'
})
const tabIndex = computed(() => {
return isFormField.value ? -1 : 0
})
const onToggleLink = () => {
const activeNode = editor.value?.state?.selection?.$from?.nodeBefore || editor.value?.state?.selection?.$from?.nodeAfter
@ -74,19 +88,29 @@ const onToggleLink = () => {
}, 100)
}
}
const isOptionVisible = (option: RichTextBubbleMenuOptions) => {
if (isFormField.value) return !hiddenOptions.value.includes(option)
return true
}
const showDivider = (options: RichTextBubbleMenuOptions[]) => {
return !isFormField.value || options.some((o) => !hiddenOptions.value.includes(o))
}
</script>
<template>
<div
class="bubble-menu flex-row gap-x-1 py-1 rounded-lg"
class="bubble-menu flex-row gap-x-1 rounded-lg"
:class="{
'inline-flex !bg-transparent': isFormField,
'flex bg-gray-100 px-1': !isFormField,
'nc-form-field-bubble-menu inline-flex py-0': isFormField,
'flex bg-gray-100 px-1 py-1': !isFormField,
'embed-mode': embedMode,
'full-mode': !embedMode,
}"
>
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<NcTooltip :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
@ -100,14 +124,14 @@ const onToggleLink = () => {
type="text"
:class="{ 'is-active': editor.isActive('bold') }"
:disabled="editor.isActive('codeBlock')"
:tabindex="isFormField ? -1 : 0"
:tabindex="tabIndex"
@click="editor!.chain().focus().toggleBold().run()"
>
<MdiFormatBold />
</NcButton>
</NcTooltip>
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<NcTooltip :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
@ -121,13 +145,13 @@ const onToggleLink = () => {
type="text"
:disabled="editor.isActive('codeBlock')"
:class="{ 'is-active': editor.isActive('italic') }"
:tabindex="isFormField ? -1 : 0"
:tabindex="tabIndex"
@click=";(editor!.chain().focus() as any).toggleItalic().run()"
>
<MdiFormatItalic />
</NcButton>
</NcTooltip>
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<NcTooltip :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
@ -142,13 +166,13 @@ const onToggleLink = () => {
type="text"
:class="{ 'is-active': editor.isActive('underline') }"
:disabled="editor.isActive('codeBlock')"
:tabindex="isFormField ? -1 : 0"
:tabindex="tabIndex"
@click="editor!.chain().focus().toggleUnderline().run()"
>
<MdiFormatUnderline />
</NcButton>
</NcTooltip>
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<NcTooltip :placement="tooltipPlacement" :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
@ -162,156 +186,179 @@ const onToggleLink = () => {
type="text"
:class="{ 'is-active': editor.isActive('strike') }"
:disabled="editor.isActive('codeBlock')"
:tabindex="isFormField ? -1 : 0"
:tabindex="tabIndex"
@click="editor!.chain().focus().toggleStrike().run()"
>
<MdiFormatStrikeThrough />
</NcButton>
</NcTooltip>
<template v-if="!isFormField">
<NcTooltip v-if="embedMode">
<template #title> {{ $t('general.code') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('codeBlock') }"
@click="editor!.chain().focus().toggleCodeBlock().run()"
>
<MsCode />
</NcButton>
</NcTooltip>
<NcTooltip v-else :disabled="editor.isActive('codeBlock')">
<template #title> {{ $t('general.quote') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('code') }"
:disabled="editor.isActive('codeBlock')"
@click="editor!.chain().focus().toggleCode().run()"
>
<MsFormatQuote />
</NcButton>
</NcTooltip>
<div class="divider"></div>
<template v-if="embedMode">
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading1') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 1</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 1 }).run()"
>
<MsFormatH1 />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading2') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 2</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 2 }).run()"
>
<MsFormatH2 />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading3') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 3</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 3 }).run()"
>
<MsFormatH3 />
</NcButton>
</NcTooltip>
<div class="divider"></div>
</template>
<NcTooltip v-if="embedMode">
<template #title> {{ $t('labels.blockQuote') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('blockquote') }"
@click="editor!.chain().focus().toggleBlockquote().run()"
>
<TablerBlockQuote class="-mt-0.25" />
</NcButton>
</NcTooltip>
<NcTooltip v-if="embedMode && isOptionVisible(RichTextBubbleMenuOptions.code)" :placement="tooltipPlacement">
<template #title> {{ $t('general.code') }}</template>
<NcButton
size="small"
type="text"
:tabindex="tabIndex"
:class="{ 'is-active': editor.isActive('codeBlock') }"
@click="editor!.chain().focus().toggleCodeBlock().run()"
>
<MsCode />
</NcButton>
</NcTooltip>
<NcTooltip
v-if="isFormField ? isOptionVisible(RichTextBubbleMenuOptions.quote) : !embedMode"
:placement="tooltipPlacement"
:disabled="editor.isActive('codeBlock')"
>
<template #title> {{ $t('general.quote') }}</template>
<NcButton
size="small"
type="text"
:tabindex="tabIndex"
:class="{ 'is-active': editor.isActive('code') }"
:disabled="editor.isActive('codeBlock')"
@click="editor!.chain().focus().toggleCode().run()"
>
<MsFormatQuote />
</NcButton>
</NcTooltip>
<div class="divider"></div>
<template v-if="embedMode && !isFormField">
<NcTooltip>
<template #title> {{ $t('labels.bulletList') }}</template>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading1') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 1</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('bulletList') }"
@click="editor!.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 1 }).run()"
>
<MdiFormatBulletList />
<MsFormatH1 />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title> {{ $t('labels.numberedList') }}</template>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading2') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 2</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('orderedList') }"
@click="editor!.chain().focus().toggleOrderedList().run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 2 }).run()"
>
<MdiFormatListNumber />
<MsFormatH2 />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title> {{ $t('labels.taskList') }}</template>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading3') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 3</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('taskList') }"
@click="editor!.chain().focus().toggleTaskList().run()"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 3 }).run()"
>
<MdiFormatListCheckbox />
<MsFormatH3 />
</NcButton>
</NcTooltip>
<div class="divider"></div>
</template>
<NcTooltip :placement="isFormField ? 'bottom' : undefined" :disabled="editor.isActive('codeBlock')">
<NcTooltip v-if="embedMode && isOptionVisible(RichTextBubbleMenuOptions.blockQuote)" :placement="tooltipPlacement">
<template #title> {{ $t('labels.blockQuote') }}</template>
<NcButton
size="small"
type="text"
:tabindex="tabIndex"
:class="{ 'is-active': editor.isActive('blockquote') }"
@click="editor!.chain().focus().toggleBlockquote().run()"
>
<TablerBlockQuote class="-mt-0.25" />
</NcButton>
</NcTooltip>
<NcTooltip v-if="isOptionVisible(RichTextBubbleMenuOptions.bulletList)" :placement="tooltipPlacement">
<template #title> {{ $t('labels.bulletList') }}</template>
<NcButton
size="small"
type="text"
:tabindex="tabIndex"
:class="{ 'is-active': editor.isActive('bulletList') }"
@click="editor!.chain().focus().toggleBulletList().run()"
>
<MdiFormatBulletList />
</NcButton>
</NcTooltip>
<NcTooltip v-if="isOptionVisible(RichTextBubbleMenuOptions.numberedList)" :placement="tooltipPlacement">
<template #title> {{ $t('labels.numberedList') }}</template>
<NcButton
size="small"
type="text"
:tabindex="tabIndex"
:class="{ 'is-active': editor.isActive('orderedList') }"
@click="editor!.chain().focus().toggleOrderedList().run()"
>
<MdiFormatListNumber />
</NcButton>
</NcTooltip>
<NcTooltip v-if="isOptionVisible(RichTextBubbleMenuOptions.taskList)" :placement="tooltipPlacement">
<template #title> {{ $t('labels.taskList') }}</template>
<NcButton
size="small"
type="text"
:tabindex="tabIndex"
:class="{ 'is-active': editor.isActive('taskList') }"
@click="editor!.chain().focus().toggleTaskList().run()"
>
<MdiFormatListCheckbox />
</NcButton>
</NcTooltip>
<div
v-if="
showDivider([
RichTextBubbleMenuOptions.blockQuote,
RichTextBubbleMenuOptions.bulletList,
RichTextBubbleMenuOptions.numberedList,
RichTextBubbleMenuOptions.taskList,
])
"
class="divider"
></div>
<NcTooltip
v-if="isOptionVisible(RichTextBubbleMenuOptions.link)"
:placement="tooltipPlacement"
:disabled="editor.isActive('codeBlock')"
>
<template #title> {{ $t('general.link') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('link') }"
:disabled="editor.isActive('codeBlock')"
:tabindex="isFormField ? -1 : 0"
:tabindex="tabIndex"
@click="onToggleLink"
>
<GeneralIcon v-if="isFormField" icon="link2"></GeneralIcon>
@ -349,14 +396,14 @@ const onToggleLink = () => {
box-shadow: 0px 0px 1.2rem 0 rgb(230, 230, 230) !important;
}
.bubble-menu.embed-mode {
.bubble-menu.embed-mode:not(.nc-form-field-bubble-menu) {
@apply border-transparent !shadow-none;
}
.bubble-menu.form-field-mode {
@apply bg-transparent px-0;
}
.embed-mode.bubble-menu {
.embed-mode.bubble-menu:not(.nc-form-field-bubble-menu) {
@apply !py-0 !my-0 !border-0;
.divider {
@ -373,12 +420,20 @@ const onToggleLink = () => {
@apply bg-white;
border-width: 1px;
&.nc-form-field-bubble-menu {
.divider {
@apply border-r-1 border-gray-200 my-0;
}
}
.nc-button.is-active {
@apply !hover:outline-gray-200 bg-gray-100 text-brand-500;
outline: 1px;
}
.divider {
@apply border-r-1 border-gray-200 !h-6 !mx-0.5 my-1;
&:not(.nc-form-field-bubble-menu) {
.divider {
@apply border-r-1 border-gray-200 !h-6 !mx-0.5 my-1;
}
}
.ant-select-selector {
@apply !rounded-md;

12
packages/nc-gui/components/cell/SingleSelect.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { Select as AntSelect } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import tinycolor from 'tinycolor2'
import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk'
import type { FormFieldsLimitOptionsType } from '~/lib'
import {
@ -11,6 +11,7 @@ import {
EditModeInj,
IsFormInj,
IsKanbanInj,
IsSurveyFormInj,
ReadonlyInj,
computed,
enumColor,
@ -62,6 +63,8 @@ const isPublic = inject(IsPublicInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const { $api } = useNuxtApp()
const searchVal = ref()
@ -187,11 +190,6 @@ useSelectedCellKeyupListener(activeCell, (e) => {
}
})
// close dropdown list on escape
useSelectedCellKeyupListener(isOpen, (e) => {
if (e.key === 'Escape') isOpen.value = false
})
async function addIfMissingAndSave() {
if (!tempSelectedOptState.value || isPublic.value) return false
@ -317,6 +315,8 @@ const onFocus = () => {
isFocusing.value = false
}, 250)
if (isSurveyForm.value && vModel.value) return
isOpen.value = true
}
</script>

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

@ -36,9 +36,13 @@ const rowHeight = inject(RowHeightInj, ref(1 as const))
const isForm = inject(IsFormInj, ref(false))
const readOnly = inject(ReadonlyInj, ref(false))
const { showNull } = useGlobal()
const vModel = useVModel(props, 'modelValue', emits)
const vModel = useVModel(props, 'modelValue', emits, {
shouldEmit: () => !readOnly.value,
})
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
@ -77,8 +81,6 @@ const inputWrapperRef = ref<HTMLElement | null>(null)
const inputRef = ref<HTMLTextAreaElement | null>(null)
const readOnly = inject(ReadonlyInj)
watch(isVisible, () => {
if (isVisible.value) {
setTimeout(() => {
@ -210,8 +212,28 @@ watch(inputWrapperRef, () => {
'h-full w-full': isForm,
}"
>
<div v-if="isForm && isRichMode" class="w-full">
<div
class="w-full relative w-full px-0 pb-1"
:class="{
'pt-11': !readOnly,
}"
>
<LazyCellRichText
v-model:value="vModel"
class="!max-h-50"
:class="{
'border-t-1 border-gray-100': !readOnly,
}"
:autofocus="false"
show-menu
:read-only="readOnly"
/>
</div>
</div>
<div
v-if="isRichMode"
v-else-if="isRichMode"
class="w-full cursor-pointer nc-readonly-rich-text-wrapper"
:class="{
'nc-readonly-rich-text-grid ': !isExpandedFormOpen && !isForm,
@ -227,7 +249,7 @@ watch(inputWrapperRef, () => {
<LazyCellRichText v-model:value="vModel" sync-value-change read-only />
</div>
<textarea
v-else-if="editEnabled && !isVisible"
v-else-if="(editEnabled && !isVisible) || isForm"
:ref="focus"
v-model="vModel"
:rows="isForm ? 5 : 4"
@ -271,7 +293,7 @@ watch(inputWrapperRef, () => {
<span v-else>{{ vModel }}</span>
<NcTooltip
v-if="!isVisible"
v-if="!isVisible && !isForm"
placement="bottom"
class="!absolute top-1 hidden nc-text-area-expand-btn group-hover:block z-3"
:class="isForm ? 'right-1' : 'right-0'"

16
packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue

@ -103,21 +103,13 @@ async function onOpenModal({
close(1000)
}
}
const isEasterEggEnabled = ref(false)
watch(isOpen, (val) => {
if (!val) {
isEasterEggEnabled.value = false
}
})
</script>
<template>
<NcDropdown v-model:visible="isOpen" :overlay-class-name="overlayClassName" destroy-popup-on-hide @click.stop="isOpen = true">
<slot />
<template #overlay>
<NcMenu class="max-w-48" @dblclick.stop="isEasterEggEnabled = true">
<NcMenu class="max-w-48">
<NcMenuItem @click.stop="onOpenModal({ type: ViewTypes.GRID })">
<div class="item" data-testid="sidebar-view-create-grid">
<div class="item-inner">
@ -163,11 +155,7 @@ watch(isOpen, (val) => {
<GeneralIcon v-else class="plus" icon="plus" />
</div>
</NcMenuItem>
<NcMenuItem
v-if="isEasterEggEnabled"
data-testid="sidebar-view-create-calendar"
@click="onOpenModal({ type: ViewTypes.CALENDAR })"
>
<NcMenuItem data-testid="sidebar-view-create-calendar" @click="onOpenModal({ type: ViewTypes.CALENDAR })">
<div class="item">
<div class="item-inner">
<GeneralViewIcon :meta="{ type: ViewTypes.CALENDAR }" />

52
packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue

@ -2,7 +2,7 @@
import type { SourceType } from 'nocodb-sdk'
import { Form, message } from 'ant-design-vue'
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select'
import type { DefaultConnection, ProjectCreateForm, SQLiteConnection } from '#imports'
import type { DefaultConnection, ProjectCreateForm, SQLiteConnection, SnowflakeConnection } from '#imports'
import {
CertTypes,
ClientType,
@ -387,6 +387,56 @@ onMounted(async () => {
<a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" />
</a-form-item>
<template v-else-if="formState.dataSource.client === ClientType.SNOWFLAKE">
<!-- Account -->
<a-form-item :label="$t('labels.account')" v-bind="validateInfos['dataSource.connection.account']">
<a-input
v-model:value="(formState.dataSource.connection as SnowflakeConnection).account"
class="nc-extdb-host-address"
/>
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.username']">
<a-input
v-model:value="(formState.dataSource.connection as SnowflakeConnection).username"
class="nc-extdb-host-user"
/>
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')" v-bind="validateInfos['dataSource.connection.password']">
<a-input-password
v-model:value="(formState.dataSource.connection as SnowflakeConnection).password"
class="nc-extdb-host-password"
/>
</a-form-item>
<!-- Warehouse -->
<a-form-item label="Warehouse" v-bind="validateInfos['dataSource.connection.warehouse']">
<a-input
v-model:value="(formState.dataSource.connection as SnowflakeConnection).warehouse"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Database -->
<a-form-item :label="$t('labels.database')" v-bind="validateInfos['dataSource.connection.database']">
<a-input
v-model:value="(formState.dataSource.connection as SnowflakeConnection).database"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Schema -->
<a-form-item label="Schema" v-bind="validateInfos['dataSource.connection.schema']">
<a-input
v-model:value="(formState.dataSource.connection as SnowflakeConnection).schema"
class="nc-extdb-host-database"
/>
</a-form-item>
</template>
<template v-else>
<!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">

7
packages/nc-gui/components/dlg/TableCreate.vue

@ -30,7 +30,7 @@ const inputEl = ref<HTMLInputElement>()
const { addTab } = useTabs()
const { isMysql, isMssql, isPg } = useBase()
const { isMysql, isMssql, isPg, isSnowflake } = useBase()
const { loadProjectTables, addTable } = useTablesStore()
@ -145,7 +145,7 @@ onMounted(() => {
</template>
<div class="flex flex-col mt-2">
<a-form :model="table" name="create-new-table-form" @keydown.enter="_createTable" @keydown.esc="dialogShow = false">
<a-form-item v-bind="validateInfos.title">
<a-form-item v-bind="validateInfos.title" :class="{ '!mb-1': isSnowflake(props.sourceId) }">
<a-input
ref="inputEl"
v-model:value="table.title"
@ -155,6 +155,9 @@ onMounted(() => {
:placeholder="$t('msg.info.enterTableName')"
/>
</a-form-item>
<template v-if="isSnowflake(props.sourceId)">
<a-checkbox v-model:checked="table.is_hybrid" class="!flex flex-row items-center"> Hybrid Table </a-checkbox>
</template>
<div class="nc-table-advanced-options" :class="{ active: isAdvanceOptVisible }">
<div>
<div class="mb-1">

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

@ -370,9 +370,9 @@ watchEffect(() => {})
<div>
{{ $t('activity.surveyMode') }}
</div>
<NcTooltip>
<NcTooltip class="flex items-center">
<template #title> {{ $t('tooltip.surveyFormInfo') }}</template>
<GeneralIcon icon="info" class="text-gray-600 cursor-pointer"></GeneralIcon>
<GeneralIcon icon="info" class="flex-none text-gray-600 cursor-pointer"></GeneralIcon>
</NcTooltip>
</div>
<a-switch
@ -405,13 +405,13 @@ watchEffect(() => {})
{{ $t('activity.preFilledFields.title') }}
</div>
<NcTooltip>
<NcTooltip class="flex items-center">
<template #title>
<div class="text-center">
{{ $t('tooltip.preFillFormInfo') }}
</div>
</template>
<GeneralIcon icon="info" class="text-gray-600 cursor-pointer"></GeneralIcon>
<GeneralIcon icon="info" class="flex-none text-gray-600 cursor-pointer"></GeneralIcon>
</NcTooltip>
</div>
<a-switch

6
packages/nc-gui/components/general/CopyUrl.vue

@ -12,19 +12,21 @@ const isCopied = ref({
embed: false,
})
const { copy } = useCopy()
const openUrl = async () => {
window.open(url.value, '_blank', 'noopener,noreferrer')
}
const embedHtml = async () => {
await navigator.clipboard.writeText(`<iframe src="${url.value}" width="100%" height="100%" style="border: none;"></iframe>`)
await copy(`<iframe src="${url.value}" width="100%" height="100%" style="border: none;"></iframe>`)
isCopied.value.embed = true
}
const copyUrl = async () => {
isCopied.value.link = false
await navigator.clipboard.writeText(url.value)
await copy(url.value)
setTimeout(() => {
isCopied.value.link = true

2
packages/nc-gui/components/general/ShareProject.vue

@ -47,7 +47,7 @@ const copySharedBase = async () => {
<template>
<div
v-if="!isSharedBase && isUIAllowed('baseShare') && visibility !== 'hidden' && (activeTable || base)"
class="flex flex-col justify-center h-full"
class="nc-share-base-button flex flex-col justify-center h-full"
data-testid="share-base-button"
:data-sharetype="visibility"
>

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

@ -80,7 +80,7 @@ export default {
<p class="mb-0">
<span
>Please report this error in our
<a href="https://discord.gg/8jX2GQn" target="_blank" rel="noopener noreferrer">Discord channel</a>. You can copy the
<a href="https://discord.gg/5RgZmkW" target="_blank" rel="noopener noreferrer">Discord channel</a>. You can copy the
error message by clicking the "Copy" button below.</span
>
</p>

14
packages/nc-gui/components/project/ShareBaseDlg.vue

@ -2,6 +2,8 @@
import type { RoleLabels } from 'nocodb-sdk'
import { OrderedProjectRoles, ProjectRoles } from 'nocodb-sdk'
import type { User } from '#imports'
import { extractEmail } from '~/helpers/parsers/parserHelpers'
const props = defineProps<{
modelValue: boolean
baseId?: string
@ -64,13 +66,17 @@ const insertOrUpdateString = (str: string) => {
emailBadges.value.push(str)
}
const emailInputValidation = (input: string): boolean => {
const emailInputValidation = (input: string, isBulkEmailCopyPaste: boolean = false): boolean => {
if (!input.length) {
if (isBulkEmailCopyPaste) return false
emailValidation.isError = true
emailValidation.message = 'Email should not be empty'
return false
}
if (!validateEmail(input.trim())) {
if (isBulkEmailCopyPaste) return false
emailValidation.isError = true
emailValidation.message = 'Invalid Email'
return false
@ -157,6 +163,8 @@ watch(dialogShow, (newVal) => {
// when bulk email is pasted
const onPaste = (e: ClipboardEvent) => {
emailValidation.isError = false
const pastedText = e.clipboardData?.getData('text')
const inputArray = pastedText?.split(',') || pastedText?.split(' ')
@ -168,7 +176,9 @@ const onPaste = (e: ClipboardEvent) => {
}
inputArray?.forEach((el) => {
const isEmailIsValid = emailInputValidation(el)
el = extractEmail(el) || el
const isEmailIsValid = emailInputValidation(el, inputArray.length > 1)
if (!isEmailIsValid) return

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

@ -195,7 +195,8 @@ onUnmounted(() => {
`nc-cell-${(column?.uidt || 'default').toLowerCase()}`,
{
'text-brand-500': isPrimary(column) && !props.virtual && !isForm && !isCalendar,
'nc-grid-numeric-cell-right': isGrid && isNumericField && !isEditColumnMenu && !isForm && !isExpandedFormOpen,
'nc-grid-numeric-cell-right':
isGrid && isNumericField && !isEditColumnMenu && !isForm && !isExpandedFormOpen && !isRating(column),
'h-10': !isEditColumnMenu && isForm && !isAttachment(column) && !isTextArea(column) && !isJSON(column) && !props.virtual,
'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu,
'!min-h-30': isTextArea(column) && (isForm || isSurveyForm),

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

File diff suppressed because it is too large Load Diff

19
packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import type { ColumnType } from 'nocodb-sdk'
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { type Row, computed, ref, useViewColumnsOrThrow } from '#imports'
import { isRowEmpty } from '~/utils'
const emit = defineEmits(['expandRecord', 'newRecord'])
@ -12,8 +12,7 @@ const container = ref()
const { isUIAllowed } = useRoles()
const { selectedDate, formattedData, formattedSideBarData, calendarRange, updateRowProperty, displayField } =
useCalendarViewStoreOrThrow()
const { selectedDate, formattedData, formattedSideBarData, calendarRange, updateRowProperty } = useCalendarViewStoreOrThrow()
const fields = inject(FieldsInj, ref())
@ -29,8 +28,6 @@ const getFieldStyle = (field: ColumnType) => {
}
}
const fieldsWithoutDisplay = computed(() => fields.value?.filter((f) => !isPrimary(f)))
// We loop through all the records and calculate the position of each record based on the range
// We only need to calculate the top, of the record since there is no overlap in the day view of date Field
const recordsAcrossAllRange = computed<Row[]>(() => {
@ -223,17 +220,7 @@ const newRecord = () => {
size="small"
@click="emit('expandRecord', record)"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField!).bold"
:column="displayField!"
:italic="getFieldStyle(displayField!).italic"
:underline="getFieldStyle(displayField!).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<template v-for="(field, id) in fields" :key="id">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"

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

@ -1,7 +1,7 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import type { ColumnType } from 'nocodb-sdk'
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { type Row, computed, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emit = defineEmits(['expandRecord', 'newRecord'])
@ -40,8 +40,6 @@ const getFieldStyle = (field: ColumnType) => {
}
}
const fieldsWithoutDisplay = computed(() => fields.value?.filter((f) => !isPrimary(f)))
const hours = computed(() => {
const hours: Array<dayjs.Dayjs> = []
const _selectedDate = dayjs(selectedDate.value)
@ -220,7 +218,7 @@ const recordsAcrossAllRange = computed<{
}
} = {}
const perRecordHeight = 80
const perRecordHeight = 60
/* const columnArray: Array<Array<Row>> = [[]]
const gridTimeMap = new Map() */
@ -264,18 +262,18 @@ const recordsAcrossAllRange = computed<{
scheduleEnd,
})
// The top of the record is calculated based on the start hour and minute
const topInPixels = (startDate.hour() + startDate.minute() / 60) * 80
const topInPixels = startDate.hour() + startDate.minute()
// A minimum height of 80px is set for each record
// The height of the record is calculated based on the difference between the start and end date
const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 80, perRecordHeight)
const heightInPixels = Math.max(endDate.diff(startDate, 'minute'), perRecordHeight)
const startHour = startDate.hour()
let _startDate = startDate.clone()
const style: Partial<CSSStyleDeclaration> = {
height: `${heightInPixels}px`,
top: `${topInPixels + 5 + startHour * 2}px`,
top: `${topInPixels}px`,
}
// We loop through every 1 minutes between the start and end date and keep track of the number of records that overlap at a given time
@ -371,18 +369,13 @@ const recordsAcrossAllRange = computed<{
// The top of the record is calculated based on the start hour
// Update such that it is also based on Minutes
const minutes = startDate.minute() + startDate.hour() * 60
const updatedTopInPixels = (minutes * 80) / 60
const topInPixels = startDate.minute() + startDate.hour() * 60
// A minimum height of 80px is set for each record
const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 80, perRecordHeight)
const finalTopInPixels = updatedTopInPixels + startHour * 2
const heightInPixels = Math.max((endDate.diff(startDate, 'minute') / 60) * 60, perRecordHeight)
style = {
...style,
top: `${finalTopInPixels + 2}px`,
top: `${topInPixels + 1}px`,
height: `${heightInPixels - 2}px`,
}
@ -849,6 +842,19 @@ const newRecord = (hour: dayjs.Dayjs) => {
}
emit('newRecord', record)
}
watch(
() => recordsAcrossAllRange.value,
() => {
setTimeout(() => {
if (isDragging.value) return
const records = document.querySelectorAll('.draggable-record')
if (records.length) records.item(0)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
else document.querySelectorAll('.nc-calendar-day-hour').item(9)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 100)
},
{ immediate: true },
)
</script>
<template>
@ -863,12 +869,12 @@ const newRecord = (hour: dayjs.Dayjs) => {
:class="{
'!border-brand-500': hour.isSame(selectedTime),
}"
class="flex w-full min-h-20 relative border-1 group hover:bg-gray-50 border-white border-b-gray-100"
class="flex w-full h-15 nc-calendar-day-hour relative border-1 group hover:bg-gray-50 border-white border-b-gray-100"
data-testid="nc-calendar-day-hour"
@click="selectHour(hour)"
@dblclick="newRecord(hour)"
>
<div class="pt-2 px-4 text-xs text-gray-500 font-semibold h-20">
<div class="pt-2 px-4 text-xs text-gray-500 font-semibold h-15">
{{ dayjs(hour).format('H A') }}
</div>
<div></div>
@ -955,6 +961,7 @@ const newRecord = (hour: dayjs.Dayjs) => {
<NcButton
v-if="isOverflowAcrossHourRange(hour).isOverflow"
v-e="`['c:calendar:week-view-more']`"
class="!absolute bottom-2 text-center w-15 mx-auto inset-x-0 z-3 text-gray-500"
size="xxsmall"
type="secondary"
@ -991,17 +998,7 @@ const newRecord = (hour: dayjs.Dayjs) => {
color="blue"
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField!).bold"
:column="displayField!"
:italic="getFieldStyle(displayField!).italic"
:underline="getFieldStyle(displayField!).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<template v-for="(field, id) in fields" :key="id">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"

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

@ -1,7 +1,7 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import type { ColumnType } from 'nocodb-sdk'
import { type Row, computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { type Row, computed, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emit = defineEmits(['newRecord', 'expandRecord'])
@ -75,8 +75,6 @@ const getFieldStyle = (field: ColumnType | undefined) => {
}
}
const fieldsWithoutDisplay = computed(() => fields.value?.filter((f) => !isPrimary(f)))
const dates = computed(() => {
const startOfMonth = selectedMonth.value.startOf('month')
const endOfMonth = selectedMonth.value.endOf('month')
@ -761,6 +759,7 @@ const addRecord = (date: dayjs.Dayjs) => {
recordsToDisplay.count[dayjs(day).format('YYYY-MM-DD')]?.overflow &&
!draggingId
"
v-e="`['c:calendar:month-view-more']`"
class="!absolute bottom-1 right-1 text-center min-w-4.5 mx-auto z-3 text-gray-500"
size="xxsmall"
type="secondary"
@ -796,16 +795,7 @@ const addRecord = (date: dayjs.Dayjs) => {
@resize-start="onResizeStart"
@dblclick.stop="emit('expandRecord', record)"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField).bold"
:column="displayField"
:italic="getFieldStyle(displayField).italic"
:underline="getFieldStyle(displayField).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<template v-for="(field, id) in fields" :key="id">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"

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

@ -79,7 +79,12 @@ const emit = defineEmits(['resize-start'])
}"
class="text-sm pr-3 mb-0.5 mr-3 break-word whitespace-nowrap overflow-hidden text-ellipsis w-full truncate text-gray-800"
>
<slot />
<NcTooltip :disabled="selected" show-on-truncate-only>
<slot />
<template #title>
<slot />
</template>
</NcTooltip>
</span>
<span v-if="position === 'leftRounded' || position === 'none'" class="absolute my-0 right-5"> .... </span>
</div>

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

@ -369,7 +369,7 @@ onUnmounted(() => {
</div>
<div
v-if="calendarRange"
v-if="calendarRange?.length"
:ref="sideBarListRef"
:class="{
'!h-[calc(100vh-10.5rem)]': width <= 1440,
@ -383,7 +383,7 @@ onUnmounted(() => {
>
<NcButton
v-if="isUIAllowed('dataEdit') && props.visible"
v-e="['c:calendar:calendar-new-record-btn']"
v-e="['c:calendar:calendar-sidemenu-new-record-btn']"
class="!absolute right-5 !border-brand-500 bottom-5 !h-12 !w-12"
data-testid="nc-calendar-side-menu-new-btn"
type="secondary"
@ -440,6 +440,17 @@ onUnmounted(() => {
</div>
</template>
</div>
<div
v-else
:class="{
'!h-[calc(100vh-10.5rem)]': width <= 1440,
'h-[calc(100vh-36.2rem)]': activeCalendarView === ('day' as const) || activeCalendarView === ('week' as const) && width >= 1440,
'h-[calc(100vh-25.1rem)]': activeCalendarView === ('month' as const) || activeCalendarView === ('year' as const) && width >= 1440,
}"
class="flex items-center justify-center h-full"
>
{{ $t('activity.noRange') }}
</div>
</div>
</div>
</template>

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

@ -71,7 +71,12 @@ const emit = defineEmits(['resize-start'])
<div v-if="position === 'bottomRounded' || position === 'none'" class="ml-3">....</div>
<span class="pl-1 pr-1 text-sm h-[80%] text-gray-800 leading-7 break-all whitespace-normal truncate w-full overflow-y-hidden">
<slot />
<NcTooltip :disabled="selected" show-on-truncate-only>
<slot />
<template #title>
<slot />
</template>
</NcTooltip>
</span>
<div v-if="position === 'topRounded' || position === 'none'" class="h-full pb-7 flex items-end ml-3">....</div>

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

@ -2,7 +2,7 @@
import dayjs from 'dayjs'
import { type ColumnType } from 'nocodb-sdk'
import type { Row } from '~/lib'
import { computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { computed, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emits = defineEmits(['expandRecord', 'newRecord'])
@ -32,8 +32,6 @@ const getFieldStyle = (field: ColumnType | undefined) => {
}
}
const fieldsWithoutDisplay = computed(() => fields.value?.filter((f) => !isPrimary(f)))
// Calculate the dates of the week
const weekDates = computed(() => {
let startOfWeek = dayjs(selectedDateRange.value.start)
@ -589,16 +587,7 @@ const addRecord = (date: dayjs.Dayjs) => {
@dblclick.stop="emits('expandRecord', record)"
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField).bold"
:column="displayField"
:italic="getFieldStyle(displayField).italic"
:underline="getFieldStyle(displayField).underline"
/>
</template>
<template v-for="(field, index) in fieldsWithoutDisplay" :key="index">
<template v-for="(field, index) in fields" :key="index">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"

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

@ -2,7 +2,7 @@
import dayjs from 'dayjs'
import { type ColumnType } from 'nocodb-sdk'
import type { Row } from '~/lib'
import { computed, isPrimary, ref, useViewColumnsOrThrow } from '#imports'
import { computed, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emits = defineEmits(['expandRecord', 'newRecord'])
@ -45,8 +45,6 @@ const getFieldStyle = (field: ColumnType | undefined) => {
}
}
const fieldsWithoutDisplay = computed(() => fields.value?.filter((f) => !isPrimary(f)))
// Since it is a datetime Week view, we need to create a 2D array of dayjs objects to represent the hours in a day for each day in the week
const datesHours = computed(() => {
const datesHours: Array<Array<dayjs.Dayjs>> = []
@ -90,11 +88,8 @@ const recordsAcrossAllRange = computed<{
records: [],
count: {},
}
const { scrollHeight } = scrollContainer.value
const perWidth = containerWidth.value / 7
const perHeight = scrollHeight / 24
const perHeight = 60
const scheduleStart = dayjs(selectedDateRange.value.start).startOf('day')
const scheduleEnd = dayjs(selectedDateRange.value.end).endOf('day')
@ -195,25 +190,12 @@ const recordsAcrossAllRange = computed<{
dayIndex = 6
}
const hourKey = ogStartDate.format('HH:mm')
// We calculate the index of the hour in the day and set the top and height of the record
const hourIndex = Math.min(
Math.max(
datesHours.value[dayIndex]?.findIndex((h) => h.startOf('hour').format('HH:mm') === hourKey),
0,
),
23,
)
const minutes = ogStartDate.minute() + ogStartDate.hour() * 60
const topPx = (minutes * perHeight) / 60
style = {
...style,
top: `${topPx - hourIndex - hourIndex * 0.15 + 0.7}px`,
height: `${perHeight - 4}px`,
top: `${minutes + 1}px`,
height: `${perHeight - 2}px`,
}
recordsToDisplay.push({
@ -735,6 +717,17 @@ const addRecord = (date: dayjs.Dayjs) => {
}
emits('newRecord', newRecord)
}
watch(
() => recordsAcrossAllRange.value,
() => {
if (dragRecord.value) return
const records = document.querySelectorAll('.draggable-record')
if (records.length) records.item(0)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
else document.querySelectorAll('.nc-calendar-day-hour').item(9)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
},
{ immediate: true },
)
</script>
<template>
@ -760,7 +753,7 @@ const addRecord = (date: dayjs.Dayjs) => {
<div
v-for="(hour, index) in datesHours[0]"
:key="index"
class="h-20 first:mt-0 pt-7.1 text-center text-xs text-gray-500 py-1"
class="h-15 first:mt-0 pt-7.1 nc-calendar-day-hour text-center text-xs text-gray-500 py-1"
>
{{ hour.format('h A') }}
</div>
@ -774,7 +767,7 @@ const addRecord = (date: dayjs.Dayjs) => {
'border-1 !border-brand-500 bg-gray-50': hour.isSame(selectedTime, 'hour'),
'!bg-gray-50': hour.get('day') === 0 || hour.get('day') === 6,
}"
class="text-center relative h-20 text-sm text-gray-500 w-full hover:bg-gray-50 py-1 border-transparent border-1 border-x-gray-100 border-t-gray-100"
class="text-center relative h-15 text-sm text-gray-500 w-full hover:bg-gray-50 py-1 border-transparent border-1 border-x-gray-100 border-t-gray-100"
data-testid="nc-calendar-week-hour"
@dblclick="addRecord(hour)"
@click="
@ -787,6 +780,7 @@ const addRecord = (date: dayjs.Dayjs) => {
>
<NcButton
v-if="isOverflowAcrossHourRange(hour).isOverflow"
v-e="`['c:calendar:week-view-more']`"
class="!absolute bottom-1 text-center w-15 ml-auto inset-x-0 z-3 text-gray-500"
size="xxsmall"
type="secondary"
@ -827,17 +821,7 @@ const addRecord = (date: dayjs.Dayjs) => {
:selected="record.rowMeta!.id === dragRecord?.rowMeta?.id"
@resize-start="onResizeStart"
>
<template v-if="!isRowEmpty(record, displayField)">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, displayField!)"
v-model="record.row[displayField!.title!]"
:bold="getFieldStyle(displayField).bold"
:column="displayField"
:italic="getFieldStyle(displayField).italic"
:underline="getFieldStyle(displayField).underline"
/>
</template>
<template v-for="(field, id) in fieldsWithoutDisplay" :key="id">
<template v-for="(field, id) in fields" :key="id">
<LazySmartsheetCalendarCell
v-if="!isRowEmpty(record, field!)"
v-model="record.row[field!.title!]"

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

@ -52,6 +52,7 @@ const {
selectedMonth,
activeDates,
pageDate,
fetchActiveDates,
showSideMenu,
selectedDateRange,
paginateCalendarView,
@ -127,8 +128,12 @@ reloadViewMetaHook?.on(async () => {
await loadCalendarMeta()
})
reloadViewDataHook?.on(async () => {
await Promise.all([loadCalendarData(), loadSidebarData()])
reloadViewDataHook?.on(async (params: void | { shouldShowLoading?: boolean }) => {
await Promise.all([
loadCalendarData(params?.shouldShowLoading ?? false),
loadSidebarData(params?.shouldShowLoading ?? false),
fetchActiveDates(),
])
})
const goToToday = () => {
@ -187,7 +192,15 @@ const headerText = computed(() => {
<NcDropdown v-model:visible="calendarRangeDropdown" :auto-close="false" :trigger="['click']">
<NcButton :class="{ '!w-22': activeCalendarView === 'year' }" class="w-45" full-width size="small" type="secondary">
<div class="flex px-2 w-full items-center justify-between">
<span class="font-bold text-center text-brand-500" data-testid="nc-calendar-active-date">{{ headerText }}</span>
<div class="flex gap-1 text-brand-500" data-testid="nc-calendar-active-date">
<span class="font-bold text-center">{{
activeCalendarView === 'month' ? headerText.split(' ')[0] : headerText
}}</span>
<span v-if="activeCalendarView === 'month'">
{{ ` ${headerText.split(' ')[1]}` }}
</span>
</div>
<component :is="iconMap.arrowDown" class="h-4 w-4 text-gray-700" />
</div>
</NcButton>
@ -269,7 +282,7 @@ const headerText = computed(() => {
</NcButton>
</NcTooltip>
</div>
<template v-if="calendarRange">
<template v-if="calendarRange?.length">
<LazySmartsheetCalendarYearView v-if="activeCalendarView === 'year'" />
<template v-if="!isCalendarDataLoading">
<LazySmartsheetCalendarMonthView

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

@ -332,7 +332,7 @@ onMounted(() => {
</div>
</div>
<div class="flex flex-row mt-1 mb-3 justify-end pr-3">
<a target="_blank" rel="noopener noreferrer" :href="suggestionPreviewed.docsUrl">
<a v-if="suggestionPreviewed.docsUrl" target="_blank" rel="noopener noreferrer" :href="suggestionPreviewed.docsUrl">
<NcButton type="text" class="!text-gray-400 !hover:text-gray-800 !text-xs"
>View in Docs
<GeneralIcon icon="openInNew" class="ml-1" />

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

@ -78,7 +78,7 @@ watch(
<div class="flex-1">
<component
:is="getMdiIcon(icon.full)"
class="mx-1"
class="mr-[2px]"
:style="{
color: vModel.meta.color,
}"

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

@ -277,7 +277,7 @@ const undoRemoveRenderedOption = (index: number) => {
watch(vModel, (next) => {
const cdfs = (next.cdf ?? '').toString().split(',')
const valuesMap = (next.colOptions.options ?? []).reduce((acc, c) => {
const valuesMap = (next.colOptions?.options ?? []).reduce((acc, c) => {
acc[c.title.replace(/^'|'$/g, '')] = c
return acc
}, {})

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

@ -333,7 +333,7 @@ onMounted(async () => {
isLoading.value = false
if (focusFirstCell) {
if (focusFirstCell && isNew.value) {
setTimeout(() => {
cellWrapperEl.value?.$el?.querySelector('input,select,textarea')?.focus()
}, 300)

248
packages/nc-gui/components/smartsheet/form/FieldMenu.vue

@ -0,0 +1,248 @@
<script lang="ts" setup>
import type { ColumnReqType, ColumnType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { computed } from 'vue'
import {
ActiveViewInj,
ColumnInj,
IsLockedInj,
MetaInj,
ReloadViewDataHookInj,
SmartsheetStoreEvents,
iconMap,
inject,
message,
toRefs,
useI18n,
useMetas,
useNuxtApp,
useSmartsheetStoreOrThrow,
} from '#imports'
const props = defineProps<{
column: ColumnType
formColumn: Record<string, any>
isRequired?: boolean
isOpen: boolean
onDelete: () => void
}>()
const emit = defineEmits(['hideField', 'update:isOpen', 'delete'])
const { column, isRequired } = toRefs(props)
const isOpen = useVModel(props, 'isOpen', emit)
const { eventBus } = useSmartsheetStoreOrThrow()
const reloadDataHook = inject(ReloadViewDataHookInj)
const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj)
provide(ColumnInj, column)
const { $api } = useNuxtApp()
const { t } = useI18n()
const { getMeta } = useMetas()
const showDeleteColumnModal = ref(false)
const isDuplicateDlgOpen = ref(false)
const selectedColumnExtra = ref<any>()
const duplicateDialogRef = ref<any>()
const duplicateVirtualColumn = async () => {
let columnCreatePayload = {}
// generate duplicate column title
const duplicateColumnTitle = getUniqueColumnName(`${column!.value.title} copy`, meta!.value!.columns!)
columnCreatePayload = {
...column!.value!,
...(column!.value.colOptions ?? {}),
title: duplicateColumnTitle,
column_name: duplicateColumnTitle.replace(/\s/g, '_'),
id: undefined,
colOptions: undefined,
order: undefined,
system: false,
}
try {
const gridViewColumnList = (await $api.dbViewColumn.list(view.value?.id as string)).list
const currentColumnIndex = gridViewColumnList.findIndex((f) => f.fk_column_id === column!.value.id)
let newColumnOrder
if (currentColumnIndex === gridViewColumnList.length - 1) {
newColumnOrder = gridViewColumnList[currentColumnIndex].order! + 1
} else {
newColumnOrder = (gridViewColumnList[currentColumnIndex].order! + gridViewColumnList[currentColumnIndex + 1].order!) / 2
}
await $api.dbTableColumn.create(meta!.value!.id!, {
...columnCreatePayload,
pv: false,
view_id: view.value!.id as string,
column_order: {
order: newColumnOrder,
view_id: view.value!.id as string,
},
} as ColumnReqType)
await getMeta(meta!.value!.id!, true)
eventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD)
reloadDataHook?.trigger()
// message.success(t('msg.success.columnDuplicated'))
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}
// closing dropdown
isOpen.value = false
}
const openDuplicateDlg = async () => {
if (!column?.value) return
if (
column.value.uidt &&
[
UITypes.Lookup,
UITypes.Rollup,
UITypes.CreatedTime,
UITypes.LastModifiedTime,
UITypes.CreatedBy,
UITypes.LastModifiedBy,
].includes(column.value.uidt as UITypes)
) {
duplicateVirtualColumn()
} else {
const gridViewColumnList = (await $api.dbViewColumn.list(view.value?.id as string)).list
const currentColumnIndex = gridViewColumnList.findIndex((f) => f.fk_column_id === column!.value.id)
let newColumnOrder
if (currentColumnIndex === gridViewColumnList.length - 1) {
newColumnOrder = gridViewColumnList[currentColumnIndex].order! + 1
} else {
newColumnOrder = (gridViewColumnList[currentColumnIndex].order! + gridViewColumnList[currentColumnIndex + 1].order!) / 2
}
selectedColumnExtra.value = {
pv: false,
view_id: view.value!.id as string,
column_order: {
order: newColumnOrder,
view_id: view.value!.id as string,
},
}
if (column.value.uidt === UITypes.Formula) {
nextTick(() => {
duplicateDialogRef?.value?.duplicate()
})
} else {
isDuplicateDlgOpen.value = true
}
isOpen.value = false
}
}
// hide the field in view
const hideField = async () => {
if (isRequired.value) return
isOpen.value = false
emit('hideField')
}
const handleDelete = () => {
// closing the dropdown
// when modal opens
isOpen.value = false
showDeleteColumnModal.value = true
}
const isDeleteAllowed = computed(() => {
return column?.value && !column.value.system
})
const isDuplicateAllowed = computed(() => {
return column?.value && !column.value.system
})
</script>
<template>
<a-dropdown
v-if="!isLocked"
v-model:visible="isOpen"
:trigger="['click']"
placement="bottomLeft"
overlay-class-name="nc-dropdown-form-column-operations !border-1 rounded-lg !shadow-xl"
@click.stop="isOpen = !isOpen"
>
<NcButton
type="secondary"
size="small"
class="nc-form-add-field"
data-testid="nc-form-add-field"
@click.stop="showAddColumnDropdown = true"
>
<component :is="iconMap.threeDotVertical" class="flex-none w-4 h-4" />
</NcButton>
<template #overlay>
<NcMenu class="flex flex-col gap-1 border-gray-200 nc-column-options">
<!-- Todo: Duplicate column with form column settings -->
<!-- eslint-disable vue/no-constant-condition -->
<NcMenuItem v-if="false" :disabled="!isDuplicateAllowed" @click="openDuplicateDlg">
<div class="nc-column-duplicate nc-form-header-menu-item">
<component :is="iconMap.duplicate" />
<!-- Duplicate -->
{{ t('general.duplicate') }}
</div>
</NcMenuItem>
<NcMenuItem :disabled="isRequired" @click="hideField">
<div class="nc-column-insert-before nc-form-header-menu-item">
<component :is="iconMap.eye" class="!w-3.75 !h-3.75" />
<!-- Hide Field -->
{{ $t('general.hideField') }}
</div>
</NcMenuItem>
<template v-if="!column?.pv">
<a-divider class="!my-0" />
<NcMenuItem :disabled="!isDeleteAllowed" class="!hover:bg-red-50" @click="handleDelete">
<div class="nc-column-delete nc-form-header-menu-item text-red-600">
<component :is="iconMap.delete" />
<!-- Delete -->
{{ $t('general.delete') }}
</div>
</NcMenuItem>
</template>
</NcMenu>
</template>
</a-dropdown>
<SmartsheetHeaderDeleteColumnModal
v-model:visible="showDeleteColumnModal"
class="nc-form-column-delete-dropdown"
:on-delete-column="onDelete"
/>
<DlgColumnDuplicate
v-if="column"
ref="duplicateDialogRef"
v-model="isDuplicateDlgOpen"
:column="column"
:extra="selectedColumnExtra"
/>
</template>
<style scoped>
.nc-form-header-menu-item {
@apply flex items-center gap-2;
}
</style>

94
packages/nc-gui/components/smartsheet/form/Layout.vue

@ -0,0 +1,94 @@
<script lang="ts" setup>
import { Pane, Splitpanes } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
const { leftSidebarWidth, windowSize, formRightSidebarState, formRightSidebarWidthPercent } = storeToRefs(useSidebarStore())
const formPreviewSize = computed(() => 100 - formRightSidebarWidthPercent.value)
function onResize(widthPercent: any) {
const sidebarWidth = (widthPercent * (windowSize.value - leftSidebarWidth.value)) / 100
if (sidebarWidth > formRightSidebarState.value.maxWidth) {
formRightSidebarState.value.width = formRightSidebarState.value.maxWidth
} else if (sidebarWidth < formRightSidebarState.value.minWidth) {
formRightSidebarState.value.width = formRightSidebarState.value.minWidth
} else {
formRightSidebarState.value.width = sidebarWidth
}
}
const normalizeSidebarWidth = computed(() => {
if (formRightSidebarState.value.width > formRightSidebarState.value.maxWidth) {
return formRightSidebarState.value.maxWidth
} else if (formRightSidebarState.value.width < formRightSidebarState.value.minWidth) {
return formRightSidebarState.value.minWidth
} else {
return formRightSidebarState.value.width
}
})
</script>
<template>
<Splitpanes
class="nc-form-right-sidebar-content-resizable-wrapper w-full h-full"
@resize="(event: any) => onResize(event[1].size)"
>
<Pane :size="formPreviewSize" class="flex-1 h-full">
<slot name="preview" />
</Pane>
<Pane
min-size="15%"
class="nc-sidebar-splitpane relative"
:size="formRightSidebarWidthPercent"
:style="{
minWidth: `${formRightSidebarState.minWidth}px !important`,
maxWidth: `${normalizeSidebarWidth}px !important`,
}"
>
<slot name="sidebar" />
</Pane>
</Splitpanes>
</template>
<style lang="scss">
/** Split pane CSS */
.nc-form-right-sidebar-content-resizable-wrapper > {
.splitpanes__splitter {
@apply !w-0 relative overflow-visible;
}
.splitpanes__splitter:before {
@apply bg-gray-200 w-0.25 absolute left-0 top-0 h-full z-40;
content: '';
}
.splitpanes__splitter:hover:before {
@apply bg-scrollbar;
width: 3px !important;
left: 0px;
}
.splitpanes--dragging .splitpanes__splitter:before {
@apply bg-scrollbar;
width: 3px !important;
left: 0px;
}
.splitpanes--dragging .splitpanes__splitter {
@apply w-1 mr-0;
}
}
.splitpanes__pane {
transition: width 0.15s ease-in-out !important;
}
.splitpanes--dragging {
cursor: col-resize;
> .splitpanes__pane {
transition: none !important;
}
}
</style>

33
packages/nc-gui/components/smartsheet/form/LimitOptions.vue

@ -8,15 +8,16 @@ import { MetaInj, iconMap } from '#imports'
const props = defineProps<{
modelValue: FormFieldsLimitOptionsType[]
formFieldState?: string | null
column: ColumnType
isRequired?: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const emits = defineEmits(['update:modelValue', 'update:formFieldState'])
const meta = inject(MetaInj)!
const column = toRef(props, 'column')
const { column, formFieldState } = toRefs(props)
const basesStore = useBases()
@ -55,7 +56,7 @@ const vModel = computed({
.sort((a, b) => a.order - b.order)
if ((props.modelValue || []).length !== collaborators.length) {
emit(
emits(
'update:modelValue',
collaborators.map((o) => ({ id: o.id, order: o.order, show: o.show })),
)
@ -78,7 +79,7 @@ const vModel = computed({
})
if ((props.modelValue || []).length !== ((column.value.colOptions as SelectOptionsType)?.options || []).length) {
emit(
emits(
'update:modelValue',
updateModelValue.map((o) => ({ id: o.id, order: o.order, show: o.show })),
)
@ -88,10 +89,24 @@ const vModel = computed({
return []
},
set: (val) => {
emit(
const fieldState = (formFieldState.value || '').split(',')
const optionsToRemoveFromFieldState: string[] = []
emits(
'update:modelValue',
val.map((o) => ({ id: o.id, order: o.order, show: o.show })),
val.map((o) => {
if (!o.show) {
if (column.value.uidt === UITypes.User && fieldState.includes(o.id)) {
optionsToRemoveFromFieldState.push(o.id)
} else if (o?.title && fieldState.includes(o.title)) {
optionsToRemoveFromFieldState.push(o.title)
}
}
return { id: o.id, order: o.order, show: o.show }
}),
)
emits('update:formFieldState', fieldState.filter((o) => !optionsToRemoveFromFieldState.includes(o)).join(','))
},
})
@ -162,7 +177,7 @@ const showOrHideAll = (showAll: boolean) => {
</div>
<div v-if="vModel.length" class="flex items-stretch gap-2 pr-2 pl-3 py-1.5 rounded-t-lg border-1 border-b-0 border-gray-200">
<NcTooltip class="truncate max-w-full" :disabled="!isRequired">
<NcTooltip :disabled="!isRequired">
<template #title> {{ $t('msg.info.preventHideAllOptions') }} </template>
<NcButton
@ -172,7 +187,7 @@ const showOrHideAll = (showAll: boolean) => {
:disabled="isRequired || vModel.filter((o) => !o.show).length === vModel.length"
@click="showOrHideAll(false)"
>
Hide all
{{ $t('general.hideAll') }}
</NcButton>
</NcTooltip>
@ -184,7 +199,7 @@ const showOrHideAll = (showAll: boolean) => {
:disabled="vModel.filter((o) => o.show).length === vModel.length"
@click="showOrHideAll(true)"
>
Show all
{{ $t('general.showAll') }}
</NcButton>
</div>
</div>

8
packages/nc-gui/components/smartsheet/header/DeleteColumnModal.vue

@ -1,9 +1,10 @@
<script lang="ts" setup>
import type { LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, isLinksOrLTAR } from 'nocodb-sdk'
import { RelationTypes, isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk'
const props = defineProps<{
visible: boolean
onDeleteColumn?: () => void
}>()
const emits = defineEmits(['update:visible'])
@ -46,6 +47,8 @@ const onDelete = async () => {
$e('a:column:delete')
visible.value = false
props.onDeleteColumn?.()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
@ -58,7 +61,8 @@ const onDelete = async () => {
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.column')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="column" class="flex flex-row items-center py-2 px-3 bg-gray-50 rounded-lg text-gray-700 mb-4">
<SmartsheetHeaderCellIcon class="nc-view-icon"></SmartsheetHeaderCellIcon>
<SmartsheetHeaderVirtualCellIcon v-if="isVirtualCol(column)" class="nc-view-icon"></SmartsheetHeaderVirtualCellIcon>
<SmartsheetHeaderCellIcon v-else class="nc-view-icon"></SmartsheetHeaderCellIcon>
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.5"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"

4
packages/nc-gui/components/smartsheet/toolbar/CalendarMode.vue

@ -44,7 +44,7 @@ watch(activeCalendarView, () => {
<div
v-for="mode in ['day', 'week', 'month', 'year']"
:key="mode"
v-e="`['c:calendar:change-calendar-range-${mode}']`"
v-e="`['c:calendar:change-calendar-view-${mode}']`"
:class="{ active: activeCalendarView === mode }"
:data-testid="`nc-calendar-view-mode-${mode}`"
class="tab"
@ -64,7 +64,7 @@ watch(activeCalendarView, () => {
<NcMenuItem
v-for="mode in ['day', 'week', 'month', 'year']"
:key="mode"
v-e="`['c:calendar:change-calendar-range-${mode}']`"
v-e="`['c:calendar:change-calendar-view-${mode}']`"
@click="changeCalendarView(mode)"
>
{{ $t(`objects.${mode}`) }}

18
packages/nc-gui/components/smartsheet/toolbar/CalendarRange.vue

@ -92,15 +92,14 @@ const dateFieldOptions = computed<SelectProps['options']>(() => {
})) ?? []
)
})
/* const addCalendarRange = async () => {
const addCalendarRange = async () => {
_calendar_ranges.value.push({
fk_from_column_id: dateFieldOptions.value![0].value as string,
fk_to_column_id: null,
})
await saveCalendarRanges()
}
/*
const removeRange = async (id: number) => {
_calendar_ranges.value = _calendar_ranges.value.filter((_, i) => i !== id)
await saveCalendarRanges()
@ -215,10 +214,17 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</NcButton>
-->
</div>
<!-- <NcButton class="mt-2" data-testid="nc-calendar-range-add-btn" size="small" type="secondary" @click="addCalendarRange">
<NcButton
v-if="_calendar_ranges.length === 0"
class="mt-2"
data-testid="nc-calendar-range-add-btn"
size="small"
type="secondary"
@click="addCalendarRange"
>
<component :is="iconMap.plus" />
Add another date field
</NcButton> -->
Add date field
</NcButton>
</div>
</template>
</NcDropdown>

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

@ -133,7 +133,9 @@ const onMove = async (_event: { moved: { newIndex: number; oldIndex: number } },
)
await loadViewColumns()
reloadViewDataHook?.trigger()
await reloadViewDataHook?.trigger({
shouldShowLoading: false,
})
$e('a:fields:reorder')
} catch (e) {
@ -406,34 +408,34 @@ useMenuCloseOnEsc(open)
:class="{
'!bg-gray-800 !text-white': field.bold,
}"
class="!rounded-r-none"
class="!rounded-r-none !w-5 !h-5"
size="xxsmall"
type="secondary"
@click.stop="toggleFieldStyles(field, 'bold', !field.bold)"
>
<component :is="iconMap.bold" />
<component :is="iconMap.bold" class="!w-3 !h-3" />
</NcButton>
<NcButton
:class="{
'!bg-gray-800 !text-white': field.italic,
}"
class="!rounded-x-none !border-x-0"
class="!rounded-x-none !border-x-0 !w-5 !h-5"
size="xxsmall"
type="secondary"
@click.stop="toggleFieldStyles(field, 'italic', !field.italic)"
>
<component :is="iconMap.italic" />
<component :is="iconMap.italic" class="!w-3 !h-3" />
</NcButton>
<NcButton
:class="{
'!bg-gray-800 !text-white': field.underline,
}"
class="!rounded-l-none"
class="!rounded-l-none !w-5 !h-5"
size="xxsmall"
type="secondary"
@click.stop="toggleFieldStyles(field, 'underline', !field.underline)"
>
<component :is="iconMap.underline" />
<component :is="iconMap.underline" class="!w-3 !h-3" />
</NcButton>
</div>
<NcSwitch :checked="field.show" :disabled="field.isViewEssentialField" @change="$t('a:fields:show-hide')" />
@ -499,4 +501,8 @@ useMenuCloseOnEsc(open)
// :deep(.ant-checkbox) {
// @apply top-auto;
// }
:deep(.xxsmall) {
@apply !min-w-0;
}
</style>

6
packages/nc-gui/components/tabs/Smartsheet.vue

@ -56,7 +56,7 @@ const { isGallery, isGrid, isForm, isKanban, isLocked, isMap, isCalendar } = use
useSqlEditor()
const reloadEventHook = createEventHook()
const reloadViewDataEventHook = createEventHook()
const reloadViewMetaEventHook = createEventHook<void | boolean>()
@ -70,7 +70,7 @@ useProvideCalendarViewStore(meta, activeView)
provide(MetaInj, meta)
provide(ActiveViewInj, activeView)
provide(IsLockedInj, isLocked)
provide(ReloadViewDataHookInj, reloadEventHook)
provide(ReloadViewDataHookInj, reloadViewDataEventHook)
provide(ReloadViewMetaHookInj, reloadViewMetaEventHook)
provide(OpenNewRecordFormHookInj, openNewRecordFormHook)
provide(FieldsInj, fields)
@ -81,7 +81,7 @@ provide(
computed(() => !isUIAllowed('dataEdit')),
)
useProvideViewColumns(activeView, meta, () => reloadEventHook?.trigger())
useProvideViewColumns(activeView, meta, () => reloadViewDataEventHook?.trigger())
const grid = ref()

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

@ -158,11 +158,13 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
}"
>
<div
class="flex items-start gap-1.5 w-full h-full py-[3px]"
class="flex gap-1.5 w-full h-full py-[3px]"
:class="{
'flex-wrap': rowHeight !== 1 && !isAttachment(lookupColumn),
'!overflow-x-auto nc-cell-lookup-scroll nc-scrollbar-x-md !overflow-y-hidden':
rowHeight === 1 || isAttachment(lookupColumn),
'items-center': rowHeight === 1,
'items-start': rowHeight !== 1,
}"
>
<div
@ -187,10 +189,19 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
:edit-enabled="false"
:virtual="true"
:read-only="true"
:class="{
'min-h-0 min-w-0': isAttachment(lookupColumn),
'!min-w-20 !w-auto px-2': !isAttachment(lookupColumn),
}"
:class="[
`${
[UITypes.MultiSelect, UITypes.SingleSelect, UITypes.User].includes(lookupColumn.uidt)
? 'pl-2'
: !isAttachment(lookupColumn)
? 'px-2'
: ''
}`,
{
'min-h-0 min-w-0': isAttachment(lookupColumn),
'!w-auto ': !isAttachment(lookupColumn),
},
]"
/>
</div>
</div>

15
packages/nc-gui/components/virtual-cell/components/LinkedItems.vue

@ -59,6 +59,7 @@ const {
link,
meta,
headerDisplayValue,
resetChildrenListOffsetCount,
} = useLTARStoreOrThrow()
const { isNew, state, removeLTARRef, addLTARRef } = useSmartsheetRowStoreOrThrow()
@ -69,6 +70,11 @@ watch(
if ((nextVal[0] || nextVal[1]) && !isNew.value) {
loadChildrenList()
}
// reset offset count when closing modal
if (!nextVal[0]) {
resetChildrenListOffsetCount()
}
},
{ immediate: true },
)
@ -207,9 +213,16 @@ watch(childrenListPagination, () => {
})
onUnmounted(() => {
resetChildrenListOffsetCount()
childrenListPagination.query = ''
window.removeEventListener('keydown', linkedShortcuts)
})
const onFilterChange = () => {
childrenListPagination.page = 1
// reset offset count when filter changes
resetChildrenListOffsetCount()
}
</script>
<template>
@ -242,7 +255,7 @@ onUnmounted(() => {
:placeholder="`Search in ${relatedTableMeta?.title}`"
class="w-full !sm:rounded-md xs:min-h-8 !xs:rounded-xl"
size="small"
@change="childrenListPagination.page = 1"
@change="onFilterChange"
@keydown.capture.stop="
(e) => {
if (e.key === 'Escape') {

1
packages/nc-gui/components/virtual-cell/components/UnLinkedItems.vue

@ -256,6 +256,7 @@ onMounted(() => {
})
onUnmounted(() => {
resetChildrenExcludedOffsetCount()
childrenExcludedListPagination.query = ''
window.removeEventListener('keydown', linkedShortcuts)
})

13
packages/nc-gui/components/workspace/InviteSection.vue

@ -4,6 +4,7 @@ import type { RoleLabels } from 'nocodb-sdk'
import { OrderedWorkspaceRoles, WorkspaceUserRoles } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, useWorkspace } from '#imports'
import { validateEmail } from '~/utils/validation'
import { extractEmail } from '~/helpers/parsers/parserHelpers'
const inviteData = reactive({
email: '',
@ -42,13 +43,17 @@ const insertOrUpdateString = (str: string) => {
emailBadges.value.push(str)
}
const emailInputValidation = (input: string): boolean => {
const emailInputValidation = (input: string, isBulkEmailCopyPaste: boolean = false): boolean => {
if (!input.length) {
if (isBulkEmailCopyPaste) return false
emailValidation.isError = true
emailValidation.message = 'Email should not be empty'
return false
}
if (!validateEmail(input.trim())) {
if (isBulkEmailCopyPaste) return false
emailValidation.isError = true
emailValidation.message = 'Invalid Email'
return false
@ -164,6 +169,8 @@ onKeyStroke('Backspace', () => {
// when bulk email is pasted
const onPaste = (e: ClipboardEvent) => {
emailValidation.isError = false
const pastedText = e.clipboardData?.getData('text')
const inputArray = pastedText?.split(',') || pastedText?.split(' ')
@ -175,7 +182,9 @@ const onPaste = (e: ClipboardEvent) => {
}
inputArray?.forEach((el) => {
const isEmailIsValid = emailInputValidation(el)
el = extractEmail(el) || el
const isEmailIsValid = emailInputValidation(el, inputArray.length > 1)
if (!isEmailIsValid) return

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

@ -312,7 +312,8 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
})
async function loadMoreSidebarData(params: Parameters<Api<any>['dbViewRow']['list']>[4] = {}) {
if ((!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic.value) return
if (((!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic.value) || !calendarRange.value?.length)
return
if (isSidebarLoading.value) return
try {
const response = !isPublic.value
@ -339,7 +340,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
}
const fetchActiveDates = async () => {
if (!base?.value?.id || !meta.value?.id || !viewMeta.value?.id || !calendarRange.value) return
if (!base?.value?.id || !meta.value?.id || !viewMeta.value?.id || !calendarRange.value?.length) return
let prevDate: dayjs.Dayjs | string | null = null
let nextDate: dayjs.Dayjs | string | null = null
let fromDate: dayjs.Dayjs | string | null = null
@ -432,8 +433,9 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
}
}
async function loadCalendarData() {
if ((!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic?.value) return
async function loadCalendarData(showLoading = true) {
if (((!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic?.value) || !calendarRange.value?.length)
return
if (activeCalendarView.value === 'year') {
return
@ -479,7 +481,7 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
nextDate = nextDate!.format('YYYY-MM-DD HH:mm:ssZ')
try {
isCalendarDataLoading.value = true
if (showLoading) isCalendarDataLoading.value = true
const res = !isPublic.value
? await api.dbCalendarViewRow.list(
@ -592,10 +594,10 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
}
}
const loadSidebarData = async () => {
if (!base?.value?.id || !meta.value?.id || !viewMeta.value?.id) return
const loadSidebarData = async (showLoading = true) => {
if (!base?.value?.id || !meta.value?.id || !viewMeta.value?.id || !calendarRange.value?.length) return
try {
isSidebarLoading.value = true
if (showLoading) isSidebarLoading.value = true
const res = !isPublic.value
? await api.dbViewRow.list('noco', base.value.id!, meta.value!.id!, viewMeta.value.id, {
...queryParams.value,

2
packages/nc-gui/composables/useCommandPalette/commands.ts

@ -32,7 +32,7 @@ export const homeCommands = [
parent: 'user',
section: 'Community',
handler: () => {
navigateTo('https://discord.gg/8jX2GQn', { external: true })
navigateTo('https://discord.gg/5RgZmkW', { external: true })
},
},
{

7
packages/nc-gui/composables/useLTARStore.ts

@ -554,6 +554,10 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
childrenExcludedOffsetCount.value = 0;
}
const resetChildrenListOffsetCount = () => {
childrenListOffsetCount.value = 0
}
return {
relatedTableMeta,
loadRelatedTableMeta,
@ -584,7 +588,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
getRelatedTableRowId,
headerDisplayValue,
relatedTableDisplayValuePropId,
resetChildrenExcludedOffsetCount
resetChildrenExcludedOffsetCount,
resetChildrenListOffsetCount,
}
},
'ltar-store',

6
packages/nc-gui/composables/useLoadingIndicator/index.ts

@ -18,7 +18,7 @@ export function useLoadingIndicator({ duration = 2000, throttle = 200 }: UseLoad
progress.value = 0
isLoading.value = true
if (throttle) {
if (process.client) {
if (import.meta.client) {
_throttle = setTimeout(_startTimer, throttle)
}
} else {
@ -44,7 +44,7 @@ export function useLoadingIndicator({ duration = 2000, throttle = 200 }: UseLoad
function _hide() {
clear()
if (process.client) {
if (import.meta.client) {
setTimeout(() => {
isLoading.value = false
setTimeout(() => {
@ -55,7 +55,7 @@ export function useLoadingIndicator({ duration = 2000, throttle = 200 }: UseLoad
}
function _startTimer() {
if (process.client) {
if (import.meta.client) {
_timer = setInterval(() => {
_increase(step.value)
}, 100)

7
packages/nc-gui/composables/useMultiSelect/convertCellData.ts

@ -3,6 +3,7 @@ import type { AttachmentType, ColumnType, LinkToAnotherRecordType, SelectOptions
import { UITypes, getDateFormat, getDateTimeFormat, populateUniqueFileName } from 'nocodb-sdk'
import type { AppInfo } from '~/composables/useGlobal'
import { isBt, isMm, isOo, parseProp } from '#imports'
import { extractEmail } from '~/helpers/parsers/parserHelpers'
export default function convertCellData(
args: { to: UITypes; value: string; column: ColumnType; appInfo: AppInfo; files?: FileList | File[]; oldValue?: unknown },
@ -291,6 +292,12 @@ export default function convertCellData(
throw new Error(`Unsupported conversion for ${to}`)
}
}
case UITypes.Email: {
if (parseProp(column.meta).validate) {
return extractEmail(value) || value
}
return value
}
case UITypes.Lookup:
case UITypes.Rollup:
case UITypes.Formula:

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

@ -16,11 +16,13 @@ import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } f
import { isString } from '@vue/shared'
import { filterNullOrUndefinedObjectProperties } from '~/helpers/parsers/parserHelpers'
import {
NcErrorType,
PreFilledMode,
SharedViewPasswordInj,
computed,
createEventHook,
extractSdkResponseErrorMsg,
extractSdkResponseErrorMsgv2,
isNumericFieldType,
isValidURL,
message,
@ -176,13 +178,16 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
handlePreFillForm()
} catch (e: any) {
const error = await extractSdkResponseErrorMsgv2(e)
if (e.response && e.response.status === 404) {
notFound.value = true
// TODO - handle invalidSharedViewPassword
} else if (await extractSdkResponseErrorMsg(e)) {
} else if (error.error === NcErrorType.INVALID_SHARED_VIEW_PASSWORD) {
passwordDlg.value = true
if (password.value && password.value !== '') passwordError.value = 'Something went wrong. Please check your credentials.'
if (password.value && password.value !== '') {
passwordError.value = error.message
}
}
}
}

3
packages/nc-gui/composables/useTableNew.ts

@ -23,10 +23,11 @@ import {
} from '#imports'
export function useTableNew(param: { onTableCreate?: (tableMeta: TableType) => void; baseId: string; sourceId?: string }) {
const table = reactive<{ title: string; table_name: string; columns: string[] }>({
const table = reactive<{ title: string; table_name: string; columns: string[]; is_hybrid: boolean }>({
title: '',
table_name: '',
columns: SYSTEM_COLUMNS,
is_hybrid: true,
})
const { t } = useI18n()

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

@ -191,7 +191,7 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
if (!disableDataReload) {
await loadViewColumns()
reloadData?.()
reloadData?.({ shouldShowLoading: false })
}
}

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

@ -1,8 +1,18 @@
import { UITypes } from 'nocodb-sdk'
import isURL from 'validator/lib/isURL'
// This regex pattern matches email addresses by looking for sequences that start with characters before the "@" symbol, followed by the domain.
// It's designed to capture most email formats, including those with periods and "+" symbols in the local part.
const validateEmail = (v: string) =>
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i.test(v)
export const extractEmail = (v: string) => {
const matches = v.match(
/(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})/i,
)
return matches ? matches[0] : null
}
const booleanOptions = [
{ checked: true, unchecked: false },
{ 'x': true, '': false },

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown-Liste",
"list": "Liste",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Tag",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",
@ -699,7 +701,7 @@
"hideNocodbBranding": "Hide NocoDB Branding",
"showOnConditions": "Show on condtions",
"showFieldOnConditionsMet": "Shows field only when conditions are met",
"limitOptions": "Limit ptions",
"limitOptions": "Limit options",
"limitOptionsSubtext": "Limit options visible to users by selecting available options",
"clearSelection": "Clear selection"
},
@ -972,7 +974,7 @@
"clientKey": "Select .key file",
"clientCert": "Select .cert file",
"clientCA": "Select CA file",
"changeIconColour": "Change Icon Colour",
"changeIconColour": "Change icon colour",
"preFillFormInfo": "To get a prefilled link, make sure you’ve filled the necessary fields in the form view builder.",
"surveyFormInfo": "Form mode with one field per page"
},
@ -1122,7 +1124,7 @@
"selectFieldToGroup": "Select Field to Group",
"thereAreNoRecordsInTable": "There are no records in table",
"createWebhookMsg1": "Get started with web-hooks!",
"createWebhookMsg2": "Power you automations. Get notified as soon as there are changes in your data",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following",
"areYouSureUWantToDeleteLabel": "Are you sure you want to {deleteLabel} the following",
"idColumnRequired": "ID field is required, you can rename this later if required.",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Día",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "Liste",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Jour",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Rozwijane menu",
"list": "Lista",
"apply": "Zastosuj"
"apply": "Zastosuj",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Dzień",
@ -339,7 +341,7 @@
"removeFile": "Usuń plik",
"hasMany": "Ma wiele",
"manyToMany": "Wiele do wielu",
"oneToOne": "One to One",
"oneToOne": "Jeden do jednego",
"virtualRelation": "Relacja wirtualna",
"linkMore": "Połącz więcej",
"linkMoreRecords": "Połącz więcej rekordów",
@ -699,7 +701,7 @@
"hideNocodbBranding": "Ukryj branding NocoDB",
"showOnConditions": "Pokaż na warunkach",
"showFieldOnConditionsMet": "Pokazuje pole tylko, gdy spełnione są warunki",
"limitOptions": "Ogranicz opcje",
"limitOptions": "Limit options",
"limitOptionsSubtext": "Ogranicz opcje widoczne dla użytkowników, wybierając dostępne opcje",
"clearSelection": "Wyczyść wybór"
},

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Aplicar"
"apply": "Aplicar",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Dia",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Dia",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,13 +198,15 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",
"week": "Week",
"month": "Month",
"year": "Year",
"day": "Dag",
"week": "Vecka",
"month": "Månad",
"year": "År",
"workspace": "Workspace",
"workspaces": "Workspaces",
"project": "Projekt",
@ -449,7 +451,7 @@
"previous": "Previous",
"nextMonth": "Next Month",
"previousMonth": "Previous Month",
"next": "Next",
"next": "Nästa",
"organiseBy": "Organise by",
"heading1": "Heading 1",
"heading2": "Heading 2",
@ -487,7 +489,7 @@
"enterDefaultUrlOptional": "Enter default URL (Optional)",
"negative": "Negative",
"discard": "Discard",
"default": "Default",
"default": "Standard",
"defaultNumberPercent": "Default Number (%)",
"durationFormat": "Duration Format",
"dateFormat": "Date Format",
@ -509,7 +511,7 @@
"searchUsers": "Search Users",
"superAdmin": "Super Admin",
"allTables": "All Tables",
"members": "Members",
"members": "Medlemmar",
"dataSources": "Data Sources",
"connectDataSource": "Connect a Data Source",
"searchProjects": "Search Bases",
@ -695,7 +697,7 @@
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
},
"appearanceSettings": "Appearance Settings",
"backgroundColor": "Background Color",
"backgroundColor": "Bakgrundsfärg",
"hideNocodbBranding": "Hide NocoDB Branding",
"showOnConditions": "Show on conditions",
"showFieldOnConditionsMet": "Shows field only when conditions are met",
@ -713,7 +715,7 @@
"toggleSidebar": "Toggle Sidebar",
"addEndDate": "Add end date",
"withEndDate": "with end date",
"calendar": "Calendar",
"calendar": "Kalender",
"viewSettings": "View settings",
"googleOAuth": "Google OAuth",
"registerOIDC": "Register OIDC Identity Provider",
@ -748,7 +750,7 @@
"surveyMode": "Survey Mode",
"rtlOrientation": "RTL Orientation",
"useTheme": "Use Theme",
"copyLink": "Copy Link",
"copyLink": "Kopiera länk",
"copiedLink": "Link Copied",
"copyInviteLink": "Copy invite link",
"copiedInviteLink": "Copied invite link",
@ -901,7 +903,7 @@
"deleteRecord": "Ta bort en post",
"fullWidth": "Full width",
"exitFullWidth": "Exit full width",
"markAllAsRead": "Mark all as read",
"markAllAsRead": "Markera alla som lästa",
"column": {
"delete": "Delete Field",
"addNumber": "Add Number Field",
@ -1045,7 +1047,7 @@
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number",
"invalidPhoneNumber": "Ogiltigt telefonnummer",
"pageSizeChanged": "Page size changed",
"errorLoadingData": "Error loading data",
"webhookBodyMsg1": "Use context variable",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -176,19 +176,19 @@
"inactive": "Неактивний",
"linked": "прив'язаний",
"finish": "Закінчити",
"min": "Min",
"max": "Max",
"avg": "Avg",
"sum": "Sum",
"count": "Count",
"countDistinct": "Count Distinct",
"sumDistinct": "Sum Distinct",
"avgDistinct": "Avg Distinct",
"join": "Join",
"options": "Options",
"primaryValue": "Primary Value",
"useSurveyMode": "Use Survey Mode",
"shift": "Shift",
"min": "Мінімум",
"max": "Максимум",
"avg": "Середнє",
"sum": "Сума",
"count": "Кількість",
"countDistinct": "Підрахувати унікальні",
"sumDistinct": "Сума унікальних",
"avgDistinct": "Середнє унікальних",
"join": "Приєднатись",
"options": "Параметри",
"primaryValue": "Головне значення",
"useSurveyMode": "Використовувати режим опитування",
"shift": "Зміна",
"enter": "Enter",
"seconds": "Seconds",
"paste": "Paste",
@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

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

@ -193,18 +193,20 @@
"seconds": "秒",
"paste": "粘贴",
"restore": "还原",
"replace": "Replace",
"banner": "Banner",
"replace": "替换",
"banner": "Banner",
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"dropdown": "下拉",
"list": "列表",
"apply": "应用",
"text": "文本",
"appearance": "外观"
},
"objects": {
"day": "Day",
"week": "Week",
"month": "Month",
"year": "Year",
"day": "",
"week": "",
"month": "",
"year": "",
"workspace": "工作区",
"workspaces": "工作区",
"project": "项目",
@ -311,7 +313,7 @@
"isNotNull": "不为空(Not Null)"
},
"title": {
"sso": "Authentication (SSO)",
"sso": "身份验证 (SSO)",
"docs": "文档",
"forum": "论坛",
"parameter": "参数",
@ -339,11 +341,11 @@
"removeFile": "删除文件",
"hasMany": "单对多",
"manyToMany": "多对多",
"oneToOne": "One to One",
"oneToOne": "一对一",
"virtualRelation": "虚拟关系",
"linkMore": "链接更多",
"linkMoreRecords": "链接更多记录",
"linkRecords": "Link Records",
"linkRecords": "关联的记录",
"downloadFile": "下载文件",
"renameTable": "重命名表格",
"renamingTable": "重新命名表格",
@ -410,7 +412,7 @@
"findRowByScanningCode": "通过扫描二维码或条码查找行",
"tokenManagement": "Token 管理",
"addNewToken": "添加新令牌(Token)",
"createNewToken": "Create new token",
"createNewToken": "创建新令牌",
"accountSettings": "账户设置",
"resetPasswordMenu": "密码重置",
"tokens": "令牌",
@ -429,28 +431,28 @@
"setNull": "设置 NULL",
"setDefault": "设为默认"
},
"selectFieldsFromRightPannelToAddHere": "Select fields from right panel to add here",
"noOptionsFound": "No options found",
"surveyFormSubmitConfirmMsg": "Are you sure you want to submit this form?"
"selectFieldsFromRightPannelToAddHere": "从右侧面板选择要添加的字段",
"noOptionsFound": "未找到选项",
"surveyFormSubmitConfirmMsg": "您确定要提交吗?"
},
"labels": {
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
"metadataUrl": "Metadata URL",
"audience-entityId": "Audience/ Entity ID",
"redirectUrl": "Redirect URL",
"oidc": "OpenID Connect (OIDC)",
"selectYear": "选择年份",
"save": "保存",
"cancel": "取消",
"metadataUrl": "元数据 URL",
"audience-entityId": "受众/实体 ID",
"redirectUrl": "跳转链接",
"oidc": "OpenID协议(OIDC)",
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"ssoSettings": "SSO Settings",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
"previousMonth": "Previous Month",
"next": "Next",
"organiseBy": "Organise by",
"newProvider": "连接服务",
"generalSettings": "通用设置",
"ssoSettings": "SSO设置",
"organizeBy": "组织方式",
"previous": "返回",
"nextMonth": "下个月",
"previousMonth": "上个月",
"next": "下一页",
"organiseBy": "组织方式",
"heading1": "一级标题",
"heading2": "二级标题",
"heading3": "三级标题",
@ -494,8 +496,8 @@
"timeFormat": "时间格式",
"singularLabel": "单数标签",
"pluralLabel": "复数标签",
"selectDateField": "Select a date field",
"endDateField": "End date field",
"selectDateField": "选择日期字段",
"endDateField": "结束日期",
"optional": "(可选)",
"clickToMake": "点击创建",
"visibleForRole": "角色可见:",
@ -538,9 +540,9 @@
"duplicateFormView": "复制表单视图",
"createFormView": "创建表格视图",
"duplicateKanbanView": "复制看板视图",
"duplicateCalendarView": "Duplicate Calendar View",
"duplicateCalendarView": "复制日历视图",
"createKanbanView": "创建看板视图",
"createCalendarView": "Create Calendar View",
"createCalendarView": "创建日历视图",
"viewName": "查看名称",
"viewLink": "查看链接",
"columnName": "列名",
@ -551,7 +553,7 @@
"databaseType": "数据库中的类型",
"lengthValue": "长度/值",
"dbType": "数据库类型",
"servername": "servername / hostAddr",
"servername": "服务器地址",
"sqliteFile": "SQLite 文件",
"hostAddress": "服务器地址",
"port": "端口号",
@ -694,30 +696,30 @@
"selectField": "请设置需要查询的字段",
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
},
"appearanceSettings": "Appearance Settings",
"backgroundColor": "Background Color",
"hideNocodbBranding": "Hide NocoDB Branding",
"appearanceSettings": "外观设置",
"backgroundColor": "背景颜色",
"hideNocodbBranding": "隐藏 NocoDB 标识",
"showOnConditions": "Show on conditions",
"showFieldOnConditionsMet": "Shows field only when conditions are met",
"limitOptions": "Limit options",
"limitOptionsSubtext": "Limit options visible to users by selecting available options",
"clearSelection": "Clear selection"
"showFieldOnConditionsMet": "在满足条件时显示字段",
"limitOptions": "限制选项",
"limitOptionsSubtext": "限制用户可见",
"clearSelection": "清除选择"
},
"activity": {
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
"toggleSidebar": "Toggle Sidebar",
"addEndDate": "Add end date",
"withEndDate": "with end date",
"calendar": "Calendar",
"viewSettings": "View settings",
"addMembers": "添加成员",
"enterEmail": "输入邮箱地址",
"inviteToBase": "邀请",
"addMember": "添加用户到项目",
"noRange": "日历视图需要日期范围",
"goToToday": "转到今天",
"toggleSidebar": "切换侧边栏",
"addEndDate": "添加结束日期",
"withEndDate": "结束日期",
"calendar": "日历",
"viewSettings": "视图设置",
"googleOAuth": "Google OAuth",
"registerOIDC": "Register OIDC Identity Provider",
"registerSAML": "Register SAML Identity Provider",
"registerOIDC": "添加 OIDC 身份提供商",
"registerSAML": "添加 SAML 身份提供商",
"openInANewTab": "在新标签页中打开",
"copyIFrameCode": "复制 IFrame 代码",
"onCondition": "条件",
@ -931,17 +933,17 @@
},
"toggleMobileMode": "切换移动模式",
"startCommenting": "开始评论",
"clearForm": "Clear Form",
"addFieldFromFormView": "Add Field",
"selectAllFields": "Select all fields",
"clearForm": "清除表单",
"addFieldFromFormView": "添加字段",
"selectAllFields": "选择所有字段",
"preFilledFields": {
"title": "Enable Pre-fill",
"default": "Default",
"locked": "Lock pre-filled fields as read-only",
"hidden": "Hide pre-filled fields",
"lockedFieldTooltip": "Pre-filled value"
"title": "启用预填充",
"default": "默认",
"locked": "预填充字段只读",
"hidden": "隐藏预填充字段",
"lockedFieldTooltip": "预填值"
},
"getPreFilledLink": "Get Pre-filled Link"
"getPreFilledLink": "获取预填链接"
},
"tooltip": {
"reachedSourceLimit": "暂时只限于一个数据源",
@ -972,9 +974,9 @@
"clientKey": "选择 .key 文件",
"clientCert": "选择 .cert 文件",
"clientCA": "选择 CA 文件",
"changeIconColour": "Change icon colour",
"changeIconColour": "更改图标颜色",
"preFillFormInfo": "Generate share form URL with pre-filled field data. To get a pre-filled link, make sure you’ve filled the necessary fields in the form view builder.",
"surveyFormInfo": "Form mode with one field per page"
"surveyFormInfo": "单页单表单字段模式"
},
"placeholder": {
"selectSlackChannels": "选择 Slack 频道",
@ -989,7 +991,7 @@
"selectGroupField": "选择分组字段",
"selectGroupFieldNotFound": "没有可用的单选字段,请先创建。",
"selectGeoField": "选择地理数据字段",
"notSelected": "-not selected-",
"notSelected": "未选择",
"selectGeoFieldNotFound": "未找到 GeoData 字段。请先创建一个。",
"password": {
"enter": "输入密码",
@ -1021,13 +1023,13 @@
"decimal8": "1.00000000",
"value": "值",
"key": "键",
"createTable": "Create your First Table!",
"createTable": "创建第一张数据表",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreated": "没有创建 API 令牌",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeam": "邀请成员到您的团队",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace.",
"searchOptions": "Search options"
"searchOptions": "搜索选项"
},
"msg": {
"clickToCopyFieldId": "点击复制字段ID",
@ -1112,9 +1114,9 @@
"tooltip_desc": "表中的一条记录 ",
"tooltip_desc2": " 可以从表中链接到单条记录 "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "点击\"引用\"将数据关联到 \"{tableName}\"",
"noRecordsLinked": "没有链接记录",
"noLinkedRecords": "No linked records",
"noLinkedRecords": "暂无引用数据",
"recordsLinked": "链接的记录",
"acceptOnlyValid": "仅接受",
"apiTokenCreate": "创建个人 API tokens,以便在自动化或外部应用程序中使用。",
@ -1122,22 +1124,22 @@
"selectFieldToGroup": "选择要分组的字段",
"thereAreNoRecordsInTable": "表中没有记录",
"createWebhookMsg1": "开始使用Web Hooks!",
"createWebhookMsg2": "Power your automations. Get notified as soon as there are changes in your data",
"createWebhookMsg2": "数据变更自动发送通知",
"areYouSureUWantTo": "您确定要删除以下内容",
"areYouSureUWantToDeleteLabel": "您确定要 {deleteLabel} 以下内容吗?",
"idColumnRequired": "ID 字段是必填字段,如果需要,您可以稍后重新命名。",
"length59Required": "长度超过最大 59 个字符",
"noNewNotifications": "您没有新的通知",
"noRecordFound": "未找到记录",
"noRecordsFound": "No records found",
"noRecordsFound": "没有找到记录",
"rowDeleted": "记录已删除",
"saveChanges": "要保存更改吗?",
"tooLargeFieldEntity": "字段太大,无法转换为 {entity}",
"roleRequired": "角色要求",
"warning": {
"calendarNoFields": "Calendar view requires a date or date time field to be setup. Try setting up a calendar view after adding a date / date time field!",
"kanbanNoFields": "Kanban view requires a single select field to be setup. Try setting up a kanban view after adding a single select field!",
"mapNoFields": "Map view requires a geo data field to be setup. Try setting up a map view after adding a geo data field!",
"kanbanNoFields": "看板视图需要添加一个选择字段",
"mapNoFields": "地图视图需要添加一个位置字段",
"dbValid": "请确保您尝试连接的数据库有效!此操作可能导致模式丢失",
"barcode": {
"renderError": "条形码错误 - 请注意输入数据和条形码类型之间的兼容性"
@ -1156,8 +1158,8 @@
}
},
"info": {
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"idpPaste": "将此URL粘贴到您的身份提供商后台",
"noSaml": "没有配置 SAML 身份验证",
"disabledAsViewLocked": "已禁用,因为视图已锁定",
"basesMigrated": "项目已迁移。请重试。",
"pasteNotSupported": "处于活动状态的单元格不能粘贴内容",
@ -1199,20 +1201,20 @@
"formInput": "输入表单输入标签",
"formHelpText": "添加一些帮助文本",
"onlyCreator": "仅创始人可见",
"formTitle": "Add form Title",
"formTitle": "添加数据表格标题",
"formDesc": "添加表单描述",
"beforeEnablePwd": "使用密码限制访问",
"afterEnablePwd": "访问受密码限制",
"privateLink": "此视图通过私有链接共享",
"privateLinkAdditionalInfo": "拥有私人链接的人只能看到此视图中可见的单元格",
"postFormSubmissionSettings": "Post Form Submission Settings",
"postFormSubmissionSettings": "表单提交设置",
"apiOptions": "访问项目通过",
"submitAnotherForm": "显示“提交另一个表单”按钮",
"showBlankForm": "5 秒后显示空表单",
"emailForm": "发电子邮件给我",
"showSysFields": "显示系统字段",
"filterAutoApply": "自动应用",
"formDisplayMessage": "Display Message",
"formDisplayMessage": "显示消息",
"viewNotShared": "当前视图不共享!",
"showAllViews": "显示此表的所有共享视图",
"collabView": "具有编辑及更高权限的合作者可以更改视图配置。",
@ -1321,27 +1323,27 @@
"submittedFormData": "您已成功提交表单数据。",
"editingSystemKeyNotSupported": "不支持编辑系统密钥",
"notAvailableAtTheMoment": "当前不可用。",
"groupPasteIsNotSupportedOnLinksColumn": "Group paste operation is not supported on Links/LinkToAnotherRecord column",
"groupClearIsNotSupportedOnLinksColumn": "Group clear operation is not supported on Links/LinkToAnotherRecord column",
"upgradeToEnterpriseEdition": "Upgrade to Enterprise Edition {extraInfo}",
"thisFeatureIsOnlyAvailableInEnterpriseEdition": "This feature is only available in enterprise edition",
"yourCurrentRoleIs": "Your current role is",
"pleaseRequestAccessForView": "Please request for higher permission from the Admin / Base owner / Workspace owner to get access to this {viewName}",
"preventHideAllOptions": "You cannot hide all options if field is required"
"groupPasteIsNotSupportedOnLinksColumn": "分组复制操作不支持关联字段",
"groupClearIsNotSupportedOnLinksColumn": "分组清除操作不支持关联字段",
"upgradeToEnterpriseEdition": "升级至企业版 {extraInfo}",
"thisFeatureIsOnlyAvailableInEnterpriseEdition": "此功能仅适用于企业版",
"yourCurrentRoleIs": "您当前的角色是",
"pleaseRequestAccessForView": "请向上级角色申请更高的权限,以访问 {viewName}。",
"preventHideAllOptions": "必填项不能为空"
},
"error": {
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
"issuerRequired": "Issuer is required",
"clientSecretRequired": "Client Secret is required",
"jwkUrlRequired": "JWK URL is required",
"tokenUrlRequired": "Token URL is required",
"userInfoUrlRequired": "UserInfo URL is required",
"eitherXML": "Either xml or metadata url is required",
"fetchingCalendarData": "获取日历数据时出错",
"fetchingActiveDates": "获取活动日期时出错",
"scopesRequired": "范围不能为空",
"authUrlRequired": "鉴权链接不能为空",
"userNameAttributeRequired": "用户名不能为空",
"clientIdRequired": "Client ID 不能为空",
"issuerRequired": "签发不能为空",
"clientSecretRequired": "Client Secret 不能为空",
"jwkUrlRequired": "JWK URL不能为空",
"tokenUrlRequired": "Token链接不能为空",
"userInfoUrlRequired": "用户链接不能为空",
"eitherXML": "XML/元数据链接不能为空",
"nameRequired": "您必须指定一个名字",
"nameMinLength": "名称长度必须至少为 2 个字符",
"nameMaxLength": "名称长度不得超过 60 个字符",
@ -1372,7 +1374,7 @@
"invalidEmails": "邮箱格式错误",
"invalidEmail": "无效的电子邮件"
},
"invalidXml": "Invalid XML",
"invalidXml": "无效的 XML",
"invalidURL": "无效的 URL",
"invalidEmail": "无效的电子邮件",
"internalError": "发生了一些内部错误",
@ -1421,9 +1423,9 @@
"pasteFromClipboardError": "从剪贴板粘贴失败",
"multiFieldSaveValidation": "请在保存前完成所有字段的配置",
"somethingWentWrong": "出现了一些错误",
"draggedContentIsNotTypeOfImage": "Dragged content is not type of image",
"fieldToParseImageData": "Field to parse image data",
"someOfTheRequiredFieldsAreEmpty": "Some of the required fields are empty"
"draggedContentIsNotTypeOfImage": "拖动内容不是图像",
"fieldToParseImageData": "无法解析图像数据",
"someOfTheRequiredFieldsAreEmpty": "必填字段不能为空"
},
"toast": {
"exportMetadata": "项目元数据成功导出",

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

@ -198,7 +198,9 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"apply": "Apply"
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"day": "Day",

17
packages/nc-gui/lib/enums.ts

@ -142,3 +142,20 @@ export enum PreFilledMode {
Hidden = 'hidden',
Locked = 'locked',
}
export enum RichTextBubbleMenuOptions {
bold = 'bold',
italic = 'italic',
underline = 'underline',
strike = 'strike',
code = 'code',
quote = 'quote',
heading1 = 'heading1',
heading2 = 'heading2',
heading3 = 'heading3',
blockQuote = 'blockQuote',
bulletList = 'bulletList',
numberedList = 'numberedList',
taskList = 'taskList',
link = 'link',
}

10
packages/nc-gui/package.json

@ -99,7 +99,7 @@
"vue-qrcode-reader": "3.1.9",
"vue3-calendar-heatmap": "^2.0.5",
"vue3-contextmenu": "^0.2.12",
"vue3-grid-layout-next": "^1.0.6",
"vue3-grid-layout-next": "^1.0.7",
"vue3-text-clamp": "^0.1.2",
"vuedraggable": "^4.1.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz"
@ -114,16 +114,16 @@
"@iconify-json/clarity": "^1.1.12",
"@iconify-json/eva": "^1.1.10",
"@iconify-json/ic": "^1.1.17",
"@iconify-json/ion": "^1.1.16",
"@iconify-json/ion": "^1.1.17",
"@iconify-json/la": "^1.1.8",
"@iconify-json/logos": "^1.1.42",
"@iconify-json/lucide": "^1.1.177",
"@iconify-json/lucide": "^1.1.178",
"@iconify-json/material-symbols": "^1.1.76",
"@iconify-json/mdi": "^1.1.64",
"@iconify-json/mi": "^1.1.8",
"@iconify-json/ph": "^1.1.11",
"@iconify-json/ri": "^1.1.20",
"@iconify-json/simple-icons": "^1.1.96",
"@iconify-json/simple-icons": "^1.1.97",
"@iconify-json/system-uicons": "^1.1.12",
"@iconify-json/tabler": "^1.1.109",
"@iconify-json/vscode-icons": "^1.1.33",
@ -144,7 +144,7 @@
"@types/turndown": "^5.0.4",
"@types/validator": "^13.11.9",
"@types/vue-barcode-reader": "^0.0.3",
"@unocss/nuxt": "^0.58.6",
"@unocss/nuxt": "^0.58.9",
"@vitest/ui": "^0.34.7",
"@vue/compiler-sfc": "^3.4.21",
"@vue/test-utils": "^2.4.5",

18
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue

@ -121,10 +121,17 @@ p {
}
&:not(.readonly) {
input,
textarea,
&.nc-virtual-cell {
@apply bg-white !disabled:bg-transparent;
&:not(.nc-cell-longtext) {
input,
textarea,
&.nc-virtual-cell {
@apply bg-white !disabled:bg-transparent;
}
}
&.nc-cell-longtext {
textarea {
@apply bg-white !disabled:bg-transparent;
}
}
}
@ -188,6 +195,9 @@ p {
@apply !bg-gray-100;
}
}
&.nc-cell-attachment {
@apply h-auto;
}
}
}

5
packages/nc-gui/store/base.ts

@ -104,6 +104,10 @@ export const useBase = defineStore('baseStore', () => {
return getBaseType(sourceId) === 'pg'
}
function isSnowflake(sourceId?: string) {
return getBaseType(sourceId) === 'snowflake'
}
function isXcdbBase(sourceId?: string) {
const source = sources.value.find((source) => source.id === sourceId)
return (source?.is_meta as boolean) || (source?.is_local as boolean) || false
@ -284,6 +288,7 @@ export const useBase = defineStore('baseStore', () => {
isMssql,
isPg,
isSqlite,
isSnowflake,
sqlUis,
isSharedBase,
isSharedErd,

13
packages/nc-gui/store/sidebar.ts

@ -52,6 +52,16 @@ export const useSidebarStore = defineStore('sidebarStore', () => {
const nonHiddenLeftSidebarWidth = computed(() => (width.value * nonHiddenMobileSidebarSize.value) / 100)
const formRightSidebarState = ref({
minWidth: 384,
maxWidth: 600,
width: 384,
})
const formRightSidebarWidthPercent = computed(() => {
return (formRightSidebarState.value.width / (width.value - leftSidebarWidth.value)) * 100
})
return {
isLeftSidebarOpen,
isRightSidebarOpen,
@ -61,6 +71,9 @@ export const useSidebarStore = defineStore('sidebarStore', () => {
leftSidebarWidth,
mobileNormalizedSidebarSize,
nonHiddenLeftSidebarWidth,
windowSize: width,
formRightSidebarState,
formRightSidebarWidthPercent,
}
})

12
packages/nc-gui/utils/baseCreateUtils.ts

@ -137,12 +137,12 @@ const sampleConnectionData: { [key in ConnectionClientType]: DefaultConnection }
useNullAsDefault: true,
},
[ClientType.SNOWFLAKE]: {
account: 'account',
username: 'username',
password: 'password',
warehouse: 'warehouse',
database: 'database',
schema: 'schema',
account: 'LOCATOR.REGION',
username: 'USERNAME',
password: 'PASSWORD',
warehouse: 'COMPUTE_WH',
database: 'DATABASE',
schema: 'PUBLIC',
},
tidb: {
host: defaultHost,

37
packages/nc-gui/utils/errorUtils.ts

@ -1,3 +1,5 @@
import { NcErrorType } from 'nocodb-sdk'
export async function extractSdkResponseErrorMsg(e: Error & { response: any }) {
if (!e || !e.response) return e.message
let msg
@ -21,3 +23,38 @@ export async function extractSdkResponseErrorMsg(e: Error & { response: any }) {
return msg || 'Some error occurred'
}
export async function extractSdkResponseErrorMsgv2(e: Error & { response: any }): Promise<{
error: NcErrorType
message: string
details?: any
}> {
const unknownError = {
error: NcErrorType.UNKNOWN_ERROR,
message: 'Something went wrong',
}
if (!e || !e.response) {
return unknownError
}
if (e.response.data instanceof Blob) {
try {
const parsedError = JSON.parse(await e.response.data.text())
if (parsedError.error && parsedError.error in NcErrorType) {
return parsedError
}
return unknownError
} catch {
return unknownError
}
} else {
if (e.response.data.error && e.response.data.error in NcErrorType) {
return e.response.data
}
return unknownError
}
}
export { NcErrorType }

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

@ -22,7 +22,7 @@ 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.
This function compares two dates and returns the difference in the specified unit. Positive integers indicate that the second date is in the past compared to the first, and vice versa for negative values.
---
@ -94,6 +94,80 @@ Returns the day of the week as an integer between 0 and 6 (inclusive), with Mond
---
## DATESTR
The DATESTR function converts a date or datetime field into a string in "YYYY-MM-DD" format.
#### Syntax
```plaintext
DATESTR(date | datetime)
```
#### Sample
```plaintext
DATESTR('2022-03-14') => 2022-03-14
DATESTR('2022-03-14 12:00:00') => 2022-03-14
```
#### Remark
This function converts a date or datetime field into a string in "YYYY-MM-DD" format, ignoring the time part.
---
## DAY
The DAY function returns the day of the month as an integer.
#### Syntax
```plaintext
DAY(date | datetime)
```
#### Sample
```plaintext
DAY('2022-03-14') => 14
DAY('2022-03-14 12:00:00') => 14
```
#### Remark
This function returns the day of the month as an integer between 1 and 31 (inclusive). Note that the day information retrieved is based on the timezone of the server (GMT by default). If the browser timezone is different from the server timezone, the day value may differ.
---
## MONTH
The MONTH function returns the month of the year as an integer.
#### Syntax
```plaintext
MONTH(date | datetime)
```
#### Sample
```plaintext
MONTH('2022-03-14') => 3
MONTH('2022-03-14 12:00:00') => 3
```
#### Remark
This function returns the month of the year as an integer between 1 and 12 (inclusive). Note that the month information retrieved is based on the timezone of the server (GMT by default). If the browser timezone is different from the server timezone, the month value may differ.
---
## HOUR
The HOUR function returns the hour of the day as an integer.
#### Syntax
```plaintext
HOUR(datetime)
```
#### Sample
```plaintext
HOUR('2022-03-14 12:00:00') => 12
```
#### Remark
This function returns the hour of the day as an integer between 0 and 23 (inclusive). Hour information retrieved is based on a 24-hour clock & will be based on the timezone of the server (GMT by default). Note that, if browser timezone is different from the server timezone, the hour value may differ.
---
## Related Articles
- [Numeric and Logical Operators](015.operators.md)

24
packages/noco-docs/docs/070.fields/040.field-types/060.formula/060.generic-functions.md

@ -0,0 +1,24 @@
---
title: 'Generic Functions'
description: 'This article explains system functions & miscellaneous functions that can be used in formula fields.'
tags: ['Fields', 'Field types', 'Formula']
keywords: ['Fields', 'Field types', 'Formula', 'Create formula field', 'System functions', 'Miscellaneous functions']
---
# System Functions
## RECORD_ID
The `RECORD_ID` function returns the unique identifier of the record.
#### Syntax
```plaintext
RECORD_ID()
```
#### Sample
```plaintext
RECORD_ID() => 1
```
---

1
packages/noco-docs/docs/090.views/010.views-overview.md

@ -21,6 +21,7 @@ View represents data from a table. Any updates to records in a view will be refl
2. [Form View](view-types/form)
3. [Gallery View](view-types/gallery)
4. [Kanban View](view-types/kanban)
5. [Calendar View](view-types/calendar)
## View Permission Types

5
packages/noco-docs/docs/090.views/020.create-view.md

@ -7,14 +7,15 @@ keywords: ['NocoDB view', 'create view']
## Create new view
1. Click on `+` on the left sidebar next to `table name` OR click on `+ New View` button below `table name`.
1. Click on `+ New View` button below `table name`.
2. Select view type from the dropdown modal.
3. Fill view name in the pop-up modal.
- For Kanban view, select the `Single select` field to be used as the Kanban field.
- For Calendar view, select the `Date` OR `DateTime` field to be used as the Calendar field.
4. Click on `Create View` button.
![image](/img/v2/views/create-view-1.png)
![image](/img/v2/views/create-view-2.png)
### Related articles

145
packages/noco-docs/docs/090.views/040.view-types/030.form.md

@ -7,15 +7,16 @@ keywords: ['NocoDB form', 'form view', 'form builder', 'form view builder', 'for
Form View allows you to arrange fields in a form to input data.
![1010-2 Form](/img/v2/views/form-view.png)
![1010-2 Form](/img/v2/views/form-view/form-view.png)
## Form View Builder
Form view builder layout can be divided into 3 sections:
1. **Fields Area** - This is the area where fields available in the tables that are not yet added to the form are listed.
2. **Form Area** - This is the area where fields added to the form are listed.
3. **Form Settings** - This is the area where you can configure the form view. This mainly consists of actions & customisations that can be performed after a form view is submitted.
Form view builder layout can be divided into 4 sections:
1. **Form Area** - This is the area where fields added to the form are listed. This area also acts as a preview of the form.
2. **Form Fields** - This area lists all the fields available in the table. You can drag and drop fields for re-ordering and toggle the `hide` switch to remove fields from the form.
3. **Appearance Settings** - This is the area where you can configure the form view appearance settings like Background Color, Banner & Branding.
4. **Post Form Submission Settings** - This is the area where you can configure the form view to perform various actions after a form is submitted.
![Form Builder](/img/v2/views/form-view-layout.png)
![Form Builder](/img/v2/views/form-view/layout.png)
## Form View Actions
1. [Create a New Form View](/views/create-view/#create-new-view)
@ -28,51 +29,121 @@ Form view builder layout can be divided into 3 sections:
## Form View Operations
### Add Form Title & Description
In the **Form View** area, click on in input boxes provided for **Title** {"<"}1{">"} & **Description** {"<"}2{">"} to add/update title & description to the form.
In the **Form View** area, click on in input boxes provided for **Title** & **Description** to add/update title & description to the form.
:::info
Formatting options are supported for the description field. You can also use markdown to format the text.
:::
![Form Title & Description](/img/v2/views/form-view-title-description.png)
![Form Title & Description](/img/v2/views/form-view/title-description.png)
### Add Fields to the Form
To add a field to the form, either
- Drag and drop the field from the **Fields Area** to the **Form Area** at required position
- Click on the field in the **Fields Area** to add it to the end of the **Form Area**
### Add/Remove Fields
To add/remove a field
- Use toggle switch next to the field name in the **Form Fields** to add/remove a field from the form.
### Change field label & help-text
To change the field label displayed on the form & add help-text, click on the field in the **Form Area** and update the values in the input boxes provided for **Label** {"<"}1{">"} & **Help Text** {"<"}2{">"}.
![Field Label & Help Text](/img/v2/views/form-view-field-label-help-text.png)
### Mark a Field as Required
To mark a field as required, click on the field in the **Form Area** and toggle the `Required` switch.
### Form Appearance Settings
![Required Field](/img/v2/views/form-view-required-field.png)
![Form appearance](/img/v2/views/form-view/appearance.png)
### Rearrange Fields Within the Form
To rearrange fields within the form, drag and drop the field to the required position.
#### Change Background Color
To change the background color of the form, select the required color from the color picker in the **Appearance Settings** panel.
### Remove Fields from the Form
To remove a field from the form, either
- Drag and drop the field from the **Form Area** to the **Fields Area**
- Hover over the field in the **Form Area** and click on the `hide` icon
![Hide Field](/img/v2/views/form-view-remove-field.png)
#### Hide Branding
To hide NocoDB branding from the form, toggle the `Hide NocoDB Branding` switch in the **Appearance Settings** panel.
:::info
This feature is available only in the paid plans.
:::
#### Hide Form Banner
To hide the form banner, toggle the `Hide Banner` switch in the **Appearance Settings** panel.
### Rearrange Fields Within the Form
To rearrange fields within the form, drag and drop the field to the required position. This can be done in both the **Form Area** and **Form Fields** panel.
### Add a New Field to the Table
To add a new field to the table,
- Click on the `+ Add new field to this table` in the **Fields Area** and
- Click on the `+ Add field` in the **Form Fields** panel
- Select the field name & type from the dropdown.
- Click on `Save Field`
![Add Field](/img/v2/views/form-view-add-field.png)
![Add Field](/img/v2/views/form-view/add-new-field.png)
### Form View Settings
NocoDB allows you to configure the form view to perform various actions after a form is submitted. These actions can be configured in the **Form Settings** area.
1. **After Submit Message**: You can configure a message to display on successful submission of the form.
2. **Show `Submit Another Form` Button**: This option when enabled, will display a `Submit Another Form` button after the form is submitted.
3. **Show a Blank form**: This option when enabled, will display a new blank form 5 seconds after the form is submitted.
4. **Email me**: Enable this option to receive an Email after the form is submitted.
### Post Form Submission Settings
NocoDB allows you to configure the form view to perform various actions after a form is submitted. These actions can be configured in the **Post Form Submission Settings** panel.
1. **Show `Submit Another Form` Button**: This option when enabled, will display a `Submit Another Form` button after the form is submitted.
2. **Show a Blank form**: This option when enabled, will display a new blank form 5 seconds after the form is submitted.
3. **Email me**: Enable this option to receive an Email after the form is submitted.
4. **After Submit Message**: You can configure a message to display on successful submission of the form.
![Form View Settings](/img/v2/views/form-view-settings.png)
![Form View Settings](/img/v2/views/form-view/post-submit-settings.png)
:::info
Formatting options are supported for the `After Submit Message` field. You can also use markdown to format the text.
:::
## Field configuration
To change the field label displayed on the form & add help-text, click on the required field in the **Form Area** and on the right side configuration panel, configure
1. **Label** `Opitonal` : Defaults to the field name. This doesn't affect the field name in the table.
2. **Help Text** `Optional`
3. **Required** : Toggle to mark the field as required
![Field Label & Help Text](/img/v2/views/form-view/field-config.png)
:::info
Formatting options are supported for the `Help Text` field. You can also use markdown to format the text.
:::
### Field Type Specific Settings
For select based field types (`Single-Select`, `Multi-Select`, `User`), you can configure the following additional settings:
#### Limit Options
Limit the number of options displayed in the dropdown or list of shared form. This is useful when you have a large number of options & want to limit the number of options displayed in the dropdown or list for the user to select from.
- Use `Hide` button next to the option to hide the option from the dropdown or list.
- Use `Reorder` button associated with a field to reorder the options.
![Limit options](/img/v2/views/form-view/limit-options.png)
#### Options Layout
For select based field types, you can configure the options layout to be displayed as a `Dropdown` or an inline expanded `List`.
![Options Layout](/img/v2/views/form-view/options-layout.png)
## Prefill Form Fields
Prefilling form fields is a way to pre-populate form fields with default values. This can be useful when you want to save time for users by prefilling some fields with default values. The prefilled fields and their values are visible in the URL of the form view & can be manually constructed by ensuring URL parameters are appropriately encoded.
NocoDB provides an easier approach to construct prefilled URLs. One can use the form builder to prefill form fields with default values & auto-generate encoded prefilled URL. Follow the below steps to prefill form fields & generate a prefilled URL -
1. Open the form builder, prefill the required form fields with default values.
2. Click on the `Share` button in the top right corner.
3. Toggle `Enable Public Viewing` button to enable share.
4. Toggle `Enable Prefill` button to enable prefill.
5. Click on the `Copy Link` button to copy the link.
![Prefill](/img/v2/views/form-view/prefill.png)
![Prefill share](/img/v2/views/form-view/prefill-share.png)
:::info
- Prefilled fields override the default values if any were set for this field in the table
- Maximum length of the URL is 8000 characters.
:::
### Prefill modes
1. **Default**: Standard mode. This mode will prefill the form fields with the default values set in the form builder. Users can edit the prefilled fields. When shared, the prefilled fields will be visible in the URL. In the image below, the `Number` field is prefilled with the value `1234`, `Currency` field is prefilled with the value `1000` and `Year` field is prefilled with value `2023`.
![Prefill default](/img/v2/views/form-view/prefill-default.png)
2. **Hide prefilled fields**: This mode will prefill the form fields with the default values set in the form builder but will hide the prefilled fields from the user. When shared, the prefilled fields will be visible in the URL. In the image below, the `Number` field is prefilled with the value `1234`, `Currency` field is prefilled with the value `1000` and `Year` field is prefilled with value `2023`.
![Prefill hide](/img/v2/views/form-view/prefill-hide.png)
3. **Lock prefilled fields as read-only**: This mode will prefill the form fields with the default values set in the form builder and will lock the prefilled fields as read-only. When shared, the prefilled fields will be visible in the URL. In the image below, the `Number` field is prefilled with the value `1234`, `Currency` field is prefilled with the value `1000` and `Year` field is prefilled with value `2023`.
![Prefill lock](/img/v2/views/form-view/prefill-lock.png)
:::info
In any of the modes, the user can still change the prefilled fields by editing the URL.
:::
## Survey Form View
NocoDB supports a special type of form view called `Survey Form View`. This view is ideal for creating surveys & questionnaires. This view can be enabled by toggling the `Survey Mode` switch when creating [shared form view](/views/share-view#share-form-view-options).
@ -81,7 +152,7 @@ When enabled,
- Form input fields will be displayed one at a time.
- Users can navigate between fields using the `<` & `>` buttons.
![Survey Form View](/img/v2/views/survey-form.png)
![Survey Form View](/img/v2/views/form-view/survey-form.png)
## Attaching a file from mobile device
To attach a file from mobile device,

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

Loading…
Cancel
Save