Browse Source

Merge pull request #8601 from nocodb/develop

pull/8603/head 0.207.3
github-actions[bot] 4 months ago committed by GitHub
parent
commit
e305a641ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 11
      packages/nc-gui/assets/nc-icons/at-sign.svg
  2. 7
      packages/nc-gui/assets/nc-icons/strike-through.svg
  3. 144
      packages/nc-gui/components/cell/DatePicker.vue
  4. 324
      packages/nc-gui/components/cell/DateTimePicker.vue
  5. 159
      packages/nc-gui/components/cell/TimePicker.vue
  6. 123
      packages/nc-gui/components/cell/YearPicker.vue
  7. 4
      packages/nc-gui/components/cmd-l/index.vue
  8. 1
      packages/nc-gui/components/dashboard/View.vue
  9. 9
      packages/nc-gui/components/dashboard/settings/BaseAudit.vue
  10. 6
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  11. 11
      packages/nc-gui/components/dashboard/settings/Metadata.vue
  12. 6
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  13. 2
      packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue
  14. 16
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  15. 4
      packages/nc-gui/components/dlg/ProjectAudit.vue
  16. 11
      packages/nc-gui/components/general/Modal.vue
  17. 150
      packages/nc-gui/components/nc/DatePicker.vue
  18. 78
      packages/nc-gui/components/nc/DateWeekSelector.vue
  19. 5
      packages/nc-gui/components/nc/Dropdown.vue
  20. 68
      packages/nc-gui/components/nc/MonthYearSelector.vue
  21. 104
      packages/nc-gui/components/nc/TimeSelector.vue
  22. 2
      packages/nc-gui/components/shared-view/Calendar.vue
  23. 2
      packages/nc-gui/components/shared-view/Gallery.vue
  24. 1
      packages/nc-gui/components/shared-view/Grid.vue
  25. 2
      packages/nc-gui/components/shared-view/Kanban.vue
  26. 2
      packages/nc-gui/components/shared-view/Map.vue
  27. 16
      packages/nc-gui/components/smartsheet/Form.vue
  28. 4
      packages/nc-gui/components/smartsheet/details/Api.vue
  29. 259
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  30. 380
      packages/nc-gui/components/smartsheet/expanded-form/RichComment.vue
  31. 215
      packages/nc-gui/components/smartsheet/expanded-form/RichTextOptions.vue
  32. 69
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  33. 3
      packages/nc-gui/components/smartsheet/grid/Table.vue
  34. 31
      packages/nc-gui/components/smartsheet/header/Cell.vue
  35. 25
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  36. 47
      packages/nc-gui/components/smartsheet/toolbar/Calendar/Range.vue
  37. 246
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  38. 1
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  39. 9
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  40. 11
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  41. 4
      packages/nc-gui/components/tabs/Smartsheet.vue
  42. 6
      packages/nc-gui/components/webhook/Editor.vue
  43. 139
      packages/nc-gui/composables/useExpandedFormStore.ts
  44. 14
      packages/nc-gui/composables/useViewColumns.ts
  45. 2
      packages/nc-gui/composables/useViewFilters.ts
  46. 1
      packages/nc-gui/context/index.ts
  47. 11
      packages/nc-gui/lang/ar.json
  48. 11
      packages/nc-gui/lang/bn_IN.json
  49. 9
      packages/nc-gui/lang/cs.json
  50. 11
      packages/nc-gui/lang/da.json
  51. 11
      packages/nc-gui/lang/de.json
  52. 16
      packages/nc-gui/lang/en.json
  53. 15
      packages/nc-gui/lang/es.json
  54. 13
      packages/nc-gui/lang/eu.json
  55. 11
      packages/nc-gui/lang/fa.json
  56. 11
      packages/nc-gui/lang/fi.json
  57. 355
      packages/nc-gui/lang/fr.json
  58. 11
      packages/nc-gui/lang/he.json
  59. 11
      packages/nc-gui/lang/hi.json
  60. 11
      packages/nc-gui/lang/hr.json
  61. 11
      packages/nc-gui/lang/id.json
  62. 13
      packages/nc-gui/lang/it.json
  63. 11
      packages/nc-gui/lang/ja.json
  64. 9
      packages/nc-gui/lang/ko.json
  65. 11
      packages/nc-gui/lang/lv.json
  66. 11
      packages/nc-gui/lang/nl.json
  67. 11
      packages/nc-gui/lang/no.json
  68. 9
      packages/nc-gui/lang/pl.json
  69. 13
      packages/nc-gui/lang/pt.json
  70. 11
      packages/nc-gui/lang/pt_BR.json
  71. 11
      packages/nc-gui/lang/ru.json
  72. 11
      packages/nc-gui/lang/sk.json
  73. 11
      packages/nc-gui/lang/sl.json
  74. 11
      packages/nc-gui/lang/sv.json
  75. 11
      packages/nc-gui/lang/th.json
  76. 9
      packages/nc-gui/lang/tr.json
  77. 11
      packages/nc-gui/lang/uk.json
  78. 11
      packages/nc-gui/lang/vi.json
  79. 25
      packages/nc-gui/lang/zh-Hans.json
  80. 11
      packages/nc-gui/lang/zh-Hant.json
  81. 1
      packages/nc-gui/lib/types.ts
  82. 16
      packages/nc-gui/package.json
  83. 26
      packages/nc-gui/utils/datetimeUtils.ts
  84. 4
      packages/nc-gui/utils/formValidations.ts
  85. 6
      packages/nc-gui/utils/iconUtils.ts
  86. 11
      packages/noco-docs/docs/020.getting-started/050.self-hosted/020.environment-variables.md
  87. 131
      packages/noco-docs/docs/090.views/040.view-types/030.form.md
  88. BIN
      packages/noco-docs/static/img/v2/views/form-view/attachment-field-validations.png
  89. BIN
      packages/noco-docs/static/img/v2/views/form-view/date-field-validations.png
  90. BIN
      packages/noco-docs/static/img/v2/views/form-view/numeric-field-validations.png
  91. BIN
      packages/noco-docs/static/img/v2/views/form-view/text-field-validations.png
  92. 252
      packages/nocodb-sdk/src/lib/Api.ts
  93. 13
      packages/nocodb-sdk/src/lib/dateTimeHelper.ts
  94. 4
      packages/nocodb-sdk/src/lib/enums.ts
  95. 6
      packages/nocodb/Dockerfile
  96. 2
      packages/nocodb/docker/litestream.yml
  97. 42
      packages/nocodb/docker/start-litestream.sh
  98. 24
      packages/nocodb/src/app.module.ts
  99. 56
      packages/nocodb/src/controllers/audits.controller.ts
  100. 89
      packages/nocodb/src/controllers/comments.controller.ts
  101. Some files were not shown because too many files have changed in this diff Show More

11
packages/nc-gui/assets/nc-icons/at-sign.svg

@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="at-sign" clip-path="url(#clip0_439_24587)">
<path id="Vector" d="M7.99992 10.6666C9.47268 10.6666 10.6666 9.47268 10.6666 7.99992C10.6666 6.52716 9.47268 5.33325 7.99992 5.33325C6.52716 5.33325 5.33325 6.52716 5.33325 7.99992C5.33325 9.47268 6.52716 10.6666 7.99992 10.6666Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M10.6666 5.33333V8.66666C10.6666 9.19709 10.8773 9.7058 11.2524 10.0809C11.6275 10.4559 12.1362 10.6667 12.6666 10.6667C13.197 10.6667 13.7057 10.4559 14.0808 10.0809C14.4559 9.7058 14.6666 9.19709 14.6666 8.66666V7.99999C14.6665 6.49535 14.1574 5.03498 13.2221 3.85635C12.2868 2.67772 10.9803 1.85014 9.51502 1.50819C8.04974 1.16624 6.51188 1.33002 5.15149 1.9729C3.7911 2.61579 2.68819 3.69996 2.0221 5.04914C1.356 6.39832 1.1659 7.93315 1.4827 9.40407C1.7995 10.875 2.60458 12.1955 3.76701 13.1508C4.92945 14.1062 6.38088 14.6402 7.8853 14.6661C9.38973 14.692 10.8587 14.2082 12.0533 13.2933" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_439_24587">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

7
packages/nc-gui/assets/nc-icons/strike-through.svg

@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="strike-through">
<path id="Vector" d="M8.78581 12.8144L8.7858 12.8144L8.78075 12.8161C8.42892 12.9374 8.04691 13 7.63125 13C7.0617 13 6.56387 12.8877 6.12788 12.6731C5.69479 12.4578 5.33776 12.1536 5.05203 11.7548C4.85919 11.4783 4.71002 11.1643 4.60653 10.8088C4.60578 10.8063 4.60572 10.805 4.60571 10.8049L4.60571 10.8049L4.60573 10.8045C4.60577 10.8043 4.60639 10.801 4.61027 10.7953C4.61881 10.7828 4.63803 10.7668 4.66787 10.7613C4.69066 10.7571 4.71986 10.7599 4.75705 10.7832C4.79698 10.8084 4.83722 10.8531 4.86164 10.9117C5.05932 11.386 5.35201 11.7865 5.74021 12.1031L5.74019 12.1031L5.74442 12.1065C6.30071 12.5503 6.95605 12.7692 7.6875 12.7692C8.15265 12.7692 8.58829 12.6851 8.98574 12.507L8.98575 12.507L8.98917 12.5054C9.39374 12.3204 9.72685 12.0507 9.97355 11.6945C10.2302 11.3238 10.35 10.8938 10.35 10.4266C10.35 10.2805 10.3386 10.1376 10.313 10H10.4865C10.4944 10.0483 10.5 10.1354 10.5 10.3537C10.5 10.8007 10.4227 11.1786 10.2807 11.4978L10.2807 11.4978L10.2789 11.5019C10.1366 11.8294 9.94116 12.0969 9.69311 12.3122C9.437 12.5295 9.13644 12.6977 8.78581 12.8144Z" fill="#374151" stroke="currentColor"/>
<path id="Vector_2" d="M7.07937 7.54091L6.69045 7.41689C6.69002 7.41675 6.68958 7.41661 6.68914 7.41648C6.4336 7.33643 6.17378 7.23601 5.9098 7.11611L5.89921 7.11129L5.89927 7.11117C5.59699 6.96529 5.33529 6.76737 5.12024 6.51703L5.11627 6.51241L5.1163 6.51238C4.85636 6.2022 4.74739 5.81798 4.74739 5.40605C4.74739 4.97764 4.87095 4.58359 5.12364 4.24341C5.36495 3.91369 5.68549 3.66379 6.06999 3.4901C6.45874 3.31449 6.88407 3.23467 7.33682 3.24033L7.07937 7.54091ZM7.07937 7.54091L6.14982 7.55395C5.94013 7.48004 5.73664 7.39061 5.53917 7.28553C5.3363 7.17295 5.15642 7.03547 4.99793 6.87257C4.85081 6.71669 4.72946 6.52733 4.63646 6.29832C4.55046 6.08189 4.5 5.80784 4.5 5.46475C4.5 4.91851 4.62632 4.48585 4.85438 4.14174C5.09772 3.77635 5.42558 3.49592 5.84993 3.29831C6.27823 3.09886 6.77169 2.99574 7.34021 3.00013L7.34026 3.00014C7.91665 3.00453 8.41327 3.11833 8.84083 3.32988L8.84082 3.32991L8.84588 3.33235C9.27908 3.54065 9.63452 3.83839 9.91773 4.23096L9.91772 4.23096L9.91958 4.23351C10.1115 4.49596 10.2638 4.79674 10.3746 5.13963C10.3771 5.14708 10.3767 5.15078 10.3764 5.15294C10.3759 5.15595 10.3743 5.1621 10.3688 5.17053C10.3569 5.18863 10.3317 5.20933 10.2942 5.21656C10.2642 5.22235 10.2252 5.21858 10.1748 5.18558C10.1209 5.15027 10.0671 5.08814 10.0343 5.00929C9.94734 4.80085 9.83757 4.60662 9.70464 4.42792C9.43271 4.05163 9.08781 3.75898 8.67415 3.55645C8.26193 3.34973 7.81366 3.2461 7.33709 3.24033L7.07937 7.54091Z" fill="#374151" stroke="currentColor"/>
<path id="Vector_3" d="M2.66675 8H13.3334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

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

@ -29,6 +29,8 @@ const isGrid = inject(IsGridInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isDateInvalid = ref(false)
@ -43,6 +45,8 @@ const isClearedInputMode = ref<boolean>(false)
const open = ref<boolean>(false)
const tempDate = ref<dayjs.Dayjs | undefined>()
const localState = computed({
get() {
if (!modelValue || isClearedInputMode.value) {
@ -56,7 +60,9 @@ const localState = computed({
const format = picker.value === 'month' ? dateFormat : 'YYYY-MM-DD'
return dayjs(/^\d+$/.test(modelValue) ? +modelValue : modelValue, format)
const value = dayjs(/^\d+$/.test(modelValue) ? +modelValue : modelValue, format)
return value
},
set(val?: dayjs.Dayjs) {
isClearedInputMode.value = false
@ -79,20 +85,36 @@ const localState = computed({
},
})
watchEffect(() => {
if (localState.value) {
tempDate.value = localState.value
}
})
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
onClickOutside(datePickerRef, (e) => {
if ((e.target as HTMLElement)?.closest(`.${randomClass}`)) return
if ((e.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)) return
datePickerRef.value?.blur?.()
open.value = false
})
const onBlur = (e) => {
if ((e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}`)) return
if (
(e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`) ||
(e?.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)
) {
return
}
open.value = false
}
const onFocus = () => {
open.value = true
}
watch(
open,
(next) => {
@ -165,14 +187,20 @@ const clickHandler = () => {
cellClickHandler()
}
const handleKeydown = (e: KeyboardEvent) => {
if (e.key !== 'Enter') {
const handleKeydown = (e: KeyboardEvent, _open?: boolean) => {
if (e.key !== 'Enter' && e.key !== 'Tab') {
e.stopPropagation()
}
switch (e.key) {
case 'Enter':
open.value = !open.value
e.preventDefault()
if (isSurveyForm.value) {
e.stopPropagation()
}
localState.value = tempDate.value
open.value = !_open
if (!open.value) {
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
@ -181,7 +209,7 @@ const handleKeydown = (e: KeyboardEvent) => {
}
return
case 'Escape':
if (open.value) {
if (_open) {
open.value = false
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
@ -193,9 +221,18 @@ const handleKeydown = (e: KeyboardEvent) => {
datePickerRef.value?.blur?.()
}
return
case 'Tab':
open.value = false
if (isGrid.value) {
editable.value = false
datePickerRef.value?.blur?.()
}
return
default:
if (!open.value && /^[0-9a-z]$/i.test(e.key)) {
if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true
}
}
@ -232,32 +269,87 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
}
}
})
const handleUpdateValue = (e: Event) => {
const targetValue = (e.target as HTMLInputElement).value
if (!targetValue) {
tempDate.value = undefined
return
}
const value = dayjs(targetValue, dateFormat.value)
if (value.isValid()) {
tempDate.value = value
}
}
function handleSelectDate(value?: dayjs.Dayjs) {
tempDate.value = value
localState.value = value
open.value = false
}
</script>
<template>
<a-date-picker
ref="datePickerRef"
v-model:value="localState"
:disabled="readOnly"
:picker="picker"
:tabindex="0"
:bordered="false"
class="nc-cell-field !w-full !py-1 !border-none !text-current"
<NcDropdown
:visible="isOpen"
:auto-close="false"
:trigger="['click']"
class="nc-cell-field"
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]"
:format="dateFormat"
:overlay-class-name="`${randomClass} nc-picker-date ${open ? 'active' : ''} !min-w-[260px]`"
>
<div
:title="localState?.format(dateFormat)"
class="nc-date-picker h-full flex items-center justify-between ant-picker-input relative group"
>
<input
ref="datePickerRef"
type="text"
:value="localState?.format(dateFormat) ?? ''"
:placeholder="placeholder"
:allow-clear="!readOnly && !isEditColumn"
:input-read-only="!!isMobileMode"
:dropdown-class-name="`${randomClass} nc-picker-date children:border-1 children:border-gray-200 ${open ? 'active' : ''} `"
:open="isOpen"
class="nc-date-input border-none outline-none !text-current bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="readOnly || !!isMobileMode"
@blur="onBlur"
@click="clickHandler"
@keydown="handleKeydown"
@focus="onFocus"
@keydown="handleKeydown($event, open)"
@mouseup.stop
@mousedown.stop
>
<template #suffixIcon></template>
</a-date-picker>
@click="clickHandler"
@input="handleUpdateValue"
/>
<GeneralIcon
v-if="localState"
icon="closeCircle"
class="absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
@click.stop="handleSelectDate()"
/>
</div>
<template #overlay>
<div class="w-[256px]">
<NcDatePicker
v-if="picker === 'month'"
v-model:page-date="tempDate"
v-model:selected-date="localState"
:is-open="isOpen"
type="month"
size="medium"
/>
<NcDatePicker
v-else
v-model:page-date="tempDate"
:is-open="isOpen"
:selected-date="localState"
:is-monday-first="false"
type="date"
size="medium"
@update:selected-date="handleSelectDate"
/>
</div>
</template>
</NcDropdown>
<div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { dateFormats, isSystemColumn, timeFormats } from 'nocodb-sdk'
import { dateFormats, isSystemColumn, isValidTimeFormat, timeFormats } from 'nocodb-sdk'
interface Props {
modelValue?: string | null
@ -25,6 +25,8 @@ const isGrid = inject(IsGridInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const { t } = useI18n()
const isEditColumn = inject(EditColumnInj, ref(false))
@ -37,18 +39,27 @@ const isDateInvalid = ref(false)
const datePickerRef = ref<HTMLInputElement>()
const timePickerRef = ref<HTMLInputElement>()
const dateTimeFormat = computed(() => {
const dateFormat = parseProp(column?.value?.meta)?.date_format ?? dateFormats[0]
const timeFormat = parseProp(column?.value?.meta)?.time_format ?? timeFormats[0]
return `${dateFormat} ${timeFormat}`
})
const dateFormat = computed(() => parseProp(column?.value?.meta)?.date_format ?? dateFormats[0])
const timeFormat = computed(() => parseProp(column?.value?.meta)?.time_format ?? timeFormats[0])
let localModelValue = modelValue ? dayjs(modelValue).utc().local() : undefined
const isClearedInputMode = ref<boolean>(false)
const open = ref(false)
const tempDate = ref<dayjs.Dayjs | undefined>()
const isDatePicker = ref<boolean>(true)
const localState = computed({
get() {
if (!modelValue || isClearedInputMode.value) {
@ -117,8 +128,18 @@ const localState = computed({
},
})
watchEffect(() => {
if (localState.value) {
tempDate.value = localState.value
}
})
const isColDisabled = computed(() => {
return isSystemColumn(column.value) || readOnly.value || (localState.value && isPk)
})
const isOpen = computed(() => {
if (readOnly.value) return false
if (readOnly.value || isColDisabled.value) return false
return readOnly.value || (localState.value && isPk) ? false : open.value && (active.value || editable.value)
})
@ -126,15 +147,16 @@ const isOpen = computed(() => {
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
onClickOutside(datePickerRef, (e) => {
if ((e.target as HTMLElement)?.closest(`.${randomClass}`)) return
if ((e.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)) return
datePickerRef.value?.blur?.()
timePickerRef.value?.blur?.()
open.value = false
})
const onBlur = (e) => {
if ((e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}`)) return
open.value = false
const onFocus = (_isDatePicker: boolean) => {
isDatePicker.value = _isDatePicker
open.value = true
}
watch(
@ -142,7 +164,8 @@ watch(
(next) => {
if (next) {
editable.value = true
datePickerRef.value?.focus?.()
isDatePicker.value ? datePickerRef.value?.focus?.() : timePickerRef.value?.focus?.()
onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, (e) => {
if ((e?.target as HTMLElement)?.closest(`.nc-${randomClass}`)) {
@ -162,7 +185,7 @@ const placeholder = computed(() => {
((isForm.value || isExpandedForm.value) && !isDateInvalid.value) ||
(isGrid.value && !showNull.value && !isDateInvalid.value && !isSystemColumn(column.value) && active.value)
) {
return dateTimeFormat.value
return { dateTime: dateTimeFormat.value, date: dateFormat.value, time: timeFormat.value }
} else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional')
} else if (modelValue === null && showNull.value) {
@ -180,25 +203,6 @@ const cellClickHandler = () => {
open.value = active.value || editable.value
}
function okHandler(val: dayjs.Dayjs | string) {
isClearedInputMode.value = false
if (!val) {
emit('update:modelValue', null)
} else if (dayjs(val).isValid()) {
// setting localModelValue to cater NOW function in date picker
localModelValue = dayjs(val)
// send the payload in UTC format
emit('update:modelValue', dayjs(val).utc().format('YYYY-MM-DD HH:mm:ssZ'))
}
open.value = !open.value
if (!open.value && isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
datePickerRef.value?.blur?.()
editable.value = false
}
}
onMounted(() => {
cellClickHook?.on(cellClickHandler)
})
@ -206,13 +210,8 @@ onUnmounted(() => {
cellClickHook?.on(cellClickHandler)
})
const clickHandler = (e) => {
if ((e.target as HTMLElement).closest(`.nc-${randomClass} .ant-picker-clear`)) {
e.stopPropagation()
emit('update:modelValue', null)
open.value = false
return
}
const clickHandler = (e: MouseEvent, _isDatePicker: boolean = false) => {
isDatePicker.value = _isDatePicker
if (cellClickHook) {
return
@ -220,46 +219,63 @@ const clickHandler = (e) => {
cellClickHandler()
}
const isColDisabled = computed(() => {
return isSystemColumn(column.value) || readOnly.value || (localState.value && isPk)
})
const handleKeydown = (e: KeyboardEvent) => {
const handleKeydown = (e: KeyboardEvent, _open?: boolean, _isDatePicker: boolean = false) => {
if (e.key !== 'Enter') {
e.stopPropagation()
}
switch (e.key) {
case 'Enter':
if (isOpen.value) {
return okHandler((e.target as HTMLInputElement).value)
e.preventDefault()
if (isSurveyForm.value) {
e.stopPropagation()
}
localState.value = tempDate.value
if (!_isDatePicker) {
e.stopPropagation()
timePickerRef.value?.blur?.()
isDatePicker.value = false
datePickerRef.value?.focus?.()
cellClickHandler()
} else {
open.value = true
datePickerRef.value?.blur?.()
open.value = false
editable.value = false
}
return
case 'Escape':
if (open.value) {
if (_open) {
open.value = false
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
datePickerRef.value?.blur?.()
_isDatePicker ? datePickerRef.value?.blur?.() : timePickerRef.value?.blur?.()
}
} else {
editable.value = false
datePickerRef.value?.blur?.()
_isDatePicker ? datePickerRef.value?.blur?.() : timePickerRef.value?.blur?.()
}
return
case 'Tab':
open.value = false
if (isGrid.value) {
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
_isDatePicker ? datePickerRef.value?.blur?.() : timePickerRef.value?.blur?.()
if (e.shiftKey && _isDatePicker) {
editable.value = false
datePickerRef.value?.blur()
} else if (!e.shiftKey && !_isDatePicker) {
editable.value = false
} else {
e.stopPropagation()
}
}
return
default:
if (!open.value && /^[0-9a-z]$/i.test(e.key)) {
if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true
}
}
@ -288,9 +304,9 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
e.preventDefault()
break
default:
if (!isOpen.value && datePickerRef.value && /^[0-9a-z]$/i.test(e.key)) {
if (!isOpen.value && (datePickerRef.value || timePickerRef.value) && /^[0-9a-z]$/i.test(e.key)) {
isClearedInputMode.value = true
datePickerRef.value.focus()
isDatePicker.value ? datePickerRef.value?.focus() : timePickerRef.value?.focus()
editable.value = true
open.value = true
}
@ -299,35 +315,203 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
watch(editable, (nextValue) => {
if (isGrid.value && nextValue && !open.value) {
isDatePicker.value = true
open.value = true
}
})
const handleUpdateValue = (e: Event, _isDatePicker: boolean) => {
let targetValue = (e.target as HTMLInputElement).value
if (_isDatePicker) {
if (!targetValue) {
tempDate.value = undefined
return
}
const date = dayjs(targetValue, dateFormat.value)
if (date.isValid()) {
if (localState.value) {
tempDate.value = dayjs(`${date.format('YYYY-MM-DD')} ${localState.value.format(timeFormat.value)}`)
} else {
tempDate.value = date
}
}
}
if (!_isDatePicker) {
if (!targetValue) {
tempDate.value = dayjs(dayjs().format('YYYY-MM-DD'))
return
}
if (timeFormat.value === 'HH:mm' && targetValue.length > 5) {
targetValue = targetValue.slice(0, 5)
}
if (isValidTimeFormat(targetValue, timeFormat.value)) {
tempDate.value = dayjs(`${(tempDate.value ?? dayjs()).format('YYYY-MM-DD')} ${targetValue}`)
}
}
}
function handleSelectDate(value?: dayjs.Dayjs) {
if (value && localState.value) {
const dateTime = dayjs(`${value.format('YYYY-MM-DD')} ${localState.value.format(timeFormat.value)}`)
tempDate.value = dateTime
localState.value = dateTime
} else {
tempDate.value = value
localState.value = value
}
open.value = false
}
function handleSelectTime(value: dayjs.Dayjs) {
if (!value.isValid()) return
if (localState.value) {
const dateTime = dayjs(`${localState.value.format('YYYY-MM-DD')} ${value.format('HH:mm')}:00`)
tempDate.value = dateTime
localState.value = dateTime
} else {
const dateTime = dayjs(`${dayjs().format('YYYY-MM-DD')} ${value.format('HH:mm')}:00`)
tempDate.value = dateTime
localState.value = dateTime
}
open.value = false
}
const selectedTime = computed(() => {
const result = {
value: '',
label: '',
}
if (localState.value) {
const time = localState.value.format(timeFormat.value)
const [hours, minutes] = time.split(':')
result.value = `${hours}:${minutes}`
result.label = time
}
return result
})
const timeCellMaxWidth = computed(() => {
return {
[timeFormats[0]]: 'max-w-[65px]',
[timeFormats[1]]: 'max-w-[80px]',
[timeFormats[2]]: 'max-w-[110px]',
}[timeFormat.value]
})
</script>
<template>
<a-date-picker
ref="datePickerRef"
:value="localState"
:disabled="isColDisabled"
:show-time="true"
:bordered="false"
class="nc-cell-field nc-cell-picker-datetime !w-full !py-1 !border-none !text-current"
<div class="nc-cell-field group relative">
<NcDropdown
:visible="isOpen"
:placement="isDatePicker ? 'bottomLeft' : 'bottomRight'"
:auto-close="false"
:trigger="['click']"
class="nc-cell-picker-datetime"
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]"
:format="dateTimeFormat"
:placeholder="placeholder"
:allow-clear="!isColDisabled && !isEditColumn"
:input-read-only="!!isMobileMode"
:dropdown-class-name="`${randomClass} nc-picker-datetime children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
:open="isOpen"
@blur="onBlur"
@click="clickHandler"
@ok="okHandler"
@keydown="handleKeydown"
:overlay-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''} !min-w-[0] overflow-hidden`"
>
<div
:title="localState?.format(dateTimeFormat)"
class="nc-date-picker ant-picker-input flex justify-between gap-2 relative group !w-auto"
>
<div
class="flex-none hover:bg-gray-100 px-1 rounded-md box-border w-[60%] max-w-[110px]"
:class="{
'py-0': isForm,
'py-0.5': !isForm,
'bg-gray-100': isDatePicker && isOpen,
}"
>
<input
ref="datePickerRef"
:value="localState?.format(dateFormat) ?? ''"
:placeholder="typeof placeholder === 'string' ? placeholder : placeholder?.date"
class="nc-date-input w-full !truncate border-transparent outline-none !text-current !bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="!!isMobileMode || isColDisabled"
@focus="onFocus(true)"
@keydown="handleKeydown($event, isOpen, true)"
@mouseup.stop
@mousedown.stop
@click.stop="clickHandler($event, true)"
@input="handleUpdateValue($event, true)"
/>
</div>
<div
class="flex-none hover:bg-gray-100 px-1 rounded-md box-border flex-1"
:class="[
`${timeCellMaxWidth}`,
{
'py-0': isForm,
'py-0.5': !isForm,
'bg-gray-100': !isDatePicker && isOpen,
},
]"
>
<template #suffixIcon></template>
</a-date-picker>
<input
ref="timePickerRef"
:value="selectedTime.value ? `${selectedTime.label}` : ''"
:placeholder="typeof placeholder === 'string' ? placeholder : placeholder?.time"
class="nc-time-input w-full !truncate border-transparent outline-none !text-current !bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="!!isMobileMode || isColDisabled"
@focus="onFocus(false)"
@keydown="handleKeydown($event, open)"
@mouseup.stop
@mousedown.stop
@click.stop="clickHandler($event, false)"
@input="handleUpdateValue($event, false)"
/>
</div>
</div>
<template #overlay>
<div
class="min-w-[72px]"
:class="{
'w-[256px]': isDatePicker,
}"
>
<NcDatePicker
v-if="isDatePicker"
v-model:page-date="tempDate"
:selected-date="localState"
:is-open="isOpen"
type="date"
size="medium"
@update:selected-date="handleSelectDate"
/>
<template v-else>
<NcTimeSelector
:selected-date="localState"
:min-granularity="30"
is-min-granularity-picker
:is-open="isOpen"
@update:selected-date="handleSelectTime"
/>
</template>
</div>
</template>
</NcDropdown>
<GeneralIcon
v-if="localState && (isExpandedForm || isForm || !isGrid)"
icon="closeCircle"
class="h-4 w-4 absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
@click.stop="handleSelectDate()"
/>
</div>
<div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { isSystemColumn } from 'nocodb-sdk'
import { isSystemColumn, isValidTimeFormat } from 'nocodb-sdk'
interface Props {
modelValue?: string | null | undefined
@ -27,6 +27,8 @@ const isGrid = inject(IsGridInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const column = inject(ColumnInj)!
@ -43,6 +45,8 @@ const { t } = useI18n()
const open = ref(false)
const tempDate = ref<dayjs.Dayjs | undefined>()
const localState = computed({
get() {
if (!modelValue || isClearedInputMode.value) {
@ -78,20 +82,35 @@ const localState = computed({
},
})
watchEffect(() => {
if (localState.value) {
tempDate.value = localState.value
}
})
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
onClickOutside(datePickerRef, (e) => {
if ((e.target as HTMLElement)?.closest(`.${randomClass}`)) return
if ((e.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)) return
datePickerRef.value?.blur?.()
open.value = false
})
const onBlur = (e) => {
if ((e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}`)) return
if (
(e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`) ||
(e?.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)
) {
return
}
open.value = false
}
const onFocus = () => {
open.value = true
}
watch(
open,
(next) => {
@ -146,26 +165,42 @@ const clickHandler = () => {
open.value = active.value || editable.value
}
const handleKeydown = (e: KeyboardEvent) => {
const handleKeydown = (e: KeyboardEvent, _open?: boolean) => {
if (e.key !== 'Enter') {
e.stopPropagation()
}
switch (e.key) {
case 'Enter':
open.value = !open.value
e.preventDefault()
if (isSurveyForm.value) {
e.stopPropagation()
}
localState.value = tempDate.value
open.value = !_open
if (!open.value) {
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
editable.value = false
datePickerRef.value?.blur?.()
}
}
return
case 'Escape':
if (open.value) {
case 'Tab':
open.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
editable.value = false
datePickerRef.value?.blur?.()
}
return
case 'Escape':
if (_open) {
open.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
editable.value = false
datePickerRef.value?.blur?.()
}
} else {
@ -175,7 +210,7 @@ const handleKeydown = (e: KeyboardEvent) => {
}
return
default:
if (!open.value && /^[0-9a-z]$/i.test(e.key)) {
if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true
}
}
@ -212,39 +247,93 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
}
}
})
const handleUpdateValue = (e: Event) => {
let targetValue = (e.target as HTMLInputElement).value
if (!targetValue) {
tempDate.value = undefined
return
}
if (targetValue.length > 5) {
targetValue = targetValue.slice(0, 5)
}
if (isValidTimeFormat(targetValue, 'HH:mm')) {
tempDate.value = dayjs(`${dayjs().format('YYYY-MM-DD')} ${targetValue}`)
}
}
function handleSelectTime(value?: dayjs.Dayjs) {
if (!value) {
tempDate.value = undefined
localState.value = undefined
}
if (!value?.isValid()) return
if (localState.value) {
const dateTime = dayjs(`${localState.value.format('YYYY-MM-DD')} ${value.format('HH:mm')}:00`)
tempDate.value = dateTime
localState.value = dateTime
} else {
const dateTime = dayjs(`${dayjs().format('YYYY-MM-DD')} ${value.format('HH:mm')}:00`)
tempDate.value = dateTime
localState.value = dateTime
}
open.value = false
}
</script>
<template>
<a-time-picker
ref="datePickerRef"
v-model:value="localState"
:tabindex="0"
:disabled="readOnly"
:show-time="true"
:bordered="false"
use12-hours
format="HH:mm"
class="nc-cell-field !w-full !py-1 !border-none !text-current"
<NcDropdown
:visible="isOpen"
:auto-close="false"
:trigger="['click']"
class="nc-cell-field"
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]"
:overlay-class-name="`${randomClass} nc-picker-time ${isOpen ? 'active' : ''} !min-w-[0]`"
>
<div
:title="localState?.format('HH:mm')"
class="nc-time-picker h-full flex items-center justify-between ant-picker-input relative group"
>
<input
ref="datePickerRef"
type="text"
:value="localState?.format('HH:mm') ?? ''"
:placeholder="placeholder"
:allow-clear="!readOnly && !isPk && !isEditColumn"
:input-read-only="!!isMobileMode"
:open="isOpen"
:popup-class-name="`${randomClass} nc-picker-time children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
class="nc-time-input border-none outline-none !text-current bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="readOnly || !!isMobileMode"
@blur="onBlur"
@keydown="handleKeydown"
@click="clickHandler"
@ok="open = !open"
@focus="onFocus"
@keydown="handleKeydown($event, isOpen)"
@mouseup.stop
@mousedown.stop
>
<template #suffixIcon></template>
</a-time-picker>
@click="clickHandler"
@input="handleUpdateValue"
/>
<GeneralIcon
v-if="localState"
icon="closeCircle"
class="absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
@click.stop="handleSelectTime()"
/>
</div>
<template #overlay>
<div class="w-[72px]">
<NcTimeSelector
:selected-date="localState"
:min-granularity="30"
is-min-granularity-picker
:is-open="isOpen"
@update:selected-date="handleSelectTime"
/>
</div>
</template>
</NcDropdown>
<div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template>
<style scoped>
:deep(.ant-picker-input > input) {
@apply !text-current;
}
</style>

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

@ -27,6 +27,8 @@ const isGrid = inject(IsGridInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isYearInvalid = ref(false)
@ -39,6 +41,8 @@ const { t } = useI18n()
const open = ref<boolean>(false)
const tempDate = ref<dayjs.Dayjs | undefined>()
const localState = computed({
get() {
if (!modelValue || isClearedInputMode.value) {
@ -69,16 +73,27 @@ const localState = computed({
},
})
watchEffect(() => {
if (localState.value) {
tempDate.value = localState.value
}
})
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
onClickOutside(datePickerRef, (e) => {
if ((e.target as HTMLElement)?.closest(`.${randomClass}`)) return
if ((e.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)) return
datePickerRef.value?.blur?.()
open.value = false
})
const onBlur = (e) => {
if ((e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}`)) return
if (
(e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`) ||
(e?.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)
) {
return
}
open.value = false
}
@ -136,14 +151,20 @@ const clickHandler = () => {
open.value = active.value || editable.value
}
const handleKeydown = (e: KeyboardEvent) => {
if (e.key !== 'Enter') {
const handleKeydown = (e: KeyboardEvent, _open?: boolean) => {
if (e.key !== 'Enter' && e.key !== 'Tab') {
e.stopPropagation()
}
switch (e.key) {
case 'Enter':
open.value = !open.value
e.preventDefault()
if (isSurveyForm.value) {
e.stopPropagation()
}
localState.value = tempDate.value
open.value = !_open
if (!open.value) {
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
@ -151,9 +172,19 @@ const handleKeydown = (e: KeyboardEvent) => {
}
}
return
case 'Tab':
open.value = false
if (isGrid.value) {
editable.value = false
datePickerRef.value?.blur?.()
}
return
case 'Escape':
if (open.value) {
if (_open) {
open.value = false
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
@ -166,7 +197,7 @@ const handleKeydown = (e: KeyboardEvent) => {
}
return
default:
if (!open.value && /^[0-9a-z]$/i.test(e.key)) {
if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true
}
}
@ -203,31 +234,77 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
}
}
})
const handleUpdateValue = (e: Event) => {
const targetValue = (e.target as HTMLInputElement).value
if (!targetValue) {
tempDate.value = undefined
return
}
const value = dayjs(targetValue, 'YYYY')
if (value.isValid()) {
tempDate.value = value
}
}
function handleSelectDate(value?: dayjs.Dayjs) {
tempDate.value = value
localState.value = value
open.value = false
}
</script>
<template>
<a-date-picker
ref="datePickerRef"
v-model:value="localState"
<NcDropdown
:visible="isOpen"
:auto-close="false"
:trigger="['click']"
:disabled="readOnly"
:tabindex="0"
picker="year"
:bordered="false"
class="nc-cell-field !w-full !py-1 !border-none !text-current"
class="nc-cell-field"
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]"
:overlay-class-name="`${randomClass} nc-picker-year ${open ? 'active' : ''} !min-w-[260px]`"
>
<div
:title="localState?.format('YYYY')"
class="nc-year-picker flex items-center justify-between ant-picker-input relative group"
>
<input
ref="datePickerRef"
type="text"
:value="localState?.format('YYYY') ?? ''"
:placeholder="placeholder"
:allow-clear="!readOnly && !isPk"
:input-read-only="!!isMobileMode"
:open="isOpen"
:dropdown-class-name="`${randomClass} nc-picker-year children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
class="nc-year-input border-none outline-none !text-current bg-transparent !focus:(border-none outline-none ring-transparent)"
:readonly="readOnly || !!isMobileMode"
@blur="onBlur"
@keydown="handleKeydown"
@click="clickHandler"
@keydown="handleKeydown($event, open)"
@mouseup.stop
@mousedown.stop
>
<template #suffixIcon></template>
</a-date-picker>
@click="clickHandler"
@input="handleUpdateValue"
/>
<GeneralIcon
v-if="localState"
icon="closeCircle"
class="absolute right-0 top-[50%] transform -translate-y-1/2 invisible group-hover:visible cursor-pointer"
@click.stop="handleSelectDate()"
/>
</div>
<template #overlay>
<div class="w-[256px]">
<NcMonthYearSelector
v-model:page-date="tempDate"
v-model:selected-date="localState"
:is-open="isOpen"
is-year-picker
is-cell-input-field
size="medium"
/>
</div>
</template>
</NcDropdown>
<div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template>

4
packages/nc-gui/components/cmd-l/index.vue

@ -219,7 +219,7 @@ onMounted(() => {
</div>
<div class="flex w-1/2 justify-end text-gray-600">
<div class="flex gap-2 px-2 py-1 rounded-md items-center">
<component :is="iconMap.projectGray" class="w-3 h-3 text-transparent" />
<component :is="iconMap.project" class="w-3 h-3" />
<a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg">
<template #title>
{{ cmdOption.baseName }}
@ -230,7 +230,7 @@ onMounted(() => {
</a-tooltip>
<span class="text-bold"> / </span>
<component :is="iconMap.table" class="w-3 h-3 text-transparent" />
<component :is="iconMap.table" class="w-3 h-3" />
<a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg">
<template #title>
{{ cmdOption.tableName }}

1
packages/nc-gui/components/dashboard/View.vue

@ -173,6 +173,7 @@ const normalizedWidth = computed(() => {
:class="{
'hide-resize-bar': !isLeftSidebarOpen || sidebarState === 'openStart',
}"
@ready="() => onWindowResize()"
@resize="(event: any) => onResize(event[0].size)"
>
<Pane

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

@ -103,7 +103,7 @@ const columns = [
</script>
<template>
<div class="flex flex-col gap-4 w-full">
<div class="h-full flex flex-col gap-4 w-full">
<div v-if="!appInfo.auditEnabled" class="text-red-500">Audit logs are currently disabled by administrators.</div>
<div class="flex flex-row justify-between items-center">
<h6 class="mb-4 first-letter:capital font-bold">Audit : {{ base.title }}</h6>
@ -116,6 +116,7 @@ const columns = [
</a-button>
</div>
<div class="h-[calc(100%_-_102px)] overflow-y-auto nc-scrollbar-thin">
<a-table
class="nc-audit-table w-full"
size="small"
@ -124,12 +125,15 @@ const columns = [
:pagination="false"
:loading="isLoading"
data-testid="audit-tab-table"
sticky
bordered
>
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
</a-table>
<div class="flex flex-row justify-center items-center">
</div>
<div v-if="+totalRows > currentLimit" class="flex flex-row justify-center items-center">
<a-pagination
v-model:current="currentPage"
v-model:page-size="currentLimit"
@ -151,6 +155,7 @@ const columns = [
font-size: unset;
font-family: unset;
}
.pagination {
.ant-select-dropdown {
@apply !border-1 !border-gray-200;

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

@ -303,7 +303,7 @@ const openedTab = ref('erd')
<div>{{ $t('title.auditLogs') }}</div>
</div>
</template>
<div class="p-4 h-full overflow-auto">
<div class="p-4 h-full">
<LazyDashboardSettingsBaseAudit :source-id="activeSource.id" />
</div>
</a-tab-pane>
@ -330,7 +330,7 @@ const openedTab = ref('erd')
</div>
</template>
<div class="pt-4 h-full overflow-auto">
<div class="pt-4 h-full">
<LazyDashboardSettingsUIAcl :source-id="activeSource.id" />
</div>
</a-tab-pane>
@ -340,7 +340,7 @@ const openedTab = ref('erd')
<div>{{ $t('labels.metaSync') }}</div>
</div>
</template>
<div class="pt-4 h-full overflow-auto">
<div class="pt-4 h-full">
<LazyDashboardSettingsMetadata :source-id="activeSource.id" @source-synced="loadBases(true)" />
</div>
</a-tab-pane>

11
packages/nc-gui/components/dashboard/settings/Metadata.vue

@ -108,8 +108,8 @@ const columns = [
</script>
<template>
<div class="flex flex-col w-full">
<div class="flex flex-col">
<div class="h-full flex flex-col w-full">
<div class="h-full flex flex-col">
<div class="flex flex-row justify-between items-center w-full mb-4">
<div class="flex">
<div v-if="isDifferent">
@ -140,9 +140,9 @@ const columns = [
</div>
</a-button>
</div>
<div class="max-h-600px overflow-y-auto">
<div class="h-auto max-h-[calc(100%_-_72px)] overflow-y-auto nc-scrollbar-thin">
<a-table
class="w-full"
class="nc-metasync-table w-full"
size="small"
:custom-row="
(record) => ({
@ -153,6 +153,7 @@ const columns = [
:columns="columns"
:pagination="false"
:loading="isLoading"
sticky
bordered
>
<template #emptyText>
@ -178,3 +179,5 @@ const columns = [
</div>
</div>
</template>
<style lang="scss" scoped></style>

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

@ -130,8 +130,8 @@ const toggleSelectAll = (role: Role) => {
</script>
<template>
<div class="flex flex-row w-full items-center justify-center">
<div class="flex flex-col">
<div class="h-full flex flex-row w-full items-center justify-center">
<div class="h-full flex flex-col">
<NcTooltip class="mb-4 first-letter:capital font-bold max-w-100 truncate" show-on-truncate-only>
<template #title>{{ base.title }}</template>
<span> UI ACL : {{ base.title }} </span>
@ -159,7 +159,7 @@ const toggleSelectAll = (role: Role) => {
</div>
</div>
<div class="max-h-600px overflow-y-auto">
<div class="h-auto max-h-[calc(100%_-_102px)] overflow-y-auto nc-scrollbar-thin">
<a-table
class="w-full"
size="small"

2
packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue

@ -147,7 +147,7 @@ onMounted(async () => {
class="mr-1 flex items-center justify-center"
:class="[plugin.title === 'SES' ? 'p-2 bg-[#242f3e]' : '']"
>
<img :alt="plugin.title || 'plugin'" :src="`/${plugin.logo}`" class="h-6" />
<img :alt="plugin.title || 'plugin'" :src="plugin.logo" class="h-6" />
</div>
<span class="font-semibold text-lg">{{ plugin.formDetails.title }}</span>

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

@ -97,7 +97,8 @@ const validators = computed(() => {
'dataSource.connection.user': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
...([ClientType.PG, ClientType.MSSQL].includes(formState.value.dataSource.client)
...([ClientType.PG, ClientType.MSSQL].includes(formState.value.dataSource.client) &&
formState.value.dataSource.searchPath
? {
'dataSource.searchPath.0': [fieldRequiredValidator()],
}
@ -342,6 +343,19 @@ onMounted(async () => {
updateSSLUse()
}
})
// if searchPath is null/undefined reset it to empty array when necessary
watch(
() => formState.value.dataSource.searchPath,
(val) => {
if ([ClientType.PG, ClientType.MSSQL].includes(formState.value.dataSource.client) && !val) {
formState.value.dataSource.searchPath = []
}
},
{
immediate: true,
},
)
</script>
<template>

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

@ -45,8 +45,8 @@ onMounted(async () => {
</script>
<template>
<GeneralModal v-model:visible="isOpen" size="large" class="!w-[70rem]">
<div class="p-6">
<GeneralModal v-model:visible="isOpen" size="xl" class="!w-[70rem] !top-[5vh]">
<div class="p-6 h-full">
<DashboardSettingsBaseAudit v-if="!isLoading" :source-id="activeSourceId" :base-id="baseId" :show-all-columns="false" />
</div>
</GeneralModal>

11
packages/nc-gui/components/general/Modal.vue

@ -3,7 +3,7 @@ const props = withDefaults(
defineProps<{
visible: boolean
width?: string | number
size?: 'small' | 'medium' | 'large'
size?: 'small' | 'medium' | 'large' | 'xl'
destroyOnClose?: boolean
maskClosable?: boolean
closable?: boolean
@ -40,6 +40,10 @@ const width = computed(() => {
return '80rem'
}
if (props.size === 'xl') {
return '80rem'
}
return 'max(30vw, 600px)'
})
@ -55,6 +59,9 @@ const height = computed(() => {
if (props.size === 'large') {
return '80vh'
}
if (props.size === 'xl') {
return '90vh'
}
return 'auto'
})
@ -75,7 +82,7 @@ const visible = useVModel(props, 'visible', emits)
:mask-closable="maskClosable"
@keydown.esc="visible = false"
>
<div :class="`nc-modal max-h-[${height}]`">
<div :class="`nc-modal h-[${height}] max-h-[${height}]`">
<slot />
</div>
</a-modal>

150
packages/nc-gui/components/nc/DatePicker.vue

@ -0,0 +1,150 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
interface Props {
size?: 'medium'
selectedDate?: dayjs.Dayjs | null
pageDate?: dayjs.Dayjs
isCellInputField?: boolean
type: 'date' | 'time' | 'year' | 'month'
isOpen: boolean
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium',
selectedDate: null,
pageDate: () => dayjs(),
isCellInputField: false,
type: 'date',
isOpen: false,
})
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:selectedWeek'])
// Page date is the date we use to manage which month/date that is currently being displayed
const pageDate = useVModel(props, 'pageDate', emit)
const selectedDate = useVModel(props, 'selectedDate', emit)
const { type, isOpen } = toRefs(props)
const localPageDate = ref()
const localSelectedDate = ref()
const pickerType = ref<Props['type'] | undefined>()
const pickerStack = ref<Props['type'][]>([])
const tempPickerType = computed(() => pickerType.value || type.value)
const handleUpdatePickerType = (value?: Props['type']) => {
if (value) {
pickerType.value = value
pickerStack.value.push(value)
} else {
if (pickerStack.value.length > 1) {
pickerStack.value.pop()
const lastPicker = pickerStack.value.pop()
pickerType.value = lastPicker
} else {
pickerStack.value = []
pickerType.value = type.value
}
}
}
const localStatePageDate = computed({
get: () => {
if (localPageDate.value) {
return localPageDate.value
}
return pageDate.value
},
set: (value) => {
pageDate.value = value
localPageDate.value = value
emit('update:pageDate', value)
},
})
const localStateSelectedDate = computed({
get: () => {
if (localSelectedDate.value) {
return localSelectedDate.value
}
return pageDate.value
},
set: (value: dayjs.Dayjs) => {
if (!value.isValid()) return
if (pickerType.value === type.value) {
localPageDate.value = value
emit('update:selectedDate', value)
localSelectedDate.value = undefined
return
}
if (['date', 'month'].includes(type.value)) {
if (pickerType.value === 'year') {
localSelectedDate.value = dayjs(localPageDate.value ?? localSelectedDate.value ?? selectedDate.value ?? dayjs()).year(
+value.format('YYYY'),
)
}
if (type.value !== 'month' && pickerType.value === 'month') {
localSelectedDate.value = dayjs(localPageDate.value ?? localSelectedDate.value ?? selectedDate.value ?? dayjs()).month(
+value.format('MM') - 1,
)
}
localPageDate.value = localSelectedDate.value
handleUpdatePickerType()
}
},
})
watch(isOpen, (next) => {
if (!next) {
pickerType.value = type.value
localPageDate.value = undefined
localSelectedDate.value = undefined
pickerStack.value = []
}
})
onUnmounted(() => {
pickerType.value = type.value
localPageDate.value = undefined
localSelectedDate.value = undefined
pickerStack.value = []
})
onMounted(() => {
localPageDate.value = undefined
localSelectedDate.value = undefined
pickerStack.value = []
})
</script>
<template>
<NcDateWeekSelector
v-if="tempPickerType === 'date'"
v-model:page-date="localStatePageDate"
v-model:selected-date="localStateSelectedDate"
:picker-type="pickerType"
:is-monday-first="false"
is-cell-input-field
size="medium"
@update:picker-type="handleUpdatePickerType"
/>
<NcMonthYearSelector
v-if="['month', 'year'].includes(tempPickerType)"
v-model:page-date="localStatePageDate"
v-model:selected-date="localStateSelectedDate"
:picker-type="pickerType"
:is-year-picker="tempPickerType === 'year'"
is-cell-input-field
size="medium"
@update:picker-type="handleUpdatePickerType"
/>
</template>
<style lang="scss" scoped></style>

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

@ -13,19 +13,23 @@ interface Props {
start: dayjs.Dayjs
end: dayjs.Dayjs
} | null
isCellInputField?: boolean
pickerType?: 'date' | 'time' | 'year' | 'month'
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium',
selectedDate: null,
isMondayFirst: true,
pageDate: dayjs(),
pageDate: () => dayjs(),
isWeekPicker: false,
activeDates: [] as Array<dayjs.Dayjs>,
activeDates: () => [] as Array<dayjs.Dayjs>,
selectedWeek: null,
hideCalendar: false,
isCellInputField: false,
pickerType: 'date',
})
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:selectedWeek'])
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:selectedWeek', 'update:pickerType'])
// Page date is the date we use to manage which month/date that is currently being displayed
const pageDate = useVModel(props, 'pageDate', emit)
@ -35,6 +39,8 @@ const activeDates = useVModel(props, 'activeDates', emit)
const selectedWeek = useVModel(props, 'selectedWeek', emit)
const pickerType = useVModel(props, 'pickerType', emit)
const days = computed(() => {
if (props.isMondayFirst) {
return ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
@ -47,6 +53,14 @@ const currentMonthYear = computed(() => {
return dayjs(pageDate.value).format('MMMM YYYY')
})
const currentMonth = computed(() => {
return dayjs(pageDate.value).format('MMMM')
})
const currentYear = computed(() => {
return dayjs(pageDate.value).format('YYYY')
})
const selectWeek = (date: dayjs.Dayjs) => {
const dayOffset = +props.isMondayFirst
const dayOfWeek = (date.day() - dayOffset + 7) % 7
@ -102,6 +116,9 @@ const isDayInPagedMonth = (date: dayjs.Dayjs) => {
const handleSelectDate = (date: dayjs.Dayjs) => {
if (props.isWeekPicker) {
selectWeek(date)
} else if (props.isCellInputField) {
selectedDate.value = date
emit('update:selectedDate', date)
} else {
if (!isDayInPagedMonth(date)) {
pageDate.value = date
@ -137,17 +154,30 @@ const paginate = (action: 'next' | 'prev') => {
<template>
<div class="flex flex-col">
<div class="flex justify-between border-b-1 px-3 py-0.5 nc-date-week-header items-center">
<div
class="flex justify-between border-b-1 nc-date-week-header items-center box-border"
:class="{
'px-2 py-1 h-10': isCellInputField,
'px-3 py-0.5': !isCellInputField,
}"
>
<NcTooltip hide-on-click>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.arrowLeft" class="h-4 w-4" />
</NcButton>
<template #title>
<span>{{ $t('labels.next') }}</span>
<span>{{ $t('labels.previous') }}</span>
</template>
</NcTooltip>
<span class="text-gray-700 text-sm font-semibold">{{ currentMonthYear }}</span>
<div v-if="isCellInputField" class="text-gray-700 text-sm font-semibold">
<span class="nc-month-picker-btn cursor-pointer hover:text-brand-500" @click="pickerType = 'month'">{{
currentMonth
}}</span>
{{ ' ' }}
<span class="nc-year-picker-btn cursor-pointer hover:text-brand-500" @click="pickerType = 'year'">{{ currentYear }}</span>
</div>
<span v-else class="text-gray-700 text-sm font-semibold">{{ currentMonthYear }}</span>
<NcTooltip hide-on-click>
<NcButton class="!border-0" data-testid="nc-calendar-next-btn" size="small" type="secondary" @click="paginate('next')">
@ -159,7 +189,13 @@ const paginate = (action: 'next' | 'prev') => {
</NcTooltip>
</div>
<div v-if="!hideCalendar" class="max-w-[320px] rounded-y-xl">
<div class="flex py-1 gap-1 px-2.5 rounded-t-xl flex-row border-gray-200 justify-between">
<div class="py-1 px-2.5 h-10">
<div
class="flex gap-1"
:class="{
'border-b-1 border-gray-200 ': isCellInputField,
}"
>
<span
v-for="(day, index) in days"
:key="index"
@ -167,13 +203,22 @@ const paginate = (action: 'next' | 'prev') => {
>{{ day[0] }}</span
>
</div>
<div class="grid gap-1 py-1 px-2.5 nc-date-week-grid-wrapper grid-cols-7">
</div>
<div
class="grid gap-1 py-1 nc-date-week-grid-wrapper grid-cols-7"
:class="{
'px-2': isCellInputField,
'px-2.5': !isCellInputField,
}"
>
<span
v-for="(date, index) in dates"
:key="index"
:class="{
'rounded-lg': !isWeekPicker,
'bg-gray-200 border-1 font-bold ': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date),
'rounded-lg': !isWeekPicker && !isCellInputField,
'border-1 ': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date),
'bg-gray-200 !font-bold': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date) && !isCellInputField,
'bg-gray-300 !font-weight-600': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date) && isCellInputField,
'hover:(border-1 border-gray-200 bg-gray-100)': !isSelectedDate(date) && !isWeekPicker,
'nc-selected-week !font-semibold z-1': isDateInSelectedWeek(date) && isWeekPicker,
'border-none': isWeekPicker,
@ -183,9 +228,13 @@ const paginate = (action: 'next' | 'prev') => {
'nc-selected-week-end': isSameDate(date, selectedWeek?.end),
'rounded-md text-brand-500 !font-semibold nc-calendar-today': isSameDate(date, dayjs()) && isDateInCurrentMonth(date),
'text-gray-500': date.get('day') === 0 || date.get('day') === 6,
'nc-date-item font-weight-400': isCellInputField,
'font-medium': !isCellInputField,
'rounded': !isWeekPicker && isCellInputField,
}"
class="px-1 h-8 w-8 py-1 relative transition border-1 font-medium flex text-gray-700 items-center cursor-pointer justify-center"
class="px-1 h-8 w-8 py-1 relative transition border-1 flex text-gray-700 items-center cursor-pointer justify-center"
data-testid="nc-calendar-date"
:title="isCellInputField ? date.format('YYYY-MM-DD') : undefined"
@click="handleSelectDate(date)"
>
<span
@ -196,11 +245,16 @@ const paginate = (action: 'next' | 'prev') => {
}"
class="absolute top-1 transition right-1 h-1.5 w-1.5 z-2 border-1 rounded-full border-white bg-brand-500"
></span>
<span class="z-2">
<span class="nc-date-item-inner z-2">
{{ date.get('date') }}
</span>
</span>
</div>
<div v-if="isCellInputField" class="flex items-center justify-center px-2 pb-2 pt-1">
<NcButton class="nc-date-picker-now-btn !h-7" size="small" type="secondary" @click="handleSelectDate(dayjs())">
<span class="text-small"> {{ $t('labels.today') }} </span>
</NcButton>
</div>
</div>
</div>
</template>

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

@ -4,11 +4,13 @@ const props = withDefaults(
trigger?: Array<'click' | 'hover' | 'contextmenu'>
visible?: boolean | undefined
overlayClassName?: string | undefined
placement?: 'bottom' | 'top' | 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight' | 'topCenter' | 'bottomCenter'
autoClose?: boolean
}>(),
{
trigger: () => ['click'],
visible: undefined,
placement: 'bottomLeft',
overlayClassName: undefined,
autoClose: true,
},
@ -20,6 +22,8 @@ const trigger = toRef(props, 'trigger')
const overlayClassName = toRef(props, 'overlayClassName')
const placement = toRef(props, 'placement')
const autoClose = computed(() => props.autoClose)
const overlayClassNameComputed = computed(() => {
@ -58,6 +62,7 @@ const onVisibleUpdate = (event: any) => {
<template>
<a-dropdown
:visible="visible"
:placement="placement"
:trigger="trigger"
:overlay-class-name="overlayClassNameComputed"
@update:visible="onVisibleUpdate"

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

@ -6,20 +6,26 @@ interface Props {
pageDate?: dayjs.Dayjs
isYearPicker?: boolean
hideCalendar?: boolean
isCellInputField?: boolean
pickerType?: 'date' | 'time' | 'year' | 'month'
}
const props = withDefaults(defineProps<Props>(), {
selectedDate: null,
pageDate: dayjs(),
pageDate: () => dayjs(),
isYearPicker: false,
hideCalendar: false,
isCellInputField: false,
pickerType: 'date',
})
const emit = defineEmits(['update:selectedDate', 'update:pageDate'])
const emit = defineEmits(['update:selectedDate', 'update:pageDate', 'update:pickerType'])
const pageDate = useVModel(props, 'pageDate', emit)
const selectedDate = useVModel(props, 'selectedDate', emit)
const pickerType = useVModel(props, 'pickerType', emit)
const years = computed(() => {
const date = pageDate.value
const startOfYear = date.startOf('year')
@ -86,24 +92,41 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
<template>
<div class="flex flex-col">
<div class="flex px-2 border-b-1 py-0.5 justify-between items-center">
<div
class="flex border-b-1 justify-between items-center"
:class="{
'px-2 py-1 h-10': isCellInputField,
'px-3 py-0.5': !isCellInputField,
}"
>
<div class="flex">
<NcTooltip hide-on-click>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('prev')">
<NcButton class="nc-prev-page-btn !border-0" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.arrowLeft" class="h-4 w-4" />
</NcButton>
<template #title>
<span>{{ $t('labels.next') }}</span>
<span>{{ $t('labels.previous') }}</span>
</template>
</NcTooltip>
</div>
<span class="text-gray-700 font-semibold">{{
isYearPicker ? dayjs(selectedDate).year() : dayjs(pageDate).format('YYYY')
}}</span>
<span
class="nc-year-picker-btn text-gray-700 font-semibold"
:class="{
'cursor-pointer hover:text-brand-500': isCellInputField && !isYearPicker,
}"
@click="!isYearPicker ? (pickerType = 'year') : () => undefined"
>{{
isYearPicker
? isCellInputField
? dayjs(selectedDate).year() || dayjs().year()
: dayjs(selectedDate).year()
: dayjs(pageDate).format('YYYY')
}}</span
>
<div class="flex">
<NcTooltip hide-on-click>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('next')">
<NcButton class="nc-next-page-btn !border-0" size="small" type="secondary" @click="paginate('next')">
<component :is="iconMap.arrowRight" class="h-4 w-4" />
</NcButton>
<template #title>
@ -112,17 +135,29 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
</NcTooltip>
</div>
</div>
<div v-if="!hideCalendar" class="rounded-y-xl px-2.5 py-1 max-w-[350px]">
<div
v-if="!hideCalendar"
class="rounded-y-xl py-1 max-w-[350px]"
:class="{
'px-2': isCellInputField,
'px-2.5': !isCellInputField,
}"
>
<div class="grid grid-cols-4 gap-2">
<template v-if="!isYearPicker">
<span
v-for="(month, id) in months"
:key="id"
:class="{
'!bg-gray-200 !text-brand-900 !font-bold ': isMonthSelected(month),
'bg-gray-200 !text-brand-900 !font-bold': isMonthSelected(month) && !isCellInputField,
'bg-gray-300 !font-weight-600 ': isMonthSelected(month) && isCellInputField,
'hover:(border-1 border-gray-200 bg-gray-100)': !isMonthSelected(month),
'!text-brand-500': dayjs().isSame(month, 'month'),
'font-weight-400 rounded': isCellInputField,
'font-medium rounded-lg': !isCellInputField,
}"
class="h-8 rounded-lg flex items-center transition-all font-medium justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-700 cursor-pointer"
class="nc-month-item h-8 flex items-center transition-all justify-center text-gray-700 cursor-pointer"
:title="isCellInputField ? month.format('YYYY-MM') : undefined"
@click="selectedDate = month"
>
{{ month.format('MMM') }}
@ -133,10 +168,15 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
v-for="(year, id) in years"
:key="id"
:class="{
'!bg-gray-200 !text-brand-500 !font-bold ': compareYear(year, selectedDate),
'bg-gray-200 !text-brand-500 !font-bold ': compareYear(year, selectedDate) && !isCellInputField,
'bg-gray-300 !font-weight-600 ': compareYear(year, selectedDate) && isCellInputField,
'hover:(border-1 border-gray-200 bg-gray-100)': !compareYear(year, selectedDate),
'!text-brand-500': dayjs().isSame(year, 'year'),
'font-weight-400 text-gray-700 rounded': isCellInputField,
'font-medium text-gray-900 rounded-lg': !isCellInputField,
}"
class="h-8 rounded-lg flex items-center transition-all font-medium justify-center hover:(border-1 border-gray-200 bg-gray-100) text-gray-900 cursor-pointer"
class="nc-year-item h-8 flex items-center transition-all justify-center cursor-pointer"
:title="isCellInputField ? year.format('YYYY') : undefined"
@click="selectedDate = year"
>
{{ year.format('YYYY') }}

104
packages/nc-gui/components/nc/TimeSelector.vue

@ -0,0 +1,104 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
interface Props {
selectedDate: dayjs.Dayjs | null
is12hrFormat?: boolean
isMinGranularityPicker?: boolean
minGranularity?: number
isOpen?: boolean
}
const props = withDefaults(defineProps<Props>(), {
selectedDate: null,
is12hrFormat: false,
isMinGranularityPicker: false,
minGranularity: 30,
isOpen: false,
})
const emit = defineEmits(['update:selectedDate'])
const pageDate = ref<dayjs.Dayjs>(dayjs())
const selectedDate = useVModel(props, 'selectedDate', emit)
const { is12hrFormat, isMinGranularityPicker, minGranularity, isOpen } = toRefs(props)
const timeOptionsWrapperRef = ref<HTMLDivElement>()
const compareTime = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
if (!date1 || !date2) return false
return date1.format('HH:mm') === date2.format('HH:mm')
}
const handleSelectTime = (time: dayjs.Dayjs) => {
pageDate.value = dayjs().set('hour', time.get('hour')).set('minute', time.get('minute'))
selectedDate.value = pageDate.value
// emit('update:selectedDate', pageDate.value)
}
// TODO: 12hr time format & regular time picker
const timeOptions = computed(() => {
return Array.from({ length: is12hrFormat.value ? 12 : 24 }).flatMap((_, h) => {
return (isMinGranularityPicker.value ? [0, minGranularity.value] : Array.from({ length: 60 })).map((_m, m) => {
const time = dayjs()
.set('hour', h)
.set('minute', isMinGranularityPicker.value ? (_m as number) : m)
return time
})
})
})
const handleAutoScroll = (behavior: ScrollBehavior = 'instant') => {
if (!timeOptionsWrapperRef.value || !selectedDate.value) return
setTimeout(() => {
const timeEl = timeOptionsWrapperRef.value?.querySelector(
`[data-testid="time-option-${selectedDate.value?.format('HH:mm')}"]`,
)
timeEl?.scrollIntoView({ behavior, block: 'center' })
}, 50)
}
watch([selectedDate, isOpen], () => {
if (timeOptionsWrapperRef.value && isOpen.value && selectedDate.value) {
handleAutoScroll()
}
})
onMounted(() => {
handleAutoScroll()
})
</script>
<template>
<div class="flex flex-col max-w-[350px]">
<div v-if="isMinGranularityPicker" ref="timeOptionsWrapperRef" class="h-[180px] overflow-y-auto nc-scrollbar-thin">
<div
v-for="time of timeOptions"
:key="time.format('HH:mm')"
class="hover:bg-gray-100 py-1 px-3 text-sm text-gray-600 font-weight-500 text-center cursor-pointer"
:class="{
'nc-selected bg-gray-100': selectedDate && compareTime(time, selectedDate),
}"
:data-testid="`time-option-${time.format('HH:mm')}`"
@click="handleSelectTime(time)"
>
{{ time.format('HH:mm') }}
</div>
</div>
<div v-else></div>
<div class="px-2 py-1 box-border flex items-center justify-center">
<NcButton :tabindex="-1" class="!h-7" size="small" type="secondary" @click="handleSelectTime(dayjs())">
<span class="text-small"> {{ $t('general.now') }} </span>
</NcButton>
</div>
</div>
</template>
<style lang="scss" scoped></style>

2
packages/nc-gui/components/shared-view/Calendar.vue

@ -11,8 +11,6 @@ provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)

2
packages/nc-gui/components/shared-view/Gallery.vue

@ -11,8 +11,6 @@ provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)

1
packages/nc-gui/components/shared-view/Grid.vue

@ -18,7 +18,6 @@ provide(ReloadViewDataHookInj, reloadEventHook)
provide(ReadonlyInj, ref(true))
provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, columns)
provide(IsPublicInj, ref(true))
provide(IsLockedInj, isLocked)

2
packages/nc-gui/components/shared-view/Kanban.vue

@ -11,8 +11,6 @@ provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)

2
packages/nc-gui/components/shared-view/Map.vue

@ -11,8 +11,6 @@ provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)

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

@ -1196,7 +1196,7 @@ useEventListener(
}"
>
<!-- Form Field settings -->
<div v-if="activeField && activeColumn" :key="activeField?.id">
<div v-if="activeField && activeColumn" :key="activeField?.id" class="nc-form-field-right-panel">
<!-- Field header -->
<div class="px-3 pt-4 pb-2 flex items-center justify-between border-b border-gray-200 font-medium">
<div class="flex items-center">
@ -1297,7 +1297,7 @@ useEventListener(
<!-- Form Settings -->
<template v-else>
<Splitpanes v-if="formViewData" horizontal class="w-full nc-form-right-splitpane">
<Splitpanes v-if="formViewData" horizontal class="nc-form-settings w-full nc-form-right-splitpane">
<Pane min-size="30" size="50" class="nc-form-right-splitpane-item p-4 flex flex-col space-y-4 !min-h-200px">
<div class="flex flex-wrap justify-between items-center gap-2">
<div class="flex gap-3">
@ -1529,14 +1529,12 @@ useEventListener(
class="nc-form-hide-branding"
data-testid="nc-form-hide-branding"
:disabled="isLocked || !isEditable"
@change="
(value) => {
@change="(value) => {
if (isLocked || !isEditable) return
(formViewData!.meta as Record<string,any>).hide_branding = value
updateView()
}
"
}"
/>
<NcTooltip v-else placement="top">
@ -1558,14 +1556,12 @@ useEventListener(
class="nc-form-hide-banner"
data-testid="nc-form-hide-banner"
:disabled="isLocked || !isEditable"
@change="
(value) => {
@change="(value) => {
if (isLocked || !isEditable) return
(formViewData!.meta as Record<string,any>).hide_banner = value
updateView()
}
"
}"
/>
</div>
</div>

4
packages/nc-gui/components/smartsheet/details/Api.vue

@ -73,7 +73,7 @@ const snippet = computed(
() =>
new HTTPSnippet({
method: 'GET',
headers: [{ name: 'xc-auth', value: token.value, comment: 'JWT Auth token' }],
headers: [{ name: 'xc-token', value: '<Your API Token>', comment: 'API token' }],
url: apiUrl.value,
queryString: [
...Object.entries(queryParams.value || {}).map(([name, value]) => {
@ -95,7 +95,7 @@ const code = computed(() => {
const api = new Api({
baseURL: "${(appInfo.value && appInfo.value.ncSiteUrl) || '/'}",
headers: {
"xc-auth": ${JSON.stringify(token.value as string)}
"xc-token": "<Your API Token>"
}
})

259
packages/nc-gui/components/smartsheet/expanded-form/Comments.vue

@ -1,18 +1,30 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import type { AuditType } from 'nocodb-sdk'
import { timeAgo } from 'nocodb-sdk'
import type { CommentType } from 'nocodb-sdk'
const props = defineProps<{
loading: boolean
}>()
const { loadCommentsAndLogs, commentsAndLogs, saveComment: _saveComment, comment, updateComment } = useExpandedFormStoreOrThrow()
const {
loadComments,
deleteComment,
comments,
audits,
isCommentsLoading,
isAuditLoading,
saveComment: _saveComment,
comment: newComment,
updateComment,
} = useExpandedFormStoreOrThrow()
const { isExpandedFormCommentMode } = storeToRefs(useConfigStore())
const commentsWrapperEl = ref<HTMLDivElement>()
const commentInputRef = ref<any>()
const editRef = ref<any>()
const { user, appInfo } = useGlobal()
const isExpandedFormLoading = computed(() => props.loading)
@ -23,25 +35,12 @@ const { isUIAllowed } = useRoles()
const hasEditPermission = computed(() => isUIAllowed('commentEdit'))
const editLog = ref<AuditType>()
const editComment = ref<CommentType>()
const isEditing = ref<boolean>(false)
const isCommentMode = ref(false)
const focusCommentInput: VNodeRef = (el) => {
if (!isExpandedFormLoading.value && (isCommentMode.value || isExpandedFormCommentMode.value) && !isEditing.value) {
if (isExpandedFormCommentMode.value) {
setTimeout(() => {
isExpandedFormCommentMode.value = false
}, 400)
}
return (el as HTMLInputElement)?.focus()
}
return el
}
const focusInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
onKeyEsc(event)
@ -63,26 +62,26 @@ function onKeyEsc(event: KeyboardEvent) {
}
async function onEditComment() {
if (!isEditing.value || !editLog.value) return
if (!isEditing.value || !editComment.value) return
isCommentMode.value = true
await updateComment(editLog.value.id!, {
description: editLog.value.description,
await updateComment(editComment.value.id!, {
comment: editComment.value?.comment,
})
onStopEdit()
}
function onCancel() {
if (!isEditing.value) return
editLog.value = undefined
editComment.value = undefined
onStopEdit()
}
function onStopEdit() {
loadCommentsAndLogs()
loadComments()
isEditing.value = false
editLog.value = undefined
editComment.value = undefined
}
onKeyStroke('Enter', (event) => {
@ -91,26 +90,28 @@ onKeyStroke('Enter', (event) => {
}
})
const comments = computed(() => commentsAndLogs.value.filter((log) => log.op_type === 'COMMENT'))
const audits = computed(() => commentsAndLogs.value.filter((log) => log.op_type !== 'COMMENT' && log.details))
function editComment(log: AuditType) {
editLog.value = log
function editComments(comment: CommentType) {
editComment.value = comment
isEditing.value = true
}
const value = computed({
get() {
return editLog.value?.description?.substring(editLog.value?.description?.indexOf(':') + 1) ?? ''
return editComment.value?.comment || ''
},
set(val) {
if (!editLog.value) return
editLog.value.description = val
if (!editComment.value) return
editComment.value.comment = val
},
})
function scrollComments() {
if (commentsWrapperEl.value) commentsWrapperEl.value.scrollTop = commentsWrapperEl.value?.scrollHeight
if (commentsWrapperEl.value) {
commentsWrapperEl.value.scrollTo({
top: commentsWrapperEl.value.scrollHeight,
behavior: 'smooth',
})
}
}
const isSaving = ref(false)
@ -123,7 +124,10 @@ const saveComment = async () => {
try {
await _saveComment()
await nextTick(() => {
commentInputRef?.value?.setEditorContent('', true)
isExpandedFormCommentMode.value = true
})
scrollComments()
} catch (e) {
console.error(e)
@ -133,8 +137,12 @@ const saveComment = async () => {
}
watch(commentsWrapperEl, () => {
setTimeout(() => {
nextTick(() => {
scrollComments()
})
}, 100)
})
</script>
<template>
@ -164,118 +172,140 @@ watch(commentsWrapperEl, () => {
<div class="font-medium text-center my-6 text-gray-500">{{ $t('activity.startCommenting') }}</div>
</div>
<div v-else ref="commentsWrapperEl" class="flex flex-col h-full py-1 nc-scrollbar-thin">
<div v-for="log of comments" :key="log.id">
<div class="group gap-3 overflow-hidden hover:bg-gray-200 flex items-start px-3 pt-3 pb-4">
<GeneralUserIcon size="medium" :name="log.display_name" :email="log.user" class="mt-0.5" />
<div v-for="comment of comments" :key="comment.id" class="nc-comment-item">
<div
:class="{
'hover:bg-gray-50 bg-white': comment.id !== editComment?.id,
}"
class="group gap-3 overflow-hidden flex items-start px-3 py-1"
>
<GeneralUserIcon
:email="comment.created_by_email"
:name="comment.created_display_name"
class="mt-0.5"
size="medium"
/>
<div class="flex-1 flex flex-col gap-1 max-w-[calc(100%_-_24px)]">
<div class="w-full flex justify-between gap-3 min-h-7">
<div class="flex items-start max-w-[calc(100%_-_40px)]">
<div class="w-full flex flex-wrap items-center">
<NcTooltip class="truncate max-w-42 mr-2" show-on-truncate-only>
<div class="flex items-center max-w-[calc(100%_-_40px)]">
<div class="w-full flex flex-wrap gap-3 items-center">
<NcTooltip
class="truncate capitalize text-gray-800 font-weight-700 !text-[13px] max-w-42"
show-on-truncate-only
>
<template #title>
{{ log.display_name?.trim() || log.user || 'Shared source' }}
{{ comment.created_display_name?.trim() || comment.created_by_email || 'Shared source' }}
</template>
<span
class="text-ellipsis overflow-hidden text-gray-500 font-weight-500 text-small"
class="text-ellipsis capitalize overflow-hidden"
:style="{
lineHeight: '18px',
wordBreak: 'keep-all',
whiteSpace: 'nowrap',
display: 'inline',
}"
>
{{ log.display_name?.trim() || log.user || 'Shared source' }}
{{
comment.created_by === user?.id
? 'You'
: comment.created_display_name?.trim() || comment.created_by_email || 'Shared source'
}}
</span>
</NcTooltip>
<div v-if="log.id !== editLog?.id" class="text-xs text-gray-400">
<div class="text-xs text-gray-500">
{{
log.created_at !== log.updated_at ? `Edited ${timeAgo(log.updated_at)}` : timeAgo(log.created_at)
comment.created_at !== comment.updated_at
? `Edited ${timeAgo(comment.updated_at)}`
: timeAgo(comment.created_at)
}}
</div>
</div>
</div>
<div class="flex items-center opacity-0 transition-all group-hover:opacity-100 ease-out duration-400 gap-2">
<NcDropdown
v-if="log.user === user!.email && !editLog"
placement="bottomRight"
v-if="comment.created_by_email === user!.email && !editComment"
overlay-class-name="!min-w-[160px]"
placement="bottomRight"
>
<NcButton
type="text"
size="xsmall"
class="nc-expand-form-more-actions !w-7 !h-7 !hover:(bg-transparent text-brand-500)"
>
<GeneralIcon icon="threeDotVertical" class="text-md invisible group-hover:visible" />
<NcButton class="nc-expand-form-more-actions !w-7 !h-7 !bg-transparent" size="xsmall" type="text">
<GeneralIcon class="text-md" icon="threeDotVertical" />
</NcButton>
<template #overlay>
<NcMenu>
<NcMenuItem v-e="['c:row-expand:comment:edit']" class="text-gray-700" @click="editComment(log)">
<NcMenuItem
v-e="['c:row-expand:comment:edit']"
class="text-gray-700"
@click="editComments(comment)"
>
<div class="flex gap-2 items-center">
<component :is="iconMap.rename" class="cursor-pointer" />
{{ $t('general.edit') }}
</div>
</NcMenuItem>
<!-- eslint-disable vue/no-constant-condition -->
<template v-if="false">
<NcDivider />
<NcMenuItem v-e="['c:row-expand:comment:delete']" class="!text-red-500 !hover:bg-red-50">
<NcMenuItem
v-e="['c:row-expand:comment:delete']"
class="!text-red-500 !hover:bg-red-50"
@click="deleteComment(comment.id!)"
>
<div class="flex gap-2 items-center">
<GeneralIcon icon="delete" />
<component :is="iconMap.delete" class="cursor-pointer" />
{{ $t('general.delete') }}
</div>
</NcMenuItem>
</template>
</NcMenu>
</template>
</NcDropdown>
</div>
</div>
<a-textarea
v-if="log.id === editLog?.id"
:ref="focusInput"
<SmartsheetExpandedFormRichComment
v-if="comment.id === editComment?.id"
ref="editRef"
v-model:value="value"
class="!p-1.5 !m-0 w-full !rounded-md !text-gray-800 !text-small !leading-18px !min-h-[70px] nc-scrollbar-thin"
@keydown.stop="onKeyDown($event)"
autofocus
:hide-options="false"
class="expanded-form-comment-input !pt-1 !pb-0.5 !pl-2 !m-0 w-full !border-1 !border-gray-200 !rounded-lg !bg-transparent !text-gray-800 !text-small !leading-18px !max-h-[694px]"
data-testid="expanded-form-comment-input"
sync-value-change
@save="onEditComment"
@keydown.stop="onKeyDown"
@blur="
() => {
editComment = undefined
isEditing = false
}
"
@keydown.enter.exact.prevent="onEditComment"
/>
<div v-else class="text-small leading-18px text-gray-800">
<SmartsheetExpandedFormRichComment
:value="comment.comment"
class="!text-small !leading-18px !text-gray-800 -ml-1"
read-only
sync-value-change
/>
<div v-else class="nc-comment-description text-small leading-18px text-gray-800">
<pre>{{ log.description.substring(log.description.indexOf(':') + 1).trim() }}</pre>
</div>
<div v-if="log.id === editLog?.id" class="flex justify-end gap-1 mt-1">
<NcButton size="small" type="secondary" @click="onCancel"> Cancel </NcButton>
<NcButton v-e="['a:row-expand:comment:save']" size="small" @click="onEditComment"> Save </NcButton>
</div>
</div>
</div>
</div>
</div>
<div v-if="hasEditPermission" class="p-3 gap-2 flex">
<div class="flex flex-row w-full items-end gap-2">
<div class="expanded-form-comment-input-wrapper">
<a-textarea
:ref="focusCommentInput"
v-model:value="comment"
class="expanded-form-comment-input !py-1.5 !px-3 !m-0 w-full !border-1 !border-gray-200 !rounded-lg !bg-transparent !text-gray-800 !text-small !leading-18px !max-h-[694px] nc-scrollbar-thin"
auto-size
hide-details
:disabled="isSaving"
<div v-if="hasEditPermission" class="bg-gray-50 nc-comment-input !rounded-br-2xl gap-2 flex">
<SmartsheetExpandedFormRichComment
ref="commentInputRef"
v-model:value="newComment"
:hide-options="false"
placeholder="Comment..."
class="expanded-form-comment-input !m-0 pt-2 w-full !border-t-1 !border-gray-200 !bg-transparent !text-gray-800 !text-small !leading-18px !max-h-[566px]"
:autofocus="isExpandedFormCommentMode"
data-testid="expanded-form-comment-input"
@focus="isExpandedFormCommentMode = false"
@keydown.stop
@save="saveComment"
@keydown.enter.exact.prevent="saveComment"
/>
</div>
<NcButton
v-e="['a:row-expand:comment:save']"
size="small"
:loading="isSaving"
:disabled="!isSaving && !comment.length"
:icon-only="isSaving"
class="!disabled:bg-gray-100 !shadow-none"
@click="saveComment"
>
<GeneralIcon v-if="!isSaving" icon="send" />
</NcButton>
</div>
</div>
</div>
</div>
</a-tab-pane>
@ -304,7 +334,7 @@ watch(commentsWrapperEl, () => {
'pb-1': !appInfo.ee,
}"
>
<div v-if="isExpandedFormLoading" class="flex flex-col h-full">
<div v-if="isExpandedFormLoading || isAuditLoading" class="flex flex-col h-full">
<GeneralLoader class="!mt-16" size="xlarge" />
</div>
@ -318,14 +348,14 @@ watch(commentsWrapperEl, () => {
</div>
</template>
<div v-for="log of audits" :key="log.id" class="nc-audit-item">
<div v-for="audit of audits" :key="audit.id" class="nc-audit-item">
<div class="group gap-3 overflow-hidden flex items-start p-3">
<GeneralUserIcon size="medium" :email="log.user" :name="log.display_name" />
<GeneralUserIcon size="medium" :email="audit.user" :name="audit.display_name" />
<div class="flex-1 flex flex-col gap-1 max-w-[calc(100%_-_24px)]">
<div class="flex flex-wrap items-center min-h-7">
<NcTooltip class="truncate max-w-42 mr-2" show-on-truncate-only>
<template #title>
{{ log.display_name?.trim() || log.user || 'Shared source' }}
{{ audit.display_name?.trim() || audit.user || 'Shared source' }}
</template>
<span
class="text-ellipsis overflow-hidden font-bold text-gray-800"
@ -335,14 +365,14 @@ watch(commentsWrapperEl, () => {
display: 'inline',
}"
>
{{ log.display_name?.trim() || log.user || 'Shared source' }}
{{ audit.display_name?.trim() || audit.user || 'Shared source' }}
</span>
</NcTooltip>
<div v-if="log.id !== editLog?.id" class="text-xs text-gray-400">
{{ timeAgo(log.created_at) }}
<div class="text-xs text-gray-400">
{{ timeAgo(audit.created_at) }}
</div>
</div>
<div v-dompurify-html="log.details" class="text-sm font-medium"></div>
<div v-dompurify-html="audit.details" class="text-sm font-medium"></div>
</div>
</div>
</div>
@ -358,6 +388,12 @@ watch(commentsWrapperEl, () => {
@apply max-w-1/2;
}
.nc-comment-input {
:deep(.nc-comment-rich-editor) {
@apply !ml-1;
}
}
.nc-audit-item {
@apply border-b-1 gap-3 border-gray-200;
}
@ -386,10 +422,11 @@ watch(commentsWrapperEl, () => {
}
:deep(.ant-tabs) {
@apply !overflow-visible;
.ant-tabs-nav {
@apply px-3;
.ant-tabs-nav-list {
@apply w-[calc(100%_-_24px)] gap-6;
@apply w-[99%] mx-auto gap-6;
.ant-tabs-tab {
@apply flex-1 flex items-center justify-center pt-3 pb-2.5;
@ -407,21 +444,11 @@ watch(commentsWrapperEl, () => {
}
}
.nc-comment-description {
pre {
@apply !mb-0 py-[1px] !text-small !text-gray-700 !leading-18px;
white-space: break-spaces;
font-size: unset;
font-family: unset;
}
}
.expanded-form-comment-input-wrapper {
@apply flex-1 bg-white rounded-lg relative;
}
:deep(.expanded-form-comment-input) {
@apply transition-all duration-150;
@apply transition-all duration-150 min-h-8;
box-shadow: none;
&:focus {
&:focus,
&:focus-within {
@apply min-h-16;
}
&::placeholder {

380
packages/nc-gui/components/smartsheet/expanded-form/RichComment.vue

@ -0,0 +1,380 @@
<script lang="ts" setup>
import StarterKit from '@tiptap/starter-kit'
import { EditorContent, useEditor } from '@tiptap/vue-3'
import TurndownService from 'turndown'
import { marked } from 'marked'
import { generateJSON } from '@tiptap/html'
import Underline from '@tiptap/extension-underline'
import Placeholder from '@tiptap/extension-placeholder'
import { Link } from '~/helpers/dbTiptapExtensions/links'
const props = withDefaults(
defineProps<{
hideOptions?: boolean
value?: string | null
readOnly?: boolean
syncValueChange?: boolean
autofocus?: boolean
placeholder?: string
renderAsText?: boolean
}>(),
{
hideOptions: true,
},
)
const emits = defineEmits(['update:value', 'focus', 'blur', 'save'])
const isGrid = inject(IsGridInj, ref(false))
const isFocused = ref(false)
const keys = useMagicKeys()
const turndownService = new TurndownService({})
turndownService.addRule('lineBreak', {
filter: (node) => {
return node.nodeName === 'BR'
},
replacement: () => {
return '<br />'
},
})
turndownService.addRule('strikethrough', {
filter: ['s'],
replacement: (content) => {
return `~${content}~`
},
})
turndownService.keep(['u', 'del'])
const editorDom = ref<HTMLElement | null>(null)
const richTextLinkOptionRef = ref<HTMLElement | null>(null)
const vModel = useVModel(props, 'value', emits, { defaultValue: '' })
const tiptapExtensions = [
StarterKit.configure({
heading: false,
}),
Underline,
Link,
Placeholder.configure({
emptyEditorClass: 'is-editor-empty',
placeholder: props.placeholder,
}),
]
const editor = useEditor({
extensions: tiptapExtensions,
onUpdate: ({ editor }) => {
const markdown = turndownService
.turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />'))
.replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n')
vModel.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('.comment-bubble-menu, .nc-comment-rich-editor, .nc-rich-text-comment')
) {
isFocused.value = false
emits('blur')
}
},
})
const setEditorContent = (contentMd: any, focusEndOfDoc?: boolean) => {
if (!editor.value) return
const selection = editor.value.view.state.selection
const contentHtml = contentMd ? marked.parse(contentMd) : '<p></p>'
const content = generateJSON(contentHtml, tiptapExtensions)
editor.value.chain().setContent(content).setTextSelection(selection.to).run()
setTimeout(() => {
if (focusEndOfDoc) {
const docSize = editor.value!.state.doc.nodeSize
editor.value
?.chain()
.setTextSelection(docSize - 1)
.run()
}
;(editor.value!.state as any).history$.prevRanges = null
;(editor.value!.state as any).history$.done.eventCount = 0
}, 100)
}
const onFocusWrapper = () => {
if (!props.readOnly && !keys.shift.value) {
editor.value?.chain().focus().run()
}
}
if (props.syncValueChange) {
watch([vModel, editor], () => {
setEditorContent(vModel.value)
})
}
useEventListener(
editorDom,
'focusout',
(e: FocusEvent) => {
const targetEl = e?.relatedTarget as HTMLElement
if (
targetEl?.classList?.contains('tiptap') ||
!targetEl?.closest('.comment-bubble-menu, .tippy-content, .nc-comment-rich-editor')
) {
isFocused.value = false
emits('blur')
}
},
true,
)
useEventListener(
richTextLinkOptionRef,
'focusout',
(e: FocusEvent) => {
const targetEl = e?.relatedTarget as HTMLElement
if (!targetEl && (e.target as HTMLElement)?.closest('.comment-bubble-menu, .tippy-content, .nc-comment-rich-editor')) return
if (!targetEl?.closest('.comment-bubble-menu, .tippy-content, .nc-comment-rich-editor')) {
isFocused.value = false
emits('blur')
}
},
true,
)
onClickOutside(editorDom, (e) => {
if (!isFocused.value) return
const targetEl = e?.target as HTMLElement
if (!targetEl?.closest('.tippy-content, .comment-bubble-menu, .nc-comment-rich-editor')) {
isFocused.value = false
emits('blur')
}
})
const triggerSaveFromList = ref(false)
const emitSave = (event: KeyboardEvent) => {
if (editor.value) {
if (triggerSaveFromList.value) {
// If Enter was pressed in the list, do not emit save
triggerSaveFromList.value = false
} else {
if (editor.value.isActive('bulletList') || editor.value.isActive('orderedList')) {
event.stopPropagation()
} else {
emits('save')
}
}
}
}
const handleEnterDown = (event: KeyboardEvent) => {
const isListsActive = editor.value?.isActive('bulletList') || editor.value?.isActive('orderedList')
if (isListsActive) {
triggerSaveFromList.value = true
setTimeout(() => {
triggerSaveFromList.value = false
}, 1000)
} else {
emitSave(event)
}
}
const handleKeyPress = (event: KeyboardEvent) => {
if (event.altKey && event.key === 'Enter') {
event.stopPropagation()
} else if (event.shiftKey && event.key === 'Enter') {
event.stopPropagation()
} else if (event.key === 'Enter') {
handleEnterDown(event)
} else if (event.key === 'Escape') {
isFocused.value = false
emits('blur')
}
}
defineExpose({
setEditorContent,
})
</script>
<template>
<div
:class="{
'readonly': readOnly,
'nc-rich-text-grid': isGrid,
}"
:tabindex="1"
class="nc-rich-text-comment flex flex-col w-full h-full"
@focus="onFocusWrapper"
>
<div v-if="renderAsText" class="truncate">
<span v-if="editor"> {{ editor?.getText() ?? '' }}</span>
</div>
<template v-else>
<CellRichTextLinkOptions
v-if="editor"
ref="richTextLinkOptionRef"
:editor="editor"
:is-form-field="true"
@blur="isFocused = false"
/>
<EditorContent
ref="editorDom"
:editor="editor"
class="flex flex-col nc-comment-rich-editor px-1.5 w-full scrollbar-thin scrollbar-thumb-gray-200 nc-truncate scrollbar-track-transparent"
@keydown.stop="handleKeyPress"
/>
<div v-if="!hideOptions" class="flex justify-between px-2 py-2 items-center">
<LazySmartsheetExpandedFormRichTextOptions :editor="editor" class="!bg-transparent" />
<NcButton
v-e="['a:row-expand:comment:save']"
:disabled="!vModel?.length"
class="!disabled:bg-gray-100 !h-7 !w-7 !shadow-none"
size="xsmall"
@click="emits('save')"
>
<GeneralIcon icon="send" />
</NcButton>
</div>
</template>
</div>
</template>
<style lang="scss">
.nc-rich-text-comment {
.readonly {
.nc-comment-rich-editor {
.ProseMirror {
resize: none;
white-space: pre-line;
}
}
}
.nc-comment-rich-editor {
&.nc-truncate {
.tiptap.ProseMirror {
display: -webkit-box;
max-width: 100%;
outline: none;
-webkit-box-orient: vertical;
word-break: break-word;
}
&.nc-line-clamp-1 .tiptap.ProseMirror {
-webkit-line-clamp: 1;
}
&.nc-line-clamp-2 .tiptap.ProseMirror {
-webkit-line-clamp: 2;
}
&.nc-line-clamp-3 .tiptap.ProseMirror {
-webkit-line-clamp: 3;
}
&.nc-line-clamp-4 .tiptap.ProseMirror {
-webkit-line-clamp: 4;
}
}
.tiptap p.is-editor-empty:first-child::before {
color: #9aa2af;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.ProseMirror {
@apply flex-grow !border-0 rounded-lg;
caret-color: #3366ff;
}
p {
@apply !m-0;
}
.ProseMirror-focused {
// remove all border
outline: none;
}
ul {
li {
@apply ml-4;
list-style-type: disc;
}
}
ol {
@apply !pl-4;
li {
list-style-type: decimal;
}
}
ul,
ol {
@apply !my-0;
}
// Pre tag is the parent wrapper for Code block
pre {
border-color: #d0d5dd;
border: 1px;
color: black;
font-family: 'JetBrainsMono', monospace;
padding: 1rem;
border-radius: 0.5rem;
@apply overflow-auto mt-3 bg-gray-100;
code {
@apply !px-0;
}
}
code {
@apply rounded-md px-2 py-1 bg-gray-100;
color: inherit;
font-size: 0.8rem;
}
blockquote {
border-left: 3px solid #d0d5dd;
padding: 0 1em;
color: #666;
margin: 1em 0;
font-style: italic;
}
hr {
@apply !border-gray-300;
border: 0;
border-top: 1px solid #ccc;
margin: 1.5em 0;
}
pre {
height: fit-content;
}
}
}
</style>

215
packages/nc-gui/components/smartsheet/expanded-form/RichTextOptions.vue

@ -0,0 +1,215 @@
<script lang="ts" setup>
import type { Editor } from '@tiptap/vue-3'
import MdiFormatStrikeThrough from '~icons/mdi/format-strikethrough'
interface Props {
editor: Editor | undefined
}
const props = withDefaults(defineProps<Props>(), {})
const { appInfo } = useGlobal()
const { editor } = toRefs(props)
const cmdOrCtrlKey = computed(() => {
return isMac() ? '⌘' : 'CTRL'
})
const shiftKey = computed(() => {
return isMac() ? '⇧' : 'Shift'
})
const tabIndex = computed(() => {
return -1
})
const onToggleLink = () => {
if (!editor.value) return
const activeNode = editor.value?.state?.selection?.$from?.nodeBefore || editor.value?.state?.selection?.$from?.nodeAfter
const isLinkMarkedStoredInEditor = editor.value?.state?.storedMarks?.some((mark: any) => mark.type.name === 'link')
const isActiveNodeMarkActive = activeNode?.marks?.some((mark: any) => mark.type.name === 'link') || isLinkMarkedStoredInEditor
if (isActiveNodeMarkActive) {
editor.value.chain().focus().unsetLink().run()
} else {
if (editor.value?.state.selection.empty) {
editor
.value!.chain()
.focus()
.insertContent(' ')
.setTextSelection({ from: editor.value?.state.selection.$from.pos, to: editor.value.state.selection.$from.pos + 1 })
.toggleLink({
href: '',
})
.setTextSelection({ from: editor.value?.state.selection.$from.pos, to: editor.value.state.selection.$from.pos + 1 })
.deleteSelection()
.run()
} else {
editor
.value!.chain()
.focus()
.setLink({
href: '',
})
.selectTextblockEnd()
.run()
}
setTimeout(() => {
const linkInput = document.querySelector('.nc-text-area-rich-link-option-input')
if (linkInput) {
;(linkInput as any).focus()
}
}, 100)
}
}
const newMentionNode = () => {
editor.value?.commands.insertContent('@')
editor.value?.chain().focus().run()
}
</script>
<template>
<div class="comment-bubble-menu bg-transparent flex-row rounded-lg flex">
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.bold') }}
</div>
<div class="text-xs">{{ cmdOrCtrlKey }} B</div>
</div>
</template>
<NcButton
:class="{ 'is-active': editor?.isActive('bold') }"
:tabindex="tabIndex"
class="!h-7 !w-7 !hover:bg-gray-200"
size="xsmall"
type="text"
@click="editor?.chain().focus().toggleBold().run()"
>
<GeneralIcon icon="bold" />
</NcButton>
</NcTooltip>
<NcTooltip :disabled="editor?.isActive('italic')">
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.italic') }}
</div>
<div>{{ cmdOrCtrlKey }} I</div>
</div>
</template>
<NcButton
:class="{ 'is-active': editor?.isActive('italic') }"
:tabindex="tabIndex"
class="!h-7 !w-7 !hover:bg-gray-200"
size="xsmall"
type="text"
@click=";(editor?.chain().focus() as any).toggleItalic().run()"
>
<GeneralIcon icon="italic" />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.underline') }}
</div>
<div>{{ cmdOrCtrlKey }} U</div>
</div>
</template>
<NcButton
:class="{ 'is-active': editor?.isActive('underline') }"
:tabindex="tabIndex"
class="!h-7 !w-7 !hover:bg-gray-200"
size="xsmall"
type="text"
@click="editor?.chain().focus().toggleUnderline().run()"
>
<GeneralIcon icon="underline" />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.strike') }}
</div>
<div>{{ shiftKey }} {{ cmdOrCtrlKey }} S</div>
</div>
</template>
<NcButton
:class="{ 'is-active': editor?.isActive('strike') }"
:tabindex="tabIndex"
class="!h-7 !w-7 !hover:bg-gray-200"
size="xsmall"
type="text"
@click="editor?.chain().focus().toggleStrike().run()"
>
<GeneralIcon icon="strike" />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title> {{ $t('general.link') }}</template>
<NcButton
:class="{ 'is-active': editor?.isActive('link') }"
:tabindex="tabIndex"
class="!h-7 !w-7 !hover:bg-gray-200"
size="xsmall"
type="text"
@click="onToggleLink"
>
<GeneralIcon icon="link2"></GeneralIcon>
</NcButton>
</NcTooltip>
<NcTooltip v-if="appInfo.ee">
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.mention') }}
</div>
<div>@</div>
</div>
</template>
<NcButton
:class="{ 'is-active': editor?.isActive('suggestions') }"
:tabindex="tabIndex"
class="!h-7 !w-7 !hover:bg-gray-200"
size="xsmall"
type="text"
@click="newMentionNode"
>
<GeneralIcon icon="atSign" />
</NcButton>
</NcTooltip>
</div>
</template>
<style lang="scss" scoped>
.comment-bubble-menu {
@apply !border-none;
.nc-button.is-active {
@apply text-brand-500;
outline: 1px;
}
.ant-select-selector {
@apply !rounded-md;
}
.ant-select-selector .ant-select-selection-item {
@apply !text-xs;
}
.ant-btn-loading-icon {
@apply pb-0.5;
}
}
</style>

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

@ -129,7 +129,8 @@ const {
saveRowAndStay,
row: _row,
save: _save,
loadCommentsAndLogs,
loadComments,
loadAudits,
clearColumns,
} = useProvideExpandedFormStore(meta, row)
@ -320,13 +321,13 @@ onMounted(async () => {
if (props.loadRow) {
await _loadRow()
await loadCommentsAndLogs()
await Promise.all([loadComments(), loadAudits()])
}
if (props.rowId) {
try {
await _loadRow(props.rowId)
await loadCommentsAndLogs()
await Promise.all([loadComments(), loadAudits()])
} catch (e: any) {
if (e.response?.status === 404) {
message.error(t('msg.noRecordFound'))
@ -458,7 +459,7 @@ const onConfirmDeleteRowClick = async () => {
watch(rowId, async (nRow) => {
await _loadRow(nRow)
await loadCommentsAndLogs()
await Promise.all([loadComments(), loadAudits()])
})
const showRightSections = computed(() => {
@ -562,7 +563,7 @@ export default {
<div
class="flex min-h-7 flex-shrink-0 w-full items-center nc-expanded-form-header relative p-4 xs:(px-2 py-0 min-h-[48px]) justify-between"
>
<div class="flex-1 flex gap-3 lg:w-100 <lg:max-w-[calc(100%_-_178px)] xs:(max-w-[calc(100%_-_44px)])">
<div class="flex-1 flex gap-4 lg:w-100 <lg:max-w-[calc(100%_-_178px)] xs:(max-w-[calc(100%_-_44px)])">
<div class="flex gap-2">
<NcTooltip v-if="props.showNextPrevIcons">
<template #title> {{ renderAltOrOptlKey() }} + </template>
@ -601,9 +602,9 @@ export default {
}"
>
<div v-if="meta.title" class="flex items-center gap-2 px-2 py-1 rounded-lg bg-gray-100 text-gray-800">
<GeneralTableIcon :meta="meta" class="!text-gray-800" />
<GeneralTableIcon :meta="meta" class="!text-gray-800 !mx-0" />
<NcTooltip class="truncate max-w-[100px] xs:(max-w-[82px]) h-5" show-on-truncate-only>
<NcTooltip class="truncate text-sm max-w-[100px] xs:(max-w-[82px]) align-middle" show-on-truncate-only>
<template #title>
{{ meta.title }}
</template>
@ -617,7 +618,7 @@ export default {
{{ props.newRecordHeader ?? $t('activity.newRecord') }}
</div>
<div
v-else-if="displayValue && !row.rowMeta?.new"
v-else-if="displayValue && !row?.rowMeta?.new"
class="flex items-center font-bold text-gray-800 text-base max-w-[300px] xs:(w-auto max-w-[calc(100%_-_82px)]) overflow-hidden"
>
<span class="truncate">
@ -627,6 +628,21 @@ export default {
</div>
</div>
<div class="flex gap-2">
<NcTooltip v-if="!isMobileMode && isUIAllowed('dataEdit')">
<template #title> {{ renderAltOrOptlKey() }} + S </template>
<NcButton
v-e="['c:row-expand:save']"
:disabled="changedColumns.size === 0 && !isUnsavedFormExist"
:loading="isSaving"
class="nc-expand-form-save-btn !xs:(text-base) !h-7 !px-2"
data-testid="nc-expanded-form-save"
type="primary"
size="xsmall"
@click="save"
>
<div class="xs:px-1">{{ newRecordSubmitBtnText ?? 'Save Record' }}</div>
</NcButton>
</NcTooltip>
<NcButton
v-if="!isNew && rowId && !isMobileMode"
:disabled="isLoading"
@ -645,21 +661,6 @@ export default {
{{ isRecordLinkCopied ? $t('labels.copiedRecordURL') : $t('labels.copyRecordURL') }}
</div>
</NcButton>
<NcTooltip v-if="!isMobileMode && isUIAllowed('dataEdit')">
<template #title> {{ renderAltOrOptlKey() }} + S </template>
<NcButton
v-e="['c:row-expand:save']"
:disabled="changedColumns.size === 0 && !isUnsavedFormExist"
:loading="isSaving"
class="nc-expand-form-save-btn !xs:(text-base) !h-7 !px-2"
data-testid="nc-expanded-form-save"
type="primary"
size="xsmall"
@click="save"
>
<div class="xs:px-1">{{ newRecordSubmitBtnText ?? 'Save Record' }}</div>
</NcButton>
</NcTooltip>
<NcDropdown v-if="!isNew && rowId && !isMobileMode" placement="bottomRight">
<NcButton type="text" size="xsmall" class="nc-expand-form-more-actions !w-7 !h-7" :disabled="isLoading">
<GeneralIcon icon="threeDotVertical" class="text-md" :class="isLoading ? 'text-gray-300' : 'text-gray-700'" />
@ -720,7 +721,7 @@ export default {
</NcButton>
</div>
</div>
<div ref="wrapper" class="flex flex-grow flex-row h-[calc(100%-4rem)] w-full overflow-hidden border-t-1 border-gray-200">
<div ref="wrapper" class="flex flex-grow flex-row h-[calc(100%-4rem)] w-full border-t-1 border-gray-200">
<div
:class="{
'w-full': !showRightSections,
@ -730,7 +731,7 @@ export default {
>
<div
ref="expandedFormScrollWrapper"
class="flex flex-col flex-grow gap-3 h-full max-h-full nc-scrollbar-thin items-center w-full p-4 xs:(px-4 pt-4 pb-2 gap-6) children:max-w-[588px] <lg:(children:max-w-[450px])"
class="flex flex-col flex-grow gap-4 h-full max-h-full nc-scrollbar-thin items-center w-full p-4 xs:(px-4 pt-4 pb-2 gap-6) children:max-w-[588px] <lg:(children:max-w-[450px])"
>
<div
v-for="(col, i) of fields"
@ -937,7 +938,7 @@ export default {
<div
v-if="showRightSections"
:class="{ active: commentsDrawer && isUIAllowed('commentList') }"
class="nc-comments-drawer border-l-1 relative border-gray-200 bg-gray-50 w-1/3 max-w-[340px] min-w-0 overflow-hidden h-full xs:hidden"
class="nc-comments-drawer border-l-1 relative border-gray-200 bg-gray-50 w-1/3 max-w-[340px] min-w-0 h-full xs:hidden rounded-br-2xl"
>
<SmartsheetExpandedFormComments :loading="isLoading" />
</div>
@ -981,10 +982,6 @@ export default {
.nc-drawer-expanded-form {
@apply xs:my-0;
.ant-modal-content {
@apply overflow-hidden;
}
.ant-drawer-content-wrapper {
@apply !h-[90vh];
.ant-drawer-content {
@ -1024,14 +1021,18 @@ export default {
}
.nc-data-cell {
box-shadow: 0 0 1px rgba(0, 0, 0, 0.1);
&:hover,
@apply !rounded-lg;
transition: all 0.3s;
&:hover {
@apply !border-1 !border-brand-400;
}
&:focus-within {
box-shadow: 0 0 3px rgba(0, 0, 0, 0.1) !important;
box-shadow: 0px 0px 0px 2px rgba(51, 102, 255, 0.24) !important;
}
}
.nc-data-cell:focus-within {
@apply !border-1 !border-brand-500 !rounded-lg;
@apply !border-1 !border-brand-500;
}
:deep(.nc-system-field input) {

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

@ -1136,6 +1136,9 @@ const calculateSlices = () => {
start: 0,
end: 0,
}
// try again until the grid is rendered
setTimeout(calculateSlices, 100)
return
}

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

@ -26,6 +26,8 @@ const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isExpandedBulkUpdateForm = inject(IsExpandedBulkUpdateFormOpenInj, ref(false))
const isDropDownOpen = ref(false)
const isKanban = inject(IsKanbanInj, ref(false))
@ -58,7 +60,7 @@ const closeAddColumnDropdown = () => {
}
const openHeaderMenu = (e?: MouseEvent) => {
if (isLocked.value || (isExpandedForm.value && e?.type === 'dblclick')) return
if (isLocked.value || (isExpandedForm.value && e?.type === 'dblclick') || isExpandedBulkUpdateForm.value) return
if (!isForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value) {
editColumnDropdown.value = true
@ -83,7 +85,7 @@ const onClick = (e: Event) => {
e.preventDefault()
e.stopPropagation()
} else {
if (isExpandedForm.value && !editColumnDropdown.value) {
if (isExpandedForm.value && !editColumnDropdown.value && !isExpandedBulkUpdateForm.value) {
isDropDownOpen.value = true
return
}
@ -99,9 +101,10 @@ const onClick = (e: Event) => {
:class="{
'h-full': column,
'!text-gray-400': isKanban,
'flex-col !items-start justify-center': isExpandedForm && !isMobileMode,
'cursor-pointer hover:bg-gray-100': isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit'),
'bg-gray-100': isExpandedForm ? editColumnDropdown || isDropDownOpen : false,
'flex-col !items-start justify-center pt-0.5': isExpandedForm && !isMobileMode && !isExpandedBulkUpdateForm,
'cursor-pointer hover:bg-gray-100':
isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit') && !isExpandedBulkUpdateForm,
'bg-gray-100': isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false,
}"
@dblclick="openHeaderMenu"
@click.right="openDropDown"
@ -111,7 +114,7 @@ const onClick = (e: Event) => {
class="nc-cell-name-wrapper flex-1 flex items-center"
:class="{
'max-w-[calc(100%_-_23px)]': !isExpandedForm,
'max-w-full': isExpandedForm,
'max-w-full': isExpandedForm && !isExpandedBulkUpdateForm,
}"
>
<template v-if="column && !props.hideIcon">
@ -119,7 +122,7 @@ const onClick = (e: Event) => {
v-if="isGrid"
class="flex items-center"
placement="bottom"
:disabled="isExpandedForm ? editColumnDropdown || isDropDownOpen : false"
:disabled="isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false"
>
<template #title> {{ columnTypeName }} </template>
<SmartsheetHeaderCellIcon
@ -138,21 +141,21 @@ const onClick = (e: Event) => {
<NcTooltip
v-if="column"
:class="{
'cursor-pointer pt-0.25': !isForm && isUIAllowed('fieldEdit') && !hideMenu,
'cursor-pointer': !isForm && isUIAllowed('fieldEdit') && !hideMenu,
'cursor-default': isForm || !isUIAllowed('fieldEdit') || hideMenu,
'truncate': !isForm,
}"
class="name pl-1 max-w-full"
placement="bottom"
show-on-truncate-only
:disabled="isExpandedForm ? editColumnDropdown || isDropDownOpen : false"
:disabled="isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false"
>
<template #title> {{ column.title }} </template>
<span
:data-test-id="column.title"
:class="{
'select-none': isExpandedForm,
'select-none': isExpandedForm && !isExpandedBulkUpdateForm,
}"
>
{{ column.title }}
@ -162,9 +165,9 @@ const onClick = (e: Event) => {
<span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span>
<GeneralIcon
v-if="isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit')"
v-if="isExpandedForm && !isExpandedBulkUpdateForm && !isMobileMode && isUIAllowed('fieldEdit')"
icon="arrowDown"
class="flex-none text-grey h-full text-grey cursor-pointer ml-1 group-hover:visible"
class="flex-none cursor-pointer ml-1 group-hover:visible w-4 h-4"
:class="{
visible: editColumnDropdown || isDropDownOpen,
invisible: !(editColumnDropdown || isDropDownOpen),
@ -187,10 +190,10 @@ const onClick = (e: Event) => {
v-model:visible="editColumnDropdown"
class="h-full"
:trigger="['click']"
:placement="isExpandedForm ? 'bottomLeft' : 'bottomRight'"
:placement="isExpandedForm && !isExpandedBulkUpdateForm ? 'bottomLeft' : 'bottomRight'"
overlay-class-name="nc-dropdown-edit-column"
>
<div v-if="isExpandedForm" class="h-[1px]" @dblclick.stop>&nbsp;</div>
<div v-if="isExpandedForm && !isExpandedBulkUpdateForm" class="h-[1px]" @dblclick.stop>&nbsp;</div>
<div v-else />
<template #overlay>

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

@ -46,6 +46,8 @@ const isForm = inject(IsFormInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isExpandedBulkUpdateForm = inject(IsExpandedBulkUpdateFormOpenInj, ref(false))
const colOptions = computed(() => column.value?.colOptions)
const tableTile = computed(() => meta?.value?.title)
@ -140,7 +142,7 @@ const closeAddColumnDropdown = () => {
}
const openHeaderMenu = (e?: MouseEvent) => {
if (isLocked.value || (isExpandedForm.value && e?.type === 'dblclick')) return
if (isLocked.value || (isExpandedForm.value && e?.type === 'dblclick') || isExpandedBulkUpdateForm.value) return
if (!isForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value) {
editColumnDropdown.value = true
@ -165,7 +167,7 @@ const onClick = (e: Event) => {
e.preventDefault()
e.stopPropagation()
} else {
if (isExpandedForm.value && !editColumnDropdown.value) {
if (isExpandedForm.value && !editColumnDropdown.value && !isExpandedBulkUpdateForm.value) {
isDropDownOpen.value = true
return
}
@ -179,9 +181,10 @@ const onClick = (e: Event) => {
<div
class="flex items-center w-full h-full text-small text-gray-500 font-weight-medium group"
:class="{
'flex-col !items-start justify-center': isExpandedForm,
'bg-gray-100': isExpandedForm ? editColumnDropdown || isDropDownOpen : false,
'cursor-pointer hover:bg-gray-100': isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit'),
'flex-col !items-start justify-center pt-0.5': isExpandedForm && !isMobileMode && !isExpandedBulkUpdateForm,
'bg-gray-100': isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false,
'cursor-pointer hover:bg-gray-100':
isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit') && !isExpandedBulkUpdateForm,
}"
@dblclick="openHeaderMenu"
@click.right="openDropDown"
@ -191,7 +194,7 @@ const onClick = (e: Event) => {
class="nc-virtual-cell-name-wrapper flex-1 flex items-center"
:class="{
'max-w-[calc(100%_-_23px)]': !isExpandedForm,
'max-w-full': isExpandedForm,
'max-w-full': isExpandedForm && !isExpandedBulkUpdateForm,
}"
>
<template v-if="column && !props.hideIcon">
@ -208,7 +211,7 @@ const onClick = (e: Event) => {
<span
:data-test-id="column.title"
:class="{
'select-none': isExpandedForm,
'select-none': isExpandedForm && !isExpandedBulkUpdateForm,
}"
>
{{ column.title }}
@ -218,9 +221,9 @@ const onClick = (e: Event) => {
<span v-if="isVirtualColRequired(column, meta?.columns || []) || required" class="text-red-500">&nbsp;*</span>
<GeneralIcon
v-if="isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit')"
v-if="isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit') && !isExpandedBulkUpdateForm"
icon="arrowDown"
class="flex-none h-full cursor-pointer ml-1 group-hover:visible"
class="flex-none cursor-pointer ml-1 group-hover:visible w-4 h-4"
:class="{
visible: editColumnDropdown || isDropDownOpen,
invisible: !(editColumnDropdown || isDropDownOpen),
@ -244,10 +247,10 @@ const onClick = (e: Event) => {
v-model:visible="editColumnDropdown"
class="h-full"
:trigger="['click']"
:placement="isExpandedForm ? 'bottomLeft' : 'bottomRight'"
:placement="isExpandedForm && !isExpandedBulkUpdateForm ? 'bottomLeft' : 'bottomRight'"
overlay-class-name="nc-dropdown-edit-column"
>
<div v-if="isExpandedForm" class="h-[1px]" @dblclick.stop>&nbsp;</div>
<div v-if="isExpandedForm && !isExpandedBulkUpdateForm" class="h-[1px]" @dblclick.stop>&nbsp;</div>
<div v-else />
<template #overlay>
<SmartsheetColumnEditOrAddProvider

47
packages/nc-gui/components/smartsheet/toolbar/Calendar/Range.vue

@ -44,6 +44,26 @@ const calendarRange = computed<CalendarRangeType[]>(() => {
// We keep the calendar range here and update it when the user selects a new range
const _calendar_ranges = ref<CalendarRangeType[]>(calendarRange.value)
const isSetup = computed(() => {
return _calendar_ranges.value.length > 0 && _calendar_ranges.value[0].fk_from_column_id
})
watch(
calendarRangeDropdown,
(newVal) => {
if (!newVal && !isSetup.value) {
calendarRangeDropdown.value = true
if (_calendar_ranges.value.length === 0) {
_calendar_ranges.value.push({
fk_from_column_id: undefined,
fk_to_column_id: null,
})
}
}
},
{ immediate: true },
)
const saveCalendarRanges = async () => {
if (activeView.value) {
try {
@ -56,8 +76,12 @@ const saveCalendarRanges = async () => {
await $api.dbView.calendarUpdate(activeView.value?.id as string, {
calendar_range: calRanges as CalendarRangeType[],
})
if (activeView.value.view) activeView.value.view.calendar_range = calRanges
await loadCalendarMeta()
await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()])
calendarRangeDropdown.value = false
} catch (e) {
console.log(e)
message.error('There was an error while updating view!')
@ -78,13 +102,6 @@ const dateFieldOptions = computed<SelectProps['options']>(() => {
})) ?? []
)
})
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)
@ -235,22 +252,14 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</NcButton>
-->
</div>
<div v-if="!isSetup" class="flex items-center gap-2 !mt-2">
<GeneralIcon icon="warning" class="text-sm mt-0.5 text-orange-500" />
<span class="text-sm text-gray-500"> Date field is required! </span>
</div>
<!--
<div class="text-[13px] text-gray-500 py-2">Records in this view will be based on the specified date field.</div>
-->
<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 date field
</NcButton>
</div>
</template>
</NcDropdown>

246
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -11,6 +11,7 @@ interface Props {
modelValue?: undefined | Filter[]
webHook?: boolean
draftFilter?: Partial<FilterType>
isOpen?: boolean
}
const props = withDefaults(defineProps<Props>(), {
@ -27,6 +28,7 @@ const emit = defineEmits(['update:filtersLength', 'update:draftFilter', 'update:
const excludedFilterColUidt = [UITypes.QrCode, UITypes.Barcode]
const draftFilter = useVModel(props, 'draftFilter', emit)
const modelValue = useVModel(props, 'modelValue', emit)
const { nestedLevel, parentId, autoSave, hookId, showLoading, webHook } = toRefs(props)
@ -386,6 +388,15 @@ watch(
immediate: true,
},
)
const addFilterBtnRef = ref()
watchEffect(() => {
if (props.isOpen && !nested.value && addFilterBtnRef.value) {
setTimeout(() => {
addFilterBtnRef.value?.$el?.focus()
}, 10)
}
})
</script>
<template>
@ -394,13 +405,63 @@ watch(
:class="{
'max-h-[max(80vh,500px)] min-w-112 py-2 pl-4': !nested,
'w-full ': nested,
'py-4': !filters.length,
}"
>
<div v-if="nested" class="flex w-full items-center mb-2">
<div :class="[`nc-filter-logical-op-level-${nestedLevel}`]"><slot name="start"></slot></div>
<div class="flex-grow"></div>
<NcDropdown :trigger="['hover']" overlay-class-name="nc-dropdown-filter-group-sub-menu">
<GeneralIcon icon="plus" class="cursor-pointer" />
<template #overlay>
<NcMenu>
<template v-if="isEeUI && !isPublic">
<template v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)">
<NcMenuItem @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</div>
</NcMenuItem>
<NcMenuItem v-if="nestedLevel < 5" @click.stop="addFilterGroup()">
<div class="flex items-center gap-1">
<!-- Add Filter Group -->
<component :is="iconMap.plusSquare" />
{{ $t('activity.addFilterGroup') }}
</div>
</NcMenuItem>
</template>
</template>
<template v-else>
<NcMenuItem @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</div>
</NcMenuItem>
<NcMenuItem v-if="!webHook && nestedLevel < 5" @click.stop="addFilterGroup()">
<div class="flex items-center gap-1">
<!-- Add Filter Group -->
<component :is="iconMap.plusSquare" />
{{ $t('activity.addFilterGroup') }}
</div>
</NcMenuItem>
</template>
</NcMenu>
</template>
</NcDropdown>
<div>
<slot name="end"></slot>
</div>
</div>
<div
v-if="filters && filters.length"
ref="wrapperDomRef"
class="flex flex-col gap-y-3 nc-filter-grid w-full"
class="flex flex-col gap-y-1.5 nc-filter-grid w-full"
:class="{ 'max-h-420px nc-scrollbar-thin nc-filter-top-wrapper pr-4 my-2 py-1': !nested }"
@click.stop
>
@ -408,17 +469,29 @@ watch(
<template v-if="filter.status !== 'delete'">
<template v-if="filter.is_group">
<div class="flex flex-col w-full gap-y-2">
<div class="flex flex-row w-full justify-between items-center">
<span v-if="!i" class="flex items-center ml-2">{{ $t('labels.where') }}</span>
<div class="flex rounded-lg p-2 w-full border-1" :class="[`nc-filter-nested-level-${nestedLevel}`]">
<LazySmartsheetToolbarColumnFilter
v-if="filter.id || filter.children || !autoSave"
:key="filter.id ?? i"
ref="localNestedFilters"
v-model="filter.children"
:nested-level="nestedLevel + 1"
:parent-id="filter.id"
:auto-save="autoSave"
:web-hook="webHook"
>
<template #start>
<span v-if="!i" class="flex items-center nc-filter-where-label ml-1">{{ $t('labels.where') }}</span>
<div v-else :key="`${i}nested`" class="flex nc-filter-logical-op">
<NcSelect
v-model:value="filter.logical_op"
v-e="['c:filter:logical-op:select']"
:dropdown-match-select-width="false"
class="min-w-20 capitalize"
class="min-w-18 max-w-18 capitalize"
placeholder="Group op"
dropdown-class-name="nc-dropdown-filter-logical-op-group"
:disabled="visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed"
:disabled="i > 1 && !isLogicalOpChangeAllowed"
:class="{ 'nc-disabled-logical-op': filter.readOnly || (i > 1 && !isLogicalOpChangeAllowed) }"
@click.stop
@change="onLogicalOpUpdate(filter, i)"
>
@ -435,6 +508,8 @@ watch(
</a-select-option>
</NcSelect>
</div>
</template>
<template #end>
<NcButton
v-if="!filter.readOnly"
:key="i"
@ -446,33 +521,29 @@ watch(
>
<component :is="iconMap.deleteListItem" />
</NcButton>
</div>
<div class="flex border-1 rounded-lg p-2 w-full" :class="nestedLevel % 2 !== 0 ? 'bg-white' : 'bg-gray-100'">
<LazySmartsheetToolbarColumnFilter
v-if="filter.id || filter.children || !autoSave"
:key="filter.id ?? i"
ref="localNestedFilters"
v-model="filter.children"
:nested-level="nestedLevel + 1"
:parent-id="filter.id"
:auto-save="autoSave"
:web-hook="webHook"
/>
</template>
</LazySmartsheetToolbarColumnFilter>
</div>
</div>
</template>
<div v-else class="flex flex-row gap-x-2 w-full" :class="`nc-filter-wrapper-${filter.fk_column_id}`">
<span v-if="!i" class="flex items-center ml-2 mr-7.35">{{ $t('labels.where') }}</span>
<div v-else class="flex flex-row gap-x-0 w-full nc-filter-wrapper" :class="`nc-filter-wrapper-${filter.fk_column_id}`">
<div v-if="!i" class="flex items-center !min-w-18 !max-w-18 pl-3 nc-filter-where-label">
{{ $t('labels.where') }}
</div>
<NcSelect
v-else
v-model:value="filter.logical_op"
v-e="['c:filter:logical-op:select']"
:dropdown-match-select-width="false"
class="h-full !min-w-20 !max-w-20 capitalize"
class="h-full !min-w-18 !max-w-18 capitalize"
hide-details
:disabled="filter.readOnly || (visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed)"
dropdown-class-name="nc-dropdown-filter-logical-op"
:class="{
'nc-disabled-logical-op': filter.readOnly || (visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed),
}"
@change="onLogicalOpUpdate(filter, i)"
@click.stop
>
@ -488,6 +559,7 @@ watch(
</div>
</a-select-option>
</NcSelect>
<SmartsheetToolbarFieldListAutoCompleteDropdown
:key="`${i}_6`"
v-model="filter.fk_column_id"
@ -497,6 +569,7 @@ watch(
@click.stop
@change="selectFilterField(filter, i)"
/>
<NcSelect
v-model:value="filter.comparison_op"
v-e="['c:filter:comparison-op:select']"
@ -529,6 +602,7 @@ watch(
</NcSelect>
<div v-if="['blank', 'notblank'].includes(filter.comparison_op)" class="flex flex-grow"></div>
<NcSelect
v-else-if="isDateType(types[filter.fk_column_id])"
v-model:value="filter.comparison_sub_op"
@ -567,6 +641,7 @@ watch(
</a-select-option>
</template>
</NcSelect>
<a-checkbox
v-if="filter.field && types[filter.field] === 'boolean'"
v-model:checked="filter.value"
@ -583,6 +658,7 @@ watch(
@update-filter-value="(value) => updateFilterValue(value, filter, i)"
@click.stop
/>
<div v-else-if="!isDateType(types[filter.fk_column_id])" class="flex-grow"></div>
<NcButton
@ -600,16 +676,16 @@ watch(
</template>
</div>
<template v-if="!nested">
<template v-if="isEeUI && !isPublic">
<div
v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)"
ref="addFiltersRowDomRef"
class="flex gap-2"
:class="{
'mt-1 mb-2': filters.length,
}"
>
<NcButton size="small" type="text" class="!text-brand-500" @click.stop="addFilter()">
<NcButton :ref="addFilterBtnRef" size="small" type="text" class="nc-btn-focus" @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
<!-- Add Filter -->
@ -617,7 +693,7 @@ watch(
</div>
</NcButton>
<NcButton v-if="nestedLevel < 5" type="text" size="small" @click.stop="addFilterGroup()">
<NcButton v-if="nestedLevel < 5" class="nc-btn-focus" type="text" size="small" @click.stop="addFilterGroup()">
<div class="flex items-center gap-1">
<!-- Add Filter Group -->
<component :is="iconMap.plus" />
@ -626,6 +702,7 @@ watch(
</NcButton>
</div>
</template>
<template v-else>
<div
ref="addFiltersRowDomRef"
@ -634,7 +711,7 @@ watch(
'mt-1 mb-2': filters.length,
}"
>
<NcButton size="small" type="text" class="!text-brand-500" @click.stop="addFilter()">
<NcButton ref="addFilterBtnRef" class="nc-btn-focus" size="small" type="text" @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
<!-- Add Filter -->
@ -642,7 +719,13 @@ watch(
</div>
</NcButton>
<NcButton v-if="!webHook && nestedLevel < 5" type="text" size="small" @click.stop="addFilterGroup()">
<NcButton
v-if="!webHook && nestedLevel < 5"
class="nc-btn-focus"
type="text"
size="small"
@click.stop="addFilterGroup()"
>
<div class="flex items-center gap-1">
<!-- Add Filter Group -->
<component :is="iconMap.plus" />
@ -651,6 +734,7 @@ watch(
</NcButton>
</div>
</template>
</template>
<div
v-if="!filters.length"
class="flex flex-row text-gray-400 mt-2"
@ -666,7 +750,7 @@ watch(
</div>
</template>
<style scoped>
<style scoped lang="scss">
.nc-filter-item-remove-btn {
@apply text-gray-600 hover:text-gray-800;
}
@ -680,6 +764,112 @@ watch(
}
:deep(.ant-select-selector) {
@apply !min-h-8.25;
@apply !min-h-8;
}
.nc-disabled-logical-op :deep(.ant-select-arrow) {
@apply hidden;
}
.nc-filter-wrapper {
@apply bg-white !rounded-lg border-1px border-[#E7E7E9];
& > * {
@apply !border-none;
}
& > * > :deep(.ant-select-selector) {
border: none !important;
box-shadow: none !important;
}
& > :not(:last-child):not(:empty) {
border-right: 1px solid #eee !important;
border-bottom-right-radius: 0 !important;
border-top-right-radius: 0 !important;
}
& > :not(:first-child) {
border-bottom-left-radius: 0 !important;
border-top-left-radius: 0 !important;
}
& > :last-child {
@apply relative;
&::after {
content: '';
@apply absolute h-full w-1px bg-[#eee] -left-1px top-0;
}
}
:deep(::placeholder) {
@apply text-sm tracking-normal;
}
:deep(::-ms-input-placeholder) {
@apply text-sm tracking-normal;
}
:deep(input) {
@apply text-sm;
}
:deep(.nc-select:not(.nc-disabled-logical-op):hover) {
&,
.ant-select-selector {
@apply bg-gray-50;
}
}
}
.nc-filter-nested-level-0 {
@apply bg-[#f9f9fa];
}
.nc-filter-nested-level-1,
.nc-filter-nested-level-3 {
@apply bg-gray-[#f4f4f5];
}
.nc-filter-nested-level-2,
.nc-filter-nested-level-4 {
@apply bg-gray-[#e7e7e9];
}
.nc-filter-logical-op-level-3,
.nc-filter-logical-op-level-5 {
:deep(.nc-select.ant-select .ant-select-selector) {
@apply border-[#d9d9d9];
}
}
.nc-filter-where-label {
@apply text-gray-400;
}
:deep(.ant-select-disabled.ant-select:not(.ant-select-customize-input) .ant-select-selector) {
@apply bg-transparent text-gray-400;
}
:deep(.nc-filter-logical-op .nc-select.ant-select .ant-select-selector) {
@apply shadow-none;
}
:deep(.nc-select-expand-btn) {
@apply text-gray-500;
}
.menu-filter-dropdown {
input:not(:disabled),
select:not(:disabled),
.ant-select:not(.ant-select-disabled) {
@apply text-[#4A5268];
}
}
.nc-filter-input-wrapper :deep(input) {
@apply !px-2;
}
.nc-btn-focus:focus {
@apply !text-brand-500 !shadow-none;
}
</style>

1
packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue

@ -85,6 +85,7 @@ eventBus.on(async (event, column: ColumnType) => {
class="nc-table-toolbar-menu"
:auto-save="true"
data-testid="nc-filter-menu"
:is-open="open"
@update:filters-length="filtersLength = $event"
>
</SmartsheetToolbarColumnFilter>

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

@ -63,14 +63,15 @@ const options = computed<SelectProps['options']>(() =>
return !isVirtualSystemField
}
})
)?.map((c: ColumnType) => ({
)
// sort and keep system columns at the end
?.sort((field1, field2) => +isSystemColumn(field2) - +isSystemColumn(field1))
?.map((c: ColumnType) => ({
value: c.id,
label: c.title,
icon: h(
isVirtualCol(c) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'),
{
columnMeta: c,
},
{ columnMeta: c },
),
c,
})),

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

@ -11,8 +11,6 @@ const reloadViewMetaHook = inject(ReloadViewMetaHookInj, undefined)!
const reloadViewDataHook = inject(ReloadViewDataHookInj, undefined)!
const rootFields = inject(FieldsInj)
const { isMobileMode } = useGlobal()
const isLocked = inject(IsLockedInj, ref(false))
@ -23,7 +21,6 @@ const { $api, $e } = useNuxtApp()
const {
showSystemFields,
sortedAndFilteredFields,
fields,
filteredFieldList,
filterQuery,
@ -48,14 +45,6 @@ eventBus.on((event) => {
}
})
watch(
sortedAndFilteredFields,
(v) => {
if (rootFields) rootFields.value = v || []
},
{ immediate: true },
)
const numberOfHiddenFields = computed(() => filteredFieldList.value?.filter((field) => !field.show)?.length)
const gridDisplayValueField = computed(() => {

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

@ -16,8 +16,6 @@ useSidebar('nc-right-sidebar')
const activeTab = toRef(props, 'activeTab')
const fields = ref<ColumnType[]>([])
const route = useRoute()
const meta = computed<TableType | undefined>(() => {
@ -50,7 +48,6 @@ provide(IsLockedInj, isLocked)
provide(ReloadViewDataHookInj, reloadViewDataEventHook)
provide(ReloadViewMetaHookInj, reloadViewMetaEventHook)
provide(OpenNewRecordFormHookInj, openNewRecordFormHook)
provide(FieldsInj, fields)
provide(IsFormInj, isForm)
provide(TabMetaInj, activeTab)
provide(
@ -60,6 +57,7 @@ provide(
useExpandedFormDetachedProvider()
useProvideViewColumns(activeView, meta, () => reloadViewDataEventHook?.trigger())
useProvideViewGroupBy(activeView, meta, xWhere)
useProvideSmartsheetLtarHelpers(meta)

6
packages/nc-gui/components/webhook/Editor.vue

@ -821,7 +821,11 @@ onMounted(async () => {
</div>
<div class="my-3">
<a href="https://docs.nocodb.com/developer-resources/webhooks/" target="_blank" rel="noopener">
<a
href="https://docs.nocodb.com/automation/webhook/create-webhook/#webhook-with-custom-payload-"
target="_blank"
rel="noopener"
>
<!-- Document Reference -->
{{ $t('labels.docReference') }}
</a>

139
packages/nc-gui/composables/useExpandedFormStore.ts

@ -1,4 +1,4 @@
import type { AuditType, ColumnType, TableType } from 'nocodb-sdk'
import type { AuditType, ColumnType, CommentType, TableType } from 'nocodb-sdk'
import { UITypes, ViewTypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import dayjs from 'dayjs'
@ -6,15 +6,23 @@ import dayjs from 'dayjs'
const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((meta: Ref<TableType>, _row: Ref<Row>) => {
const { $e, $state, $api } = useNuxtApp()
const { api, isLoading: isCommentsLoading, error: commentsError } = useApi()
const { t } = useI18n()
const isPublic = inject(IsPublicInj, ref(false))
const commentsOnly = ref(false)
const comments = ref<
Array<
CommentType & {
created_display_name: string
}
>
>([])
const audits = ref<Array<AuditType>>([])
const commentsAndLogs = ref<any[]>([])
const isCommentsLoading = ref(false)
const isAuditLoading = ref(false)
const comment = ref('')
@ -24,8 +32,14 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const changedColumns = ref(new Set<string>())
const basesStore = useBases()
const { basesUser } = storeToRefs(basesStore)
const { base } = storeToRefs(useBase())
const baseUsers = computed(() => (meta.value.base_id ? basesUser.value.get(meta.value.base_id) || [] : []))
const { sharedView } = useSharedView()
const row = ref<Row>(
@ -87,24 +101,73 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
return extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
})
// actions
const loadCommentsAndLogs = async () => {
if (!isUIAllowed('commentList')) return
const loadComments = async () => {
if (!isUIAllowed('commentList') || !row.value) return
const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!rowId) return
try {
isCommentsLoading.value = true
const res = ((
await $api.utils.commentList({
row_id: rowId,
fk_model_id: meta.value.id as string,
})
).list || []) as Array<
CommentType & {
created_display_name: string
}
>
comments.value = res.map((comment) => {
const user = baseUsers.value.find((u) => u.id === comment.created_by)
return {
...comment,
created_display_name: user?.display_name ?? (user?.email ?? '').split('@')[0],
}
})
} catch (e: any) {
message.error(e.message)
} finally {
isCommentsLoading.value = false
}
}
const deleteComment = async (commentId: string) => {
if (!isUIAllowed('commentDelete')) return
if (!row.value) return
try {
await $api.utils.commentDelete(commentId)
await loadComments()
} catch (e: any) {
message.error(e.message)
}
}
const loadAudits = async () => {
if (!isUIAllowed('auditList') || !row.value) return
const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!rowId) return
commentsAndLogs.value =
try {
isAuditLoading.value = true
audits.value =
(
await api.utils.commentList({
await $api.utils.auditList({
row_id: rowId,
fk_model_id: meta.value.id as string,
comments_only: commentsOnly.value,
})
).list?.reverse?.() || []
} catch (e: any) {
message.error(e.message)
} finally {
isAuditLoading.value = false
}
}
const isYou = (email: string) => {
@ -126,15 +189,15 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
if (!rowId) return
await api.utils.commentRow({
await $api.utils.commentRow({
fk_model_id: meta.value?.id as string,
row_id: rowId,
description: `The following comment has been created: ${comment.value}`,
comment: `${comment.value}`,
})
reloadTrigger?.trigger()
await loadCommentsAndLogs()
await Promise.all([loadComments(), loadAudits()])
comment.value = ''
} catch (e: any) {
@ -154,6 +217,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
kanbanClbk?: (row: Row, isNewRow: boolean) => void
} = {},
) => {
if (!meta.value.id) return
let data
const isNewRow = row.value.rowMeta?.new ?? false
@ -222,6 +287,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
obj[col] = row.value.row[col]
return obj
}, {} as Record<string, any>)
if (Object.keys(updateOrInsertObj).length) {
const id = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
@ -240,7 +306,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
addUndo({
redo: {
fn: async (id: string, data: Record<string, any>) => {
await $api.dbTableRow.update(NOCO, base.value.id as string, meta.value.id, encodeURIComponent(id), data)
await $api.dbTableRow.update(NOCO, base.value.id as string, meta.value.id!, encodeURIComponent(id), data)
await loadKanbanData()
reloadTrigger?.trigger()
@ -249,7 +315,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
},
undo: {
fn: async (id: string, data: Record<string, any>) => {
await $api.dbTableRow.update(NOCO, base.value.id as string, meta.value.id, encodeURIComponent(id), data)
await $api.dbTableRow.update(NOCO, base.value.id as string, meta.value.id!, encodeURIComponent(id), data)
await loadKanbanData()
reloadTrigger?.trigger()
},
@ -260,7 +326,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
}
if (commentsDrawer.value) {
await loadCommentsAndLogs()
await Promise.all([loadComments(), loadAudits()])
}
} else {
// No columns to update
@ -283,15 +349,18 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
}
const loadRow = async (rowId?: string, onlyVirtual = false, onlyNewColumns = false) => {
if (row.value.rowMeta.new) return
if (row.value.rowMeta.new || isPublic.value || !meta.value?.id) return
const recordId = rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!recordId) return
if (isPublic.value || !meta.value?.id) return
let record = await $api.dbTableRow.read(
NOCO,
// todo: base_id missing on view type
(base?.value?.id || (sharedView.value?.view as any)?.base_id) as string,
((base?.value?.id ?? meta.value?.base_id) || (sharedView.value?.view as any)?.base_id) as string,
meta.value.id as string,
encodeURIComponent(rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])),
encodeURIComponent(recordId),
{
getHiddenColumn: true,
},
@ -301,9 +370,9 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
if (onlyVirtual) {
record = {
...row.value.row,
...meta.value.columns.reduce((partialRecord, col) => {
if (isVirtualCol(col) && col.title in record) {
partialRecord[col.title] = record[col.title]
...(meta.value.columns ?? []).reduce((partialRecord, col) => {
if (isVirtualCol(col) && col.title && col.title in record) {
partialRecord[col.title] = (record as Record<string, any>)[col.title as string]
}
return partialRecord
}, {} as Record<string, any>),
@ -314,7 +383,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
if (onlyNewColumns) {
record = Object.keys(record).reduce((acc, curr) => {
if (!Object.prototype.hasOwnProperty.call(row.value.row, curr)) {
acc[curr] = record[curr]
acc[curr] = record(record as Record<string, any>)[curr]
} else {
acc[curr] = row.value.row[curr]
}
@ -331,11 +400,13 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const deleteRowById = async (rowId?: string) => {
try {
const recordId = rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
const res: { message?: string[] } | number = await $api.dbTableRow.delete(
NOCO,
base.value.id as string,
meta.value.id as string,
encodeURIComponent(rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])),
encodeURIComponent(recordId),
)
if (res.message) {
@ -351,17 +422,19 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
}
}
const updateComment = async (auditId: string, audit: Partial<AuditType>) => {
return await $api.utils.commentUpdate(auditId, audit)
const updateComment = async (commentId: string, comment: Partial<CommentType>) => {
return await $api.utils.commentUpdate(commentId, comment)
}
return {
...rowStore,
commentsOnly,
loadCommentsAndLogs,
commentsAndLogs,
loadComments,
deleteComment,
loadAudits,
comments,
audits,
isAuditLoading,
isCommentsLoading,
commentsError,
saveComment,
comment,
isYou,

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

@ -6,9 +6,11 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
(
view: Ref<ViewType | undefined>,
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
reloadData?: () => void,
reloadData?: (params?: { shouldShowLoading?: boolean }) => void,
isPublic = false,
) => {
const rootFields = ref<ColumnType[]>([])
const fields = ref<Field[]>()
const filterQuery = ref('')
@ -356,6 +358,16 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
}
}
watch(
sortedAndFilteredFields,
(v) => {
if (rootFields) rootFields.value = v || []
},
{ immediate: true },
)
provide(FieldsInj, rootFields)
return {
fields,
loadViewColumns,

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

@ -188,7 +188,7 @@ export function useViewFilters(
comparison_op: comparisonOpList(options.value?.[0].uidt as UITypes).filter((compOp) =>
isComparisonOpAllowed({ fk_column_id: options.value?.[0].id }, compOp),
)?.[0].value as FilterType['comparison_op'],
value: '',
value: null,
status: 'create',
logical_op: logicalOps.size === 1 ? logicalOps.values().next().value : 'and',
}

1
packages/nc-gui/context/index.ts

@ -19,6 +19,7 @@ export const IsGalleryInj: InjectionKey<Ref<boolean>> = Symbol('is-gallery-injec
export const IsKanbanInj: InjectionKey<Ref<boolean>> = Symbol('is-kanban-injection')
export const IsLockedInj: InjectionKey<Ref<boolean>> = Symbol('is-locked-injection')
export const IsExpandedFormOpenInj: InjectionKey<Ref<boolean>> = Symbol('is-expanded-form-open-injection')
export const IsExpandedBulkUpdateFormOpenInj: InjectionKey<Ref<boolean>> = Symbol('is-expanded-bulk-update-form-open-injection')
export const CellValueInj: InjectionKey<Ref<any>> = Symbol('cell-value-injection')
export const ActiveViewInj: InjectionKey<Ref<ViewType>> = Symbol('active-view-injection')
export const ReadonlyInj: InjectionKey<Ref<boolean>> = Symbol('readonly-injection')

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "اسم الجدول",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Project not accessible",
"copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Table name",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Project not accessible",
"copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Akceptované typy souborů jsou .xls, .xlsx, .xlsm, .ods, .ots.",
"parameterKeyCannotBeEmpty": "Klíč parametru nesmí být prázdný",
"duplicateParameterKeysAreNotAllowed": "Duplicitní klíče parametrů nejsou povoleny",
"fieldRequired": "{value} nemůže být prázdný.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Projekt není přístupný",
"copyToClipboardError": "Nepodařilo se zkopírovat do schránky",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Tabelnavn.",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "De accepterede filtyper er .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameternøglen kan ikke være tom",
"duplicateParameterKeysAreNotAllowed": "Det er ikke tilladt at duplikere parameternøgler",
"fieldRequired": "{value} kan ikke være tom.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Projektet er ikke tilgængeligt",
"copyToClipboardError": "Kopiering til udklipsholderen mislykkedes",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Tabellenname",
"dashboardName": "Dashboard name",
"createView": "Neue Ansicht erstellen",
"createView": "Ansicht erstellen",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Die akzeptierten Dateitypen sind .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameterschlüssel darf nicht leer sein",
"duplicateParameterKeysAreNotAllowed": "Doppelte Parameterschlüssel sind nicht erlaubt",
"fieldRequired": "{value} kann nicht leer sein.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Projekt nicht zugänglich",
"copyToClipboardError": "Kopieren in die Zwischenablage fehlgeschlagen",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -450,6 +451,7 @@
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -589,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Table name",
"dashboardName": "Dashboard name",
"createView": "Create view",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -756,7 +758,7 @@
"selectField": "Select a field",
"selectFieldLabel": "Make changes to field properties by selecting a field from the list"
},
"appearanceSettings": "Appearance settings",
"appearanceSettings": "Appearance Settings",
"backgroundColor": "Background Color",
"hideNocodbBranding": "Hide NocoDB Branding",
"showOnConditions": "Show on condtions",
@ -919,7 +921,7 @@
"clearMetadata": "Clear Metadata",
"exportToFile": "Export to file",
"changePwd": "Change Password",
"createView": "Create a View",
"createView": "Create View",
"shareView": "Share View",
"findRowByCodeScan": "Find record by scan",
"fillByCodeScan": "Fill by scan",
@ -1015,7 +1017,7 @@
"group": "Group"
},
"tooltip": {
"reachedSourceLimit": "Limited to 10 data sources per base",
"reachedSourceLimit": "Limited to only one data source for the moment",
"saveChanges": "Save changes",
"xcDB": "Create a new base",
"extDB": "Supports MySQL, PostgreSQL, SQL Server & SQLite",
@ -1286,7 +1288,7 @@
"afterEnablePwd": "Access is password restricted",
"privateLink": "This view is shared via a private link",
"privateLinkAdditionalInfo": "People with private link can only see cells visible in this view",
"postFormSubmissionSettings": "Post form submission settings",
"postFormSubmissionSettings": "Post Form Submission Settings",
"apiOptions": "Access Base via",
"submitAnotherForm": "Show 'Submit Another Form' button",
"showBlankForm": "Show a blank form after 5 seconds",
@ -1498,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "This field cannot be empty.",
"fieldRequired": "{value} cannot be empty.",
"projectNotAccessible": "Base not accessible",
"copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Token sin título",
"tableName": "Nombre de la tabla",
"dashboardName": "Nombre del panel",
"createView": "Crear una vista",
"createView": "Crear Vista",
"creatingView": "Creando Vista",
"duplicateView": "Duplicar vista",
"duplicateGridView": "Duplicar vista en cuadrcula",
@ -917,7 +921,7 @@
"clearMetadata": "Limpiar metadatos",
"exportToFile": "Exportar a archivo",
"changePwd": "Cambia la contraseña",
"createView": "Crear una Vista",
"createView": "Crear vista",
"shareView": "Compartir vista",
"findRowByCodeScan": "Find row by scan",
"fillByCodeScan": "Rellenar por escaneo",
@ -1013,7 +1017,7 @@
"group": "Group"
},
"tooltip": {
"reachedSourceLimit": "Limited to 10 data sources per base",
"reachedSourceLimit": "Limitado a una única fuente de datos por el momento",
"saveChanges": "Guardar cambios",
"xcDB": "Crear un nuevo proyecto",
"extDB": "Soporta MySQL, PostgreSQL, SQL Server y SQLite",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Los tipos de archivo aceptados son .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "La clave del parámetro no puede estar vacía",
"duplicateParameterKeysAreNotAllowed": "No se permiten claves de parámetros duplicadas",
"fieldRequired": "{value} no puede estar vacía.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Proyecto no accesible",
"copyToClipboardError": "Fallo al copiar al portapapeles",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Taularen izena",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -917,7 +921,7 @@
"clearMetadata": "Clear Metadata",
"exportToFile": "Export to file",
"changePwd": "Change Password",
"createView": "Create a View",
"createView": "Create View",
"shareView": "Share View",
"findRowByCodeScan": "Find row by scan",
"fillByCodeScan": "Fill by scan",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Project not accessible",
"copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "نام جدول",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "نوع فایل های مورد قبول .xls, .xlsx, .xlm, .ods, .ots هستند",
"parameterKeyCannotBeEmpty": "کلید پارامتر نمیتواند خالی باشد",
"duplicateParameterKeysAreNotAllowed": "کلید های پارامتر تکراری مجاز نیستند",
"fieldRequired": "{value} نمیتواند خالی باشد.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Project not accessible",
"copyToClipboardError": "کپی به کلیپ برد ناموفق",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Taulukon nimi",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Hyväksytyt tiedostotyypit ovat .xls, .xlsx, .xlsm, .ods, .ots ja .xlsx.",
"parameterKeyCannotBeEmpty": "Parametriavain ei voi olla tyhjä",
"duplicateParameterKeysAreNotAllowed": "Parametrin kaksoisavaimet eivät ole sallittuja",
"fieldRequired": "{value} ei voi olla tyhjä.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Hankkeeseen ei pääse käsiksi",
"copyToClipboardError": "Kopiointi leikepöydälle epäonnistui",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -10,7 +10,7 @@
"connect": "Connecter",
"buttonActionTypes": {
"open_external_url": "Ouvrir le lien externe",
"delete_record": "Supprimer la ligne",
"delete_record": "Supprimer l'enregistrement",
"update_record": "Mettre à jour l'enregistrement",
"open_layout": "Ouvrir la mise en page"
},
@ -39,7 +39,7 @@
}
},
"general": {
"role": "Role",
"role": "Rôle",
"general": "Général",
"quit": "Quitter",
"home": "Accueil",
@ -197,14 +197,15 @@
"paste": "Coller",
"restore": "Restaurer",
"replace": "Remplacer",
"banner": "Banner",
"banner": "Bandeau",
"logo": "Logo",
"dropdown": "Liste déroulante",
"list": "Liste",
"verify": "Vérifier",
"apply": "Appliquer",
"text": "Texte",
"appearance": "Apparence"
"appearance": "Apparence",
"now": "Now"
},
"objects": {
"owner": "Propriétaire",
@ -293,7 +294,7 @@
"Formula": "Formule",
"Rollup": "Synthèse",
"Count": "Compteur",
"Lookup": "Consulter",
"Lookup": "Lookup",
"DateTime": "Date et heure",
"CreatedTime": "Date de création",
"LastModifiedTime": "Dernière modification",
@ -320,9 +321,9 @@
"isNotNull": "est non null"
},
"title": {
"renameBase": "Rename Base",
"renameBase": "Renommer la Base",
"renameWorkspace": "Renommer l'espace de travail",
"renamingWorkspace": "Renaming Workspace",
"renamingWorkspace": "Renommer l'espace de travail",
"renamingBase": "Renommer la Base",
"sso": "Authentification (SSO)",
"docs": "Documents",
@ -354,7 +355,7 @@
"manyToMany": "Plusieurs à plusieurs",
"oneToOne": "Un à un",
"virtualRelation": "Virtual Relation",
"linkMore": "Link More",
"linkMore": "Lier plus",
"linkMoreRecords": "Lier plus d'enregistrements",
"linkRecords": "Lier les enregistrements",
"downloadFile": "Télécharger le fichier",
@ -378,7 +379,7 @@
"formTitle": "Intitulé du formulaire",
"collaborative": "Collaboratif",
"locked": "Verrouillé",
"personal": "Personal",
"personal": "Personnels",
"appStore": "Magasin d'applications",
"teamAndAuth": "Équipe & Authentification",
"rolesUserMgmt": "Gestion des utilisateurs & rôles",
@ -436,54 +437,57 @@
"switchLanguage": "Changer de langue",
"renameFile": "Renommer le fichier",
"links": {
"noAction": "No Action",
"noAction": "Aucune action",
"cascade": "Cascade",
"restrict": "Restrict",
"restrict": "Restreindre",
"setNull": "Définir NULL",
"setDefault": "Définir à la valeur par défaut"
},
"selectFieldsFromRightPannelToAddHere": "Select fields from right panel to add here",
"noOptionsFound": "No options found",
"surveyFormSubmitConfirmMsg": "Are you sure you want to submit this form?",
"selectFieldsFromRightPannelToAddHere": "Sélectionnez les champs du panneau de droite à ajouter ici",
"noOptionsFound": "Aucune option trouvée",
"surveyFormSubmitConfirmMsg": "Êtes-vous sûr de vouloir envoyer ce formulaire ?",
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"today": "Today",
"workspace": "Workspace",
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Aujourd'hui",
"workspace": "Espace de travail",
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"transferOwnership": "Définir un nouveau propriétaire",
"recentActivity": "Activité récente",
"goToMembers": "Aller aux membres",
"addMember": "Ajouter un membre",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"numberOfMembers": "Nombre de membres",
"numberOfBases": "Nombre de bases",
"numberOfRecords": "Nombre d'enregistrements",
"workspaceName": "Nom de l'espace de travail",
"workspaceWithoutOwner": "Espace de travail sans propriétaire",
"inviteUsersToWorkspace": "Inviter des utilisateurs à l'espace de travail",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Ajouter des membres à l'organisation",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"signOutUser": "Déconnecter l'utilisateur",
"signOutUsers": "Déconnecter les utilisateurs",
"deactivateUser": "Désactiver l'utilisateur",
"deactivateUsers": "Désactiver les utilisateurs",
"lastActive": "Last Active",
"dateAdded": "Date ajoutée",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"uploadImage": "Charger une image",
"organizationProfile": "Profil de l'organisation",
"organizationImage": "Image de l'organisation",
"organizationName": "Nom de l'organisation",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"disablePublicSharing": "Désactiver le partage public",
"shareSettings": "Partager les paramètres",
"deleteUserAndData": "Supprimer l'utilisateur et ses données",
"userOptions": "Options de l'utilisateur",
"deleteThisOrganization": "Supprimer cette organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"selectYear": "Sélectionnez une Année",
"save": "Enregistrer",
"cancel": "Annuler",
"metadataUrl": "URL des métadonnées",
@ -496,13 +500,13 @@
"adminPanel": "Panneau d'administration",
"moveWorkspaceToOrg": "Déplacer l'espace de travail vers l'organisation",
"ssoSettings": "Paramètres SSO",
"addDomain": "Add Domain",
"domain": "Domain",
"addDomain": "Ajouter le domaine",
"domain": "Domaine",
"settings": "Réglages",
"workspaces": "Espaces de travail",
"back": "Précédent",
"dashboard": "Tableau de bord",
"organizeBy": "Organize by",
"organizeBy": "Organiser en fonction de",
"previous": "Précédent",
"nextMonth": "Mois suivant",
"previousMonth": "Mois précédent",
@ -587,7 +591,7 @@
"untitledToken": "Jeton sans titre",
"tableName": "Nom du tableau",
"dashboardName": "Nom du tableau de bord",
"createView": "Créer une vue",
"createView": "Créer vue",
"creatingView": "Création de la vue",
"duplicateView": "Dupliquer la vue",
"duplicateGridView": "Dupliquer la vue Grille",
@ -717,7 +721,7 @@
"hasMany": "a plusieurs",
"belongsTo": "appartient à",
"manyToMany": "ont des relations nombreuses et variées",
"oneToOne": "have one to one relation",
"oneToOne": "a une relation 1-1",
"extraConnectionParameters": "Paramètres de connexion supplémentaires",
"commentsOnly": "Commentaires uniquement",
"documentation": "Documentation",
@ -738,7 +742,7 @@
"includeView": "Inclure la vue",
"includeWebhook": "Inclure le Webhook",
"zoomInToViewColumns": "Zoom in to view columns",
"embedInSite": "Embed this view in your site",
"embedInSite": "Intégrer cette vue dans votre site",
"titleRequired": "le titre est obligatoire.",
"sourceNameRequired": "Le nom de la source est obligatoire",
"changeWsName": "Changer le nom de l'espace de travail",
@ -750,7 +754,7 @@
"saveChanges": "Enregistrer les modifications",
"updatedField": "Champ mis à jour",
"deletedField": "Champ supprimé",
"incompleteConfiguration": "Incomplete configuration",
"incompleteConfiguration": "Configuration incomplète",
"selectField": "Sélectionner un champ",
"selectFieldLabel": "Begin by selecting a field to customise its properties and structure."
},
@ -758,8 +762,8 @@
"backgroundColor": "Couleur d'arrière-plan",
"hideNocodbBranding": "Masquer la marque NocoDB",
"showOnConditions": "Show on conditions",
"showFieldOnConditionsMet": "Shows field only when conditions are met",
"limitOptions": "Limit options",
"showFieldOnConditionsMet": "Montre le champ uniquement lorsque les conditions sont réunies",
"limitOptions": "Limiter les options",
"limitOptionsSubtext": "Limit options visible to users by selecting available options",
"clearSelection": "Effacer la sélection"
},
@ -771,17 +775,17 @@
"newWorkspace": "Nouvel espace de travail",
"addDomain": "Add Domain",
"addMembers": "Ajouter des membres",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"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",
"enterEmail": "Entrez les adresses e-mail",
"inviteToBase": "Inviter à la base",
"inviteToWorkspace": "Inviter à l'espace de travail",
"addMember": "Ajouter un membre à la Base",
"noRange": "La vue calendrier nécessite une plage de dates",
"goToToday": "Aller à aujourd'hui",
"toggleSidebar": "Activer/désactiver la barre latérale",
"addEndDate": "Ajouter une date de fin",
"withEndDate": "avec la date de fin",
"calendar": "Calendrier",
"viewSettings": "View settings",
"viewSettings": "Afficher les paramètres",
"googleOAuth": "Google OAuth",
"registerOIDC": "Register OIDC Identity Provider",
"registerSAML": "Register SAML Identity Provider",
@ -789,27 +793,27 @@
"copyIFrameCode": "Copier le code IFrame",
"onCondition": "On Condition",
"bulkDownload": "Bulk Download",
"attachFile": "Attach File",
"viewAttachment": "View Attachments",
"attachFile": "Joindre un fichier",
"viewAttachment": "Voir les pièces jointes",
"attachmentDrop": "Cliquez ou déposez un fichier dans la cellule",
"addFiles": "Ajouter un ou des fichier(s)",
"hideInUI": "Masquer dans l'interface",
"addBase": "Add Base",
"addBase": "Ajouter une base",
"addParameter": "Ajouter un paramètre",
"submitAnotherForm": "Soumettre un autre formulaire",
"dragAndDropFieldsHereToAdd": "Drag and drop fields here to add",
"editSource": "Edit Data Source",
"enterText": "Enter text",
"dragAndDropFieldsHereToAdd": "Glissez et déposez les champs ici pour ajouter",
"editSource": "Modifier la source de données",
"enterText": "Saisir texte",
"okEditBase": "Ok & Edit Base",
"showInUI": "Afficher dans l'interface utilisateur",
"outOfSync": "Out of sync",
"newSource": "Nouvelle source de données",
"newWebhook": "New Webhook",
"newWebhook": "Nouveau Webhook",
"enablePublicAccess": "Activer l'accès public",
"doYouWantToSaveTheChanges": "Voulez-vous enregistrer les modifications ?",
"editingAccess": "Accès en modification",
"enabledPublicViewing": "Activer l'affichage anonyme",
"restrictAccessWithPassword": "Restrict access with password",
"restrictAccessWithPassword": "Restreindre l'accès avec un mot de passe",
"manageProjectAccess": "Gérer l'accès à la base",
"allowDownload": "Autoriser le téléchargement",
"surveyMode": "Mode Sondage",
@ -887,7 +891,7 @@
"previousRecord": "Ligne précédente",
"copyApiURL": "Copier l'URL de l'API",
"createTable": "Créer une nouvelle table",
"createDashboard": "Create Dashboard",
"createDashboard": "Créer un tableau de bord",
"createWorkspace": "Créer un espace de travail",
"refreshTable": "Actualiser le tableau",
"renameTable": "Renommer la table",
@ -895,7 +899,7 @@
"deleteTable": "Supprimer la table",
"addField": "Ajouter un nouveau champ à ce tableau",
"setDisplay": "Définir comme valeur d'affichage",
"addRow": "Ajouter une nouvelle ligne",
"addRow": "Ajouter un nouvel enregistrement",
"saveRow": "Enregistrer",
"saveAndExit": "Enregistrer et quitter",
"saveAndStay": "Enregistrer et rester",
@ -917,7 +921,7 @@
"clearMetadata": "Effacer les métadonnées",
"exportToFile": "Exporter vers le fichier",
"changePwd": "Changer le mot de passe",
"createView": "Créer une vue",
"createView": "Créer vue",
"shareView": "Partager la vue",
"findRowByCodeScan": "Find row by scan",
"fillByCodeScan": "Remplir avec un scanner",
@ -972,10 +976,10 @@
"markAllAsRead": "Tout marquer comme lu",
"column": {
"delete": "Supprimer le champ",
"addNumber": "Add Number Field",
"addSingleLineText": "Add SingleLineText Field",
"addLongText": "Add LongText Field",
"addOther": "Add Other Field"
"addNumber": "Ajouter un champ de type numérique",
"addSingleLineText": "Ajouter un champ de type texte sur une ligne",
"addLongText": "Ajouter un champ de type texte long",
"addOther": "Ajouter un autre champ"
},
"erd": {
"showColumns": "Afficher les colonnes",
@ -999,17 +1003,17 @@
},
"toggleMobileMode": "(Dés-)activer le mode Mobile",
"startCommenting": "Commencez à commenter !",
"clearForm": "Clear Form",
"addFieldFromFormView": "Add Field",
"selectAllFields": "Select all fields",
"clearForm": "Vider le formulaire",
"addFieldFromFormView": "Ajouter un champ",
"selectAllFields": "Sélectionner tous les champs",
"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": "Autoriser le pré-remplissage",
"default": "Valeur par défaut",
"locked": "Mettre les champs préremplis en lecture seule",
"hidden": "Masquer les champs préremplis",
"lockedFieldTooltip": "Valeur préremplie"
},
"getPreFilledLink": "Get Pre-filled Link",
"getPreFilledLink": "Obtenir un lien prérempli",
"group": "Grouper par"
},
"tooltip": {
@ -1024,7 +1028,7 @@
"light": "Jour (^⇧B)"
},
"addTable": "Ajouter un nouveau tableau",
"addDashboard": "Add new Dashboard",
"addDashboard": "Ajouter un nouveau tableau de bord",
"inviteMore": "Inviter plus d'utilisateurs",
"toggleNavDraw": "Afficher ou masquer le panneau de navigation",
"reloadApiToken": "Recharger les jetons API",
@ -1041,9 +1045,9 @@
"clientKey": "Selectionner un fichier .key",
"clientCert": "Selectionner un fichier .cert",
"clientCA": "Selectionner un fichier d'authentification",
"changeIconColour": "Change icon colour",
"changeIconColour": "Changer la couleur de l'icône",
"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": "Mode formulaire avec un champ par page (enquête)"
},
"placeholder": {
"selectSlackChannels": "Sélectionnez les canaux Slack",
@ -1052,14 +1056,14 @@
"selectMattermostChannels": "Sélectionnez les canaux Mattermost",
"webhookTitle": "Titre du Webhook",
"barcodeColumn": "Sélectionnez un champ pour la valeur du code-barres",
"notFoundContent": "No valid field Type can be found.",
"selectBarcodeFormat": "Select a Barcode format",
"notFoundContent": "Aucun type de champ valide ne peut être trouvé.",
"selectBarcodeFormat": "Sélectionnez un format de code-barres",
"projName": "Saisir le nom du projet",
"selectGroupField": "Choisir un champ de regroupement",
"selectGroupFieldNotFound": "No Single Select Field can be found. Please create one first.",
"selectGeoField": "Select a GeoData Field",
"notSelected": "-not selected-",
"selectGeoFieldNotFound": "No GeoData Field can be found. Please create one first.",
"selectGroupFieldNotFound": "Aucun champ de sélection en liste ne peut être trouvé. Veuillez en créer un d'abord.",
"selectGeoField": "Sélectionnez un champ de type GeoData",
"notSelected": "-non sélectionné-",
"selectGeoFieldNotFound": "Aucun champ de type GeoData n'a été trouvé. Veuillez en créer un d'abord.",
"password": {
"enter": "Saisir le mot de passe",
"current": "Mot de passe actuel",
@ -1067,8 +1071,8 @@
"save": "Enregistrer le mot de passe",
"confirm": "Confirmer le nouveau mot de passe"
},
"selectAColumnForTheQRCodeValue": "Select a field for the QR code value",
"allowNegativeNumbers": "Allow negative numbers",
"selectAColumnForTheQRCodeValue": "Sélectionnez un champ contenant la valeur du QR code",
"allowNegativeNumbers": "Autoriser les nombres négatifs",
"searchProjectTree": "Chercher un tableau",
"searchFields": "Champ de recherche",
"searchColumn": "Recherche {recherche} colonne",
@ -1090,20 +1094,21 @@
"decimal8": "1,00000000",
"value": "Valeur",
"key": "Clé",
"createTable": "Create your First Table!",
"createTable": "Créez votre première table !",
"createTableLabel": "Create your first table effortlessly, from scratch, or by importing/connecting to an external database.",
"noTokenCreated": "No API Tokens created",
"noTokenCreated": "Aucun jeton d'API créé",
"noTokenCreatedLabel": "Begin by creating API tokens to unlock advanced functionalities.",
"inviteYourTeam": "Invite your team",
"inviteYourTeam": "Invitez votre équipe",
"inviteYourTeamLabel": "Streamline collaboration and productivity with your team – start by inviting them to join your workspace.",
"searchOptions": "Search options"
"searchOptions": "Options de recherche"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Contrôlez le nom et l'apparence de vos organisations.",
"addCompanyDomains": "Ajoutez des domaines pour restreindre l'accès aux utilisateurs non souhaités.",
"restrictUsersFromSharing": "Empêcher les utilisateurs de partager leurs bases publiquement.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"selectUsersToBeRemoved": "Sélectionnez les utilisateurs à supprimer et à supprimer de tous les espaces de travail de l'organisation.",
"deleteOrganization": "Supprimer tous les utilisateurs, bases et données liées à cette organisation",
"clickToCopyFieldId": "Cliquer pour copier l'ID du champ",
"enterPassword": "Saisir le mot de passe",
"bySigningUp": "En vous inscrivant, vous acceptez les",
@ -1112,79 +1117,79 @@
"thisSharedViewIsProtected": "Cette vue partagée est protégée",
"successfullySubmittedFormData": "Données du formulaire soumises avec succès",
"formViewNotSupportedOnMobile": "L'affichage des formulaires n'est pas pris en charge sur les téléphones portables",
"newFormWillBeLoaded": "New form will be loaded after {seconds} seconds",
"newFormWillBeLoaded": "Un nouveau formulaire sera chargé après {seconds} secondes",
"optimizedQueryDisabled": "Optimized query is disabled",
"optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"recordCouldNotBeFound": "Record could not be found",
"linkColumnClearNotSupportedYet": "Vous n'avez aucun lien défini pour ajouter un champ Lookup",
"recordCouldNotBeFound": "L'enregistrement n'a pas pu être trouvé",
"invalidPhoneNumber": "Numéro de téléphone invalide",
"pageSizeChanged": "Page size changed",
"pageSizeChanged": "Taille de la page modifiée",
"errorLoadingData": "Erreur de chargement des données",
"webhookBodyMsg1": "Use context variable",
"webhookBodyMsg2": "body",
"webhookBodyMsg1": "Utiliser une variable de contexte",
"webhookBodyMsg2": "corps du message",
"webhookBodyMsg3": "to refer the record under consideration",
"formula": {
"hintStart": "Hint: Use {placeholder1} to reference fields, e.g: {placeholder2}. For more, please check out",
"hintEnd": "Formulas.",
"noSuggestedFormulaFound": "No suggested formula found",
"hintStart": "Astuce : Utilisez {placeholder1} pour référencer des champs, par exemple: {placeholder2}. Pour en savoir plus, veuillez vérifier",
"hintEnd": "Expressions.",
"noSuggestedFormulaFound": "Aucune formule suggérée trouvée",
"typeIsExpected": "{calleeName} requires a {type} at position {position}",
"numericTypeIsExpected": "Numeric type is expected",
"stringTypeIsExpected": "String type is expected",
"operationNotAvailable": "{operation} operation not available",
"numericTypeIsExpected": "Un type numérique est attendu",
"stringTypeIsExpected": "Un type texte est attendu",
"operationNotAvailable": "Opération {operation} non disponible",
"cantSaveFieldFormulaInvalid": "Impossible d'enregistrer le champ car la formule n'est pas valide",
"notSupportedToReferenceColumn": "Not supported to reference field {columnName}",
"typeIsExpectedButFound": "Le type {type} est attendu, mais le type {found} est trouvé",
"requiredArgumentsFormula": "{calleeName} requires {requiredArguments} arguments",
"requiredArgumentsFormula": "{calleeName} nécessite les arguments {requiredArguments}",
"minRequiredArgumentsFormula": "{calleeName} required minimum {minRequiredArguments} arguments",
"maxRequiredArgumentsFormula": "{calleeName} required maximum {maxRequiredArguments} arguments",
"functionNotAvailable": "La fonction {function} n'est pas disponible",
"firstParamWeekDayHaveDate": "The first parameter of WEEKDAY() should have date value",
"secondParamWeekDayHaveDate": "The second parameter of WEEKDAY() should have the value either \"sunday\", \"monday\", \"tuesday\", \"wednesday\", \"thursday\", \"friday\" or \"saturday\"",
"firstParamDateAddHaveDate": "The first parameter of DATEADD() should have date value",
"secondParamDateAddHaveNumber": "The second parameter of DATEADD() should have numeric value",
"thirdParamDateAddHaveDate": "The third parameter of DATEADD() should have the value either \"day\", \"week\", \"month\" or \"year\"",
"firstParamDateDiffHaveDate": "The first parameter of DATEDIFF() should have date value",
"secondParamDateDiffHaveDate": "The second parameter of DATEDIFF() should have date value",
"thirdParamDateDiffHaveDate": "The third parameter of DATETIME_DIFF() should have value either \"milliseconds\", \"ms\", \"seconds\", \"s\", \"minutes\", \"m\", \"hours\", \"h\", \"days\", \"d\", \"weeks\", \"w\", \"months\", \"M\", \"quarters\", \"Q\", \"years\", or \"y\"",
"columnNotAvailable": "Field {columnName} is not available",
"cantSaveCircularReference": "Can’t save field because it causes a circular reference",
"columnWithTypeFoundButExpected": "Field {columnName} with {columnType} type is found but {expectedType} type is expected",
"columnNotMatchedWithType": "{columnName} is not matched with {columnType}"
"firstParamWeekDayHaveDate": "Le premier paramètre de la fonction WEEKDAY() doit avoir une valeur de type date",
"secondParamWeekDayHaveDate": "Le second paramètre de WEEKDAY() doit avoir la valeur \"dimanche\", \"lundi\", \"mardi\", \"mercredi\", \"jeudi\", \"vendredi\" ou \"samedi\"",
"firstParamDateAddHaveDate": "Le premier paramètre de la fonction WEEKDAY() doit avoir une valeur de type date",
"secondParamDateAddHaveNumber": "Le deuxième paramètre de la fonction DATEADD() doit avoir une valeur numérique",
"thirdParamDateAddHaveDate": "Le troisième paramètre de DATEADD() doit avoir la valeur \"day\", \"week\", \"month\" ou \"year\"",
"firstParamDateDiffHaveDate": "Le premier paramètre de la fonction DATEDIFF() doit avoir une valeur de type date",
"secondParamDateDiffHaveDate": "Le second paramètre de la fonction DATEDIFF() doit avoir une valeur de type date",
"thirdParamDateDiffHaveDate": "Le troisième paramètre de la fonction DATETIME_DIFF() doit avoir la valeur \"millisecondes\", \"ms\", \"secondes\", \"s\", \"s\", \"s\", \"minutes\", \"m\", \"hours\", \"h\", \"days\", \"d\", \"weeks\", \"w\", \"months\", \"M\", \"quarters\", \"Q\", \"years\", ou \"y\"",
"columnNotAvailable": "Le champ {columnName} n'est pas disponible",
"cantSaveCircularReference": "Impossible d'enregistrer le champ car il provoque une référence circulaire",
"columnWithTypeFoundButExpected": "Le champ {columnName} avec le type {columnType} est trouvé mais le type {expectedType} est attendu",
"columnNotMatchedWithType": "{columnName} ne correspond pas au type {columnType}"
},
"selectOption": {
"cantBeNull": "Select options can't be null",
"multiSelectCantHaveCommas": "MultiSelect fields can't have commas(',')",
"cantHaveDuplicates": "Select options can't have duplicates",
"createNewOptionNamed": "Create new option named"
"createNewOptionNamed": "Créer une nouvelle option nommée"
},
"plsEnterANumber": "Veuillez saisir un nombre",
"plsInputEmail": "Veuillez saisir un e-mail",
"invalidDate": "Invalid date",
"invalidLocale": "Invalid locale",
"invalidDate": "Date non valide",
"invalidLocale": "Langue invalide",
"invalidCurrencyCode": "Code de devise invalide",
"postgresHasItsOwnCurrencySettings": "Le type 'money' de PostgreSQL a ses propres paramètres de devise",
"validColumnsForBarCode": "The valid Field Types for a Barcode Field are: Number, Single Line Text, Long Text, Phone Number, URL, Email, Decimal. Please create one first.",
"validColumnsForBarCode": "Les types de champs valides pour un champ de code-barres sont : Numéro, Texte à une seule ligne, Texte Long, Numéro de Téléphone, URL, Email, Décimal. Veuillez en créer un d'abord.",
"hm": {
"title": "Has Many Relation",
"tooltip_desc": "A single record from table ",
"title": "Relation 1-n",
"tooltip_desc": "Un seul enregistrement de la table ",
"tooltip_desc2": " peut être lié avec plusieurs enregistrements de la table "
},
"mm": {
"title": "Many to Many Relation",
"tooltip_desc": "Multiple records from table ",
"tooltip_desc2": " can be linked with multiple records from table "
"title": "Relation n-n",
"tooltip_desc": "Plusieurs enregistrements de la table ",
"tooltip_desc2": " peut être lié avec plusieurs enregistrements de la table "
},
"bt": {
"title": "Belongs to Relation",
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a record from table "
"title": "Relation appartient à",
"tooltip_desc": "Un seul enregistrement de la table ",
"tooltip_desc2": " peut être lié avec un enregistrement de la table "
},
"oo": {
"title": "One to One Relation",
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
"tooltip_desc2": " peut être lié à un seul enregistrement de la table "
},
"clickLinkRecordsToAddLinkFromTable": "Il semble qu'aucun enregistrement n'ait encore été lié.",
"noRecordsLinked": "Aucun enregistrement lié",
@ -1202,17 +1207,17 @@
"idColumnRequired": "ID field is required, you can rename this later if required.",
"length59Required": "The length exceeds the max 59 characters",
"noNewNotifications": "You have no new notifications",
"noRecordFound": "Record not found",
"noRecordFound": "Enregistrement non trouvé",
"noRecordsFound": "Aucun enregistrement trouvé",
"noRecordsMatchYourSearchQuery": "Aucun enregistrement ne correspond à votre recherche",
"rowDeleted": "Record deleted",
"saveChanges": "Do you want to save the changes?",
"tooLargeFieldEntity": "The field is too large to be converted to {entity}",
"rowDeleted": "Enregistrement supprimé",
"saveChanges": "Voulez-vous enregistrer les modifications ?",
"tooLargeFieldEntity": "Le champ est trop grand pour être converti en {entity}",
"roleRequired": "Role required",
"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!",
"mapNoFields": "La vue cartographique nécessite un champ de données géographiques pour être configuré. Essayez de configurer une vue cartographique après avoir ajouté un champ de données géographiques !",
"dbValid": "Veuillez vous assurer que la base de données à laquelle vous essayez de vous connecter est valide ! Cette opération peut provoquer une perte de schéma !!",
"barcode": {
"renderError": "Erreur de code-barres - veuillez vérifier la compatibiltié entre la donnée d'entrée et le type de code-barres"
@ -1220,24 +1225,24 @@
"nonEditableFields": {
"computedFieldUnableToClear": "Avertissement : Champ calculé - impossible d'effacer le texte",
"qrFieldsCannotBeDirectlyChanged": "Attention : les champs QR code ne peuvent pas être modifiés directement.",
"barcodeFieldsCannotBeDirectlyChanged": "Warning: Barcode fields cannot be directly changed."
"barcodeFieldsCannotBeDirectlyChanged": "Attention : les champs code-barres ne peuvent pas être modifiés directement."
},
"duplicateProject": "Are you sure you want to duplicate the base?",
"duplicateProject": "Êtes-vous sûr de vouloir dupliquer la base ?",
"duplicateTable": "Êtes-vous sûr de vouloir dupliquer la table ?",
"multiField": {
"fieldVisibility": "You cannot change visibility of a field that is being edited. Please save or discard changes first.",
"moveEditedField": "You cannot move field that is being edited. Either save or discard changes first",
"moveDeletedField": "You cannot move field that is deleted. Either save or discard changes first"
"fieldVisibility": "Vous ne pouvez pas modifier la visibilité d'un champ en cours de modification. Veuillez d'abord enregistrer ou annuler les modifications.",
"moveEditedField": "Vous ne pouvez pas déplacer un champ en cours d'édition. Enregistrez ou annulez d'abord les modifications",
"moveDeletedField": "Vous ne pouvez pas déplacer un champ supprimé. Enregistrez ou annulez d'abord les modifications"
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"enterWorkspaceName": "Entrez le nom de l'espace de travail",
"enterBaseName": "Entrez le nom de base",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"noSaml": "Il n'y a aucune authentification SAML configurée.",
"noOIDC": "Il n'y a aucune authentification OpenID configurée.",
"disabledAsViewLocked": "Désactivé car la vue est verrouillée",
"basesMigrated": "Bases are migrated. Please try again.",
"basesMigrated": "Les bases sont migrées. Veuillez réessayer.",
"pasteNotSupported": "L'opération de collage n'est pas prise en charge sur la cellule active",
"roles": {
"orgCreator": "Le créateur peut créer de nouveaux projets et accéder à tout projet invité.",
@ -1277,7 +1282,7 @@
"formInput": "Entrer le libellé du formulaire",
"formHelpText": "Ajouter un texte d'aide",
"onlyCreator": "Visible uniquement pour les créateurs",
"formTitle": "Add form Title",
"formTitle": "Ajouter un titre au formulaire",
"formDesc": "Ajouter une description au formulaire",
"beforeEnablePwd": "Restreindre l’accès à l’aide d’un mot de passe",
"afterEnablePwd": "L’accès est restreint par un mot de passe",
@ -1290,7 +1295,7 @@
"emailForm": "Écrivez-moi à",
"showSysFields": "Afficher les champs système",
"filterAutoApply": "Appliquer automatiquement",
"formDisplayMessage": "Display Message",
"formDisplayMessage": "Afficher le message",
"viewNotShared": "La vue actuelle n'est pas partagée!",
"showAllViews": "Montrer toutes les vues partagées sur cette table",
"collabView": "Les collaborateurs avec des autorisations d'édition ou plus peuvent modifier la configuration de la vue.",
@ -1344,8 +1349,8 @@
"addMultipleUsers": "Vous pouvez ajouter plusieurs courriels séparés par des virgules (,)",
"enterTableName": "Entrez le nom du tableau",
"enterLayoutName": "Enter Layout name",
"enterDashboardName": "Enter Dashboard name",
"defaultColumns": "Default fields",
"enterDashboardName": "Entrez le nom du tableau de bord",
"defaultColumns": "Champs par défaut",
"addDefaultColumns": "Ajouter des colonnes par défaut",
"tableNameInDb": "Nom de la table tel qu'enregistré dans la base de données",
"airtable": {
@ -1385,8 +1390,8 @@
"noMoreRecords": "Plus d'enregistrements",
"tokenNameNotEmpty": "Token name should not be empty",
"tokenNameMaxLength": "Token name should not be more than 255 characters",
"dbNameRequired": "Database name is required",
"wsNameRequired": "Workspace name required",
"dbNameRequired": "Le nom de la base de données est requis",
"wsNameRequired": "Nom de l'espace de travail requis",
"wsNameMinLength": "Le nom de l'espace de travail doit comporter au moins 3 caractères",
"wsNameMaxLength": "Le nom de l'espace de travail doit comporter au plus 50 caractères",
"wsDeleteDlg": "Supprimez cet espace de travail et tout son contenu.",
@ -1398,25 +1403,25 @@
"thankYou": "Merci !",
"submittedFormData": "Vous avez soumis les données du formulaire avec succès.",
"editingSystemKeyNotSupported": "Editing system key not supported",
"notAvailableAtTheMoment": "Not available at the moment",
"notAvailableAtTheMoment": "Non disponible pour le moment",
"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}",
"yourCurrentRoleIs": "Votre rôle actuel est",
"pleaseRequestAccessForView": "Veuillez demander des droits plus élevés au propriétaire de l’Admin / Base / Espace de travail pour avoir accès à cette {viewName}",
"preventHideAllOptions": "You cannot hide all options if field is required"
},
"error": {
"fetchingCalendarData": "Erreur lors de la récupération des données du calendrier",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"domainRequired": "Le nom de domaine est requis",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
"clientIdRequired": "L'ID du client est requis",
"issuerRequired": "Issuer is required",
"clientSecretRequired": "Client Secret is required",
"clientSecretRequired": "Le client secret est requis",
"jwkUrlRequired": "JWK URL is required",
"tokenUrlRequired": "Token URL is required",
"userInfoUrlRequired": "UserInfo URL is required",
@ -1449,8 +1454,8 @@
"atLeastOneNumber": "Un chiffre",
"atLeastOneSpecialChar": "Un caractère spécial",
"allowedSpecialCharList": "Liste des caractères spéciaux autorisés",
"invalidEmails": "Invalid emails",
"invalidEmail": "Invalid Email"
"invalidEmails": "E-mails non valides",
"invalidEmail": "Email non valide"
},
"invalidXml": "Invalid XML",
"invalidURL": "URL invalide",
@ -1483,7 +1488,7 @@
"followingCharactersAreNotAllowed": "Les caractères suivants ne sont pas autorisés",
"columnNameRequired": "Nom de la colonne requis",
"duplicateColumnName": "Duplicate field name",
"duplicateSystemColumnName": "Name already used for system field",
"duplicateSystemColumnName": "Nom déjà utilisé par un champ système",
"uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Le nom du projet dépasse les 50 caractères",
@ -1495,15 +1500,15 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Les types de fichiers acceptés sont .xls, .xlsx, .xlsm, .ods, .ots.",
"parameterKeyCannotBeEmpty": "La clé de paramètre ne peut pas être vide",
"duplicateParameterKeysAreNotAllowed": "Les doublons de clés de paramètres ne sont pas autorisés",
"fieldRequired": "{value} ne peut pas être vide.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Projet non accessible",
"copyToClipboardError": "Échec de la copie dans le presse-papiers",
"pasteFromClipboardError": "Failed to paste from clipboard",
"pasteFromClipboardError": "Échec du collage à partir du presse-papiers",
"multiFieldSaveValidation": "Please complete the configuration of all fields before saving",
"somethingWentWrong": "Something went wrong",
"somethingWentWrong": "Quelque chose n'a pas fonctionné",
"draggedContentIsNotTypeOfImage": "Dragged content is not type of image",
"fieldToParseImageData": "Field to parse image data",
"someOfTheRequiredFieldsAreEmpty": "Some of the required fields are empty"
"someOfTheRequiredFieldsAreEmpty": "Certains des champs requis sont vides"
},
"toast": {
"exportMetadata": "Les métadonnées de projet sont exportées avec succès",
@ -1523,9 +1528,9 @@
"futureRelease": "Bientôt disponible !"
},
"success": {
"licenseKeyUpdated": "License Key Updated",
"licenseKeyUpdated": "Clé de licence mise à jour",
"columnDuplicated": "Colonne dupliquée avec succès",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)",
"rowDuplicatedWithoutSavedYet": "Enregistrement en double (non enregistré)",
"updatedUIACL": "Mise à jour réussie de l'ACL de l'interface utilisateur pour les tables",
"pluginUninstalled": "Le plugin a été désinstallé avec succès",
"pluginSettingsSaved": "Les paramètres du plugin ont été enregistrés avec succès",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "שם שולחן",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Project not accessible",
"copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Table name",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Project not accessible",
"copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Tablica",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Dozvoljeni tipovi datoteka su .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Ključ parametra ne može biti prazan",
"duplicateParameterKeysAreNotAllowed": "Duplicirani ključevi parametara nisu dopušteni",
"fieldRequired": "{value} ne može biti prazno.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Project not accessible",
"copyToClipboardError": "Kopiranje u međuspremnik nije uspjelo",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Nama meja",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Jenis file yang diterima adalah .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Kunci parameter tidak boleh kosong",
"duplicateParameterKeysAreNotAllowed": "Kunci parameter duplikat tidak diperbolehkan",
"fieldRequired": "{value} tidak boleh kosong.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Proyek tidak dapat diakses",
"copyToClipboardError": "Gagal menyalin ke papan klip",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Token senza titolo",
"tableName": "Nome della tabella",
"dashboardName": "Nome dashboard",
"createView": "Crea una vista",
"createView": "Crea vista",
"creatingView": "Creazione vista",
"duplicateView": "Duplica vista",
"duplicateGridView": "Duplica Visualizzazione Griglia",
@ -917,7 +921,7 @@
"clearMetadata": "Cancella metadati",
"exportToFile": "Esporta su file",
"changePwd": "Cambia password",
"createView": "Crea una vista",
"createView": "Crea vista",
"shareView": "Condividi vista",
"findRowByCodeScan": "Find row by scan",
"fillByCodeScan": "Riempi per scansione",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "I tipi di file accettati sono .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "La chiave del parametro non può essere vuota",
"duplicateParameterKeysAreNotAllowed": "Le chiavi dei parametri duplicate non sono consentite",
"fieldRequired": "{value} non può essere vuoto.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Progetto non accessibile",
"copyToClipboardError": "Non è riuscito a copiare negli appunti",
"pasteFromClipboardError": "Impossibile incollare dagli appunti",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "テーブルの名前",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "使用可能なファイル形式は、.xls、.xlsx、.xlsm、.ods、.ots です。",
"parameterKeyCannotBeEmpty": "パラメータキーは空にできません",
"duplicateParameterKeysAreNotAllowed": "パラメータキーの重複は許可されていません",
"fieldRequired": "{value} を空にすることはできません。",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "このプロジェクトにはアクセスできません",
"copyToClipboardError": "クリップボードへのコピーに失敗しました",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "허용되는 파일 형식은 .xls, .xlsx, .xlsm, .ods, .ots입니다",
"parameterKeyCannotBeEmpty": "매개 변수 키는 비워 둘 수 없습니다.",
"duplicateParameterKeysAreNotAllowed": "중복 매개 변수 키는 허용되지 않습니다.",
"fieldRequired": "{value}은(는) 비워 둘 수 없습니다.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "프로젝트에 액세스할 수 없습니다.",
"copyToClipboardError": "클립 보드에 복사할 수 없습니다.",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Tabulas nosaukums",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Pieņemtie failu tipi ir .xls, .xlsx, .xlsm, .ods, .ots.",
"parameterKeyCannotBeEmpty": "Parametra atslēga nedrīkst būt tukša",
"duplicateParameterKeysAreNotAllowed": "Parametru taustiņu dublēšanās nav atļauta",
"fieldRequired": "{value} nevar būt tukšs.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Projekts nav pieejams",
"copyToClipboardError": "Neizdevās kopēt uz starpliktuvi",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Tabelnaam",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "De geaccepteerde bestandstypen zijn .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parametersleutel kan niet leeg zijn",
"duplicateParameterKeysAreNotAllowed": "Dubbele parametersleutels zijn niet toegestaan",
"fieldRequired": "{value} kan niet leeg zijn.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Project niet toegankelijk",
"copyToClipboardError": "Kopiëren naar klembord mislukt",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Tabellnavn",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Project not accessible",
"copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Weryfikuj",
"apply": "Zastosuj",
"text": "Tekst",
"appearance": "Wygląd"
"appearance": "Wygląd",
"now": "Now"
},
"objects": {
"owner": "Właściciel",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Dziś",
"workspace": "Obszar roboczy",
"txt": "Wartość rekordu TXT",
@ -1099,6 +1103,7 @@
"searchOptions": "Opcje wyszukiwania"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Kontroluj nazwę i wygląd organizacji.",
"addCompanyDomains": "Dodaj domeny firmy, aby ograniczyć dostęp od niechcianych użytkowników.",
"restrictUsersFromSharing": "Ogranicz użytkowników z możliwości publicznego udostępniania baz danych.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Akceptowane typy plików to: .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Klucz parametru nie może być pusty",
"duplicateParameterKeysAreNotAllowed": "Zduplikowane klucze parametrów są niedozwolone",
"fieldRequired": "{value} nie może być puste.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Projekt niedostępny",
"copyToClipboardError": "Nie udało się skopiować do schowka",
"pasteFromClipboardError": "Nie udało się wkleić ze schowka",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Aplicar",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Token sem título",
"tableName": "Nome da tabela",
"dashboardName": "Nome do painel",
"createView": "Criar uma vista",
"createView": "Criar Vista",
"creatingView": "Criando visualização",
"duplicateView": "Vista duplicada",
"duplicateGridView": "Vista duplicada em grelha",
@ -917,7 +921,7 @@
"clearMetadata": "Limpar Metadados",
"exportToFile": "Exportar para ficheiro",
"changePwd": "Alterar a senha",
"createView": "Criar uma vista",
"createView": "Criar Vista",
"shareView": "Partilhar Vista",
"findRowByCodeScan": "Find row by scan",
"fillByCodeScan": "Fill by scan",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Os tipos de ficheiro aceites são .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "A chave do parâmetro não pode estar vazia",
"duplicateParameterKeysAreNotAllowed": "Não são permitidas chaves de parâmetros duplicadas",
"fieldRequired": "{value} não pode estar vazio.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Projecto não acessível",
"copyToClipboardError": "Falha na cópia para a prancheta",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Nome da tabela",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Os tipos de ficheiro aceites são .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "A chave do parâmetro não pode estar vazia",
"duplicateParameterKeysAreNotAllowed": "Não são permitidas chaves de parâmetros duplicadas",
"fieldRequired": "{value} não pode estar vazio.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Projecto não acessível",
"copyToClipboardError": "Falha na cópia para a prancheta",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Применить",
"text": "Текст",
"appearance": "Внешний вид"
"appearance": "Внешний вид",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Название таблицы",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Допустимые типы файлов: .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Ключ параметра не может быть пустым",
"duplicateParameterKeysAreNotAllowed": "Дублирование ключей параметров не допускается",
"fieldRequired": "{value} не может быть пустым.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Проект недоступен",
"copyToClipboardError": "Не удалось скопировать в буфер обмена",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Názov tabuľky",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Akceptované typy súborov sú .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Kľúč parametra nemôže byť prázdny",
"duplicateParameterKeysAreNotAllowed": "Duplicitné kľúče parametrov nie sú povolené",
"fieldRequired": "{value} nemôže byť prázdny.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Projekt nie je prístupný",
"copyToClipboardError": "Nepodarilo sa skopírovať do schránky",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Ime tabele",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Sprejete vrste datotek so .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Ključ parametra ne more biti prazen",
"duplicateParameterKeysAreNotAllowed": "Podvojeni ključi parametrov niso dovoljeni",
"fieldRequired": "{value} ne more biti prazen.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Projekt ni dostopen",
"copyToClipboardError": "Neuspešno kopiranje v odložišče",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Tabellnamn",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "De accepterade filtyperna är .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameternyckeln kan inte vara tom",
"duplicateParameterKeysAreNotAllowed": "Dubbla parameternycklar är inte tillåtna",
"fieldRequired": "{value} får inte vara tom.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Projektet är inte tillgängligt",
"copyToClipboardError": "Kopiering till urklipp misslyckades",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "ชอตาราง",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Project not accessible",
"copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Uygula",
"text": "Metin",
"appearance": "Görünüm"
"appearance": "Görünüm",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Kabul edilen dosya türleri .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parametre anahtarı boş olamaz",
"duplicateParameterKeysAreNotAllowed": "Yinelenen parametre anahtarlarına izin verilmez",
"fieldRequired": "{value} boş olamaz.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Proje erişilebilir değil",
"copyToClipboardError": "Panoya kopyalanamadı",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Назва таблиці",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "Дозволені типи файлів: .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Ключ параметра не може бути порожнім",
"duplicateParameterKeysAreNotAllowed": "Дублювання ключів параметрів неприпустимо",
"fieldRequired": "{value} не може бути порожнім.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Проєкт недоступний",
"copyToClipboardError": "Не вдалося скопіювати в буфер обміну",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "Tên bảng.",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "The accepted file types are .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "Parameter key cannot be empty",
"duplicateParameterKeysAreNotAllowed": "Duplicate parameter keys are not allowed",
"fieldRequired": "{value} cannot be empty.",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Project not accessible",
"copyToClipboardError": "Failed to copy to clipboard",
"pasteFromClipboardError": "Failed to paste from clipboard",

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

@ -162,7 +162,7 @@
"insertAbove": "将上方内容插入",
"insertBelow": "将下方内容插入",
"hideField": "隐藏字段",
"showField": "Show Field",
"showField": "显示字段",
"sortAsc": "升序",
"sortDesc": "降序",
"move": "移动",
@ -204,7 +204,8 @@
"verify": "验证",
"apply": "应用",
"text": "文本",
"appearance": "外观"
"appearance": "外观",
"now": "Now"
},
"objects": {
"owner": "所有者",
@ -448,16 +449,19 @@
"noResultsMatchedYourSearch": "您的搜索没有任何匹配结果"
},
"labels": {
"today": "Today",
"workspace": "Workspace",
"connectionDetails": "链接明细",
"metaSync": "元同步",
"mention": "Mention",
"today": "今天",
"workspace": "工作区",
"txt": "TXT 记录值",
"transferOwnership": "转让所有权",
"recentActivity": "最近动态",
"goToMembers": "转到成员",
"addMember": "添加成员",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"numberOfMembers": "工作区序号",
"numberOfBases": "项目序号",
"numberOfRecords": "记录序号",
"workspaceName": "工作区名称",
"workspaceWithoutOwner": "工作区(无所有者)",
"inviteUsersToWorkspace": "邀请用户访问工作区",
@ -469,7 +473,7 @@
"signOutUsers": "注销用户",
"deactivateUser": "停用用户",
"deactivateUsers": "停用用户",
"lastActive": "Last Active",
"lastActive": "最近活动",
"dateAdded": "日期已添加",
"uploadImage": "上传图片",
"organizationProfile": "组织简介",
@ -482,7 +486,7 @@
"deleteUserAndData": "删除用户及其数据",
"userOptions": "用户选项",
"deleteThisOrganization": "删除该组织",
"dangerZone": "Dangerzone",
"dangerZone": "危险区",
"selectYear": "选择年份",
"save": "保存",
"cancel": "取消",
@ -1099,6 +1103,7 @@
"searchOptions": "搜索选项"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "控制您的组织名称和外观。",
"addCompanyDomains": "添加公司域,限制不需要的用户访问。",
"restrictUsersFromSharing": "限制用户公开共享项目 。",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "可接受的文件类型是 .xls, .xlsx, .xlsm, .ods, .ots",
"parameterKeyCannotBeEmpty": "参数键不能为空",
"duplicateParameterKeysAreNotAllowed": "不允许重复的参数键",
"fieldRequired": "{value} 不能为空。",
"fieldRequired": "此字段不能为空。",
"projectNotAccessible": "无权访问此项目",
"copyToClipboardError": "未能复制到剪贴板",
"pasteFromClipboardError": "从剪贴板粘贴失败",

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

@ -204,7 +204,8 @@
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
"appearance": "Appearance",
"now": "Now"
},
"objects": {
"owner": "Owner",
@ -448,6 +449,9 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"connectionDetails": "Connection Details",
"metaSync": "Meta Sync",
"mention": "Mention",
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
@ -587,7 +591,7 @@
"untitledToken": "Untitled token",
"tableName": "表名稱",
"dashboardName": "Dashboard name",
"createView": "Create a View",
"createView": "Create View",
"creatingView": "Creating View",
"duplicateView": "Duplicate View",
"duplicateGridView": "Duplicate Grid View",
@ -1099,6 +1103,7 @@
"searchOptions": "Search options"
},
"msg": {
"formulaNotSupported": "This function is unavailable for your database",
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
@ -1495,7 +1500,7 @@
"theAcceptedFileTypesAreXlsXlsxXlsmOdsOts": "受支持的檔案類型包括 .xls、.xlsx、.xlsm、.ods 和 .ots",
"parameterKeyCannotBeEmpty": "參數鍵不可為空",
"duplicateParameterKeysAreNotAllowed": "不允許重複的參數鍵",
"fieldRequired": "{value} 不能為空",
"fieldRequired": "This field cannot be empty.",
"projectNotAccessible": "Project not accessible",
"copyToClipboardError": "複製到剪貼簿失敗",
"pasteFromClipboardError": "Failed to paste from clipboard",

1
packages/nc-gui/lib/types.ts

@ -16,6 +16,7 @@ interface User {
invite_token?: string
base_id?: string
display_name?: string | null
featureFlags?: Record<string, boolean>
}
interface ProjectMetaInfo {

16
packages/nc-gui/package.json

@ -41,14 +41,14 @@
"@iconify/vue": "^4.1.2",
"@nuxt/image": "^1.3.0",
"@pinia/nuxt": "^0.5.1",
"@tiptap/extension-link": "2.2.6",
"@tiptap/extension-placeholder": "^2.2.6",
"@tiptap/extension-task-list": "2.2.6",
"@tiptap/extension-underline": "^2.2.6",
"@tiptap/html": "2.2.6",
"@tiptap/pm": "^2.2.6",
"@tiptap/starter-kit": "^2.2.6",
"@tiptap/vue-3": "2.2.6",
"@tiptap/extension-link": "^2.4.0",
"@tiptap/extension-placeholder": "^2.4.0",
"@tiptap/extension-task-list": "2.4.0",
"@tiptap/extension-underline": "^2.4.0",
"@tiptap/html": "2.4.0",
"@tiptap/pm": "^2.4.0",
"@tiptap/starter-kit": "^2.4.0",
"@tiptap/vue-3": "2.4.0",
"@vue-flow/additional-components": "^1.3.3",
"@vue-flow/core": "^1.30.1",
"@vuelidate/core": "^2.0.3",

26
packages/nc-gui/utils/datetimeUtils.ts

@ -24,5 +24,29 @@ export const timeAgo = (date: any) => {
date += '+00:00'
}
// show in local time
return dayjs(date).fromNow()
const diff = dayjs().diff(date)
const seconds = Math.floor(diff / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
const months = Math.floor(days / 30)
const years = Math.floor(days / 365)
if (seconds < 60) {
return `${seconds}s ago`
}
if (minutes < 60) {
return `${minutes}m ago`
}
if (hours < 24) {
return `${hours}h ago`
}
if (days < 30) {
return `${days}d ago`
}
if (months < 12) {
return `${months}mo ago`
}
return `${years}y ago`
}

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

@ -77,7 +77,9 @@ export const requiredFieldValidatorFn = (value: unknown) => {
}
if (typeof value === 'object') {
for (let _ in value) return true
if (Object.keys(value).length > 0) {
return true
}
return false
}

6
packages/nc-gui/utils/iconUtils.ts

@ -136,8 +136,10 @@ import NcPhoneCall from '~icons/nc-icons/phone-call'
import NcItalic from '~icons/nc-icons/italic'
import NcBold from '~icons/nc-icons/bold'
import NcUnderline from '~icons/nc-icons/underline'
import NcCrop from '~icons/nc-icons/crop'
import NcLink from '~icons/nc-icons/link'
import NcAtSign from '~icons/nc-icons/at-sign'
import NcStrike from '~icons/nc-icons/strike-through'
import NcCrop from '~icons/nc-icons/crop'
import NcControlPanel from '~icons/nc-icons/control-panel'
import NcHome from '~icons/nc-icons/home'
import NcWorkspace from '~icons/nc-icons/workspace'
@ -337,6 +339,8 @@ import NcMessageCircle from '~icons/nc-icons/message-circle'
} as const */
export const iconMap = {
strike: NcStrike,
atSign: NcAtSign,
slash: NcSlash,
arrowUpRight: NcArrowUpRight,
ncWorkspace: NcWorkspace,

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

@ -14,7 +14,7 @@ For production use cases, it is **recommended** to set at least:
- `NC_REDIS_URL`
| Variable | Description | If absent |
| -------- | ----------- | --------- |
| -------- | ----------- |-----------------------------------------------------------------------------------------------------|
| `NC_DB` | See our example database URLs [here](https://github.com/nocodb/nocodb#docker). | A local SQLite database is created in root folder if `NC_DB` is not set. |
| `NC_DB_JSON` | Can be used instead of `NC_DB` and value should be valid knex connection JSON string. | |
| `NC_DB_JSON_FILE` | Can be used instead of `NC_DB` and value should be a valid path to knex connection JSON file. | |
@ -68,7 +68,8 @@ For production use cases, it is **recommended** to set at least:
| `NC_ALLOW_LOCAL_HOOKS` | ⚠ Allow webhooks to call local links, which can raise security issues. ⚠ Set to `true` to enable, any other value is treated as `false` | Defaults to `false`. |
| `NC_SANITIZE_COLUMN_NAME` | Sanitize the column name during column creation. Set to `true` to enable, any other value is treated as `false` | Defaults to `true`. |
| `NODE_OPTIONS` | Node.js [options](https://nodejs.org/api/cli.html#node_optionsoptions) to pass to instance. | |
| `LITESTREAM_S3_ENDPOINT` | URL of an S3-compatible object storage service endpoint for [Litestream](https://litestream.io/) replication of NocoDB's default SQLite database. Example: `s3.eu-central-1.amazonaws.com` | *Litestream replication is disabled if this variable is not set.* |
| `LITESTREAM_S3_ENDPOINT` | URL of an S3-compatible object storage service endpoint for [Litestream](https://litestream.io/) replication of NocoDB's default SQLite database. Example: `s3.eu-central-1.amazonaws.com` | Defaults to [AWS S3](https://aws.amazon.com/s3/). |
| `LITESTREAM_S3_REGION` | AWS region of the Litestream replication object storage bucket. Note that `LITESTREAM_S3_ENDPOINT` takes precedence if configured (the endpoint URL includes the region). | Defaults to the [default region configured in AWS](https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-plan-region.html). |
| `LITESTREAM_S3_BUCKET` | Name of the object storage bucket to store the Litestream replication in. | *Litestream replication is disabled if this variable is not set.* |
| `LITESTREAM_S3_PATH` | Directory path to use within the Litestream replication object storage bucket. | Defaults to `nocodb`. |
| `LITESTREAM_S3_ACCESS_KEY_ID` | Authentication key ID for the Litestream replication object storage bucket. | *Litestream replication is disabled if this variable is not set.* |
@ -78,3 +79,9 @@ For production use cases, it is **recommended** to set at least:
| `LITESTREAM_RETENTION_CHECK_INTERVAL` | Frequency in which Litestream will check if retention needs to be enforced. | Defaults to `72h` (3 days). |
| `LITESTREAM_SNAPSHOT_INTERVAL` | Frequency in which new Litestream snapshots are created. A higher frequency reduces the time to restore since newer snapshots will have fewer WAL frames to apply. Retention still applies to these snapshots. | Defaults to `24h` (1 day). |
| `LITESTREAM_SYNC_INTERVAL` | Frequency in which frames are pushed to the Litestream replica. Increasing this frequency can increase object storage costs significantly. | Defaults to `60s` (1 minute). |
| `LITESTREAM_AGE_PUBLIC_KEY` | [age](https://age-encryption.org/) public key generated by `age-keygen` (`age1...`) or SSH public key (`ssh-ed25519 AAAA...`, `ssh-rsa AAAA...`) used to encrypt the Litestream replication for. Refer to the relevant [Litestream documentation](https://litestream.io/reference/config/#encryption) for details. | *Litestream replication is unencrypted if this variable is not set.* |
| `LITESTREAM_AGE_SECRET_KEY` | [age](https://age-encryption.org/) secret key (`AGE-SECRET-KEY-1...`) used to encrypt the Litestream replication with. Refer to the relevant [Litestream documentation](https://litestream.io/reference/config/#encryption) for details. | *Litestream replication is unencrypted if this variable is not set.* |
| `AWS_ACCESS_KEY_ID` | ***Deprecated***. Please use `LITESTREAM_S3_ACCESS_KEY_ID` instead. | |
| `AWS_SECRET_ACCESS_KEY` | ***Deprecated***. Please use `LITESTREAM_S3_SECRET_ACCESS_KEY` instead. | |
| `AWS_BUCKET` | ***Deprecated***. Please use `LITESTREAM_S3_BUCKET` instead. | |
| `AWS_BUCKET_PATH` | ***Deprecated***. Please use `LITESTREAM_S3_PATH` instead. | |

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

@ -89,26 +89,145 @@ To change the field label displayed on the form & add help-text, click on the re
2. **Help Text** `Optional`
3. **Required** : Toggle to mark the field as required
:::info
- Required fields are marked with a red asterisk (*) in the form.
- If Checkbox field is marked as required, it has to be checked to submit the form.
:::
![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:
### Select Field Type
#### 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)
#### 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.
Limit the number of options displayed in the dropdown or list. This is useful when dealing with a large number of options, allowing you to restrict the visible choices for the user.
- 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`.
## Field Validations ☁
![Options Layout](/img/v2/views/form-view/options-layout.png)
:::info
Field validations are only available in the cloud version of NocoDB.
:::
NocoDB allows you to configure input data validations for fields in the form. To configure field validation, click on the required field in the **Form Area**, and in the right configuration panel, configure the desired validations. Supported validations are type-specific and are listed below.
:::info
- Validation rules for fields are only applied when input fields are not empty. Set *Required field* to enforce these validation rules and ensure that the field cannot be left blank.
- The form cannot be submitted if the validation rules are not met.
:::
### Text-based Field Types
For text-based field types `Single Line Text` `Email` `Phone number` `URL` `Long Text` `Rich Text`, you can configure the following validations:
- **Minimum characters**: Specifies the minimum number of characters required for the field.
- **Maximum characters**: Specifies the maximum number of characters allowed for the field.
- **Starts with**: Allows you to define a prefix that the field value must start with.
- **Ends with**: Allows you to define a suffix that the field value must end with.
- **Contains string**: Allows you to define a substring that the field value must contain.
- **Does not contain string**: Allows you to define a substring that the field value must not contain.
- **Regular Expression**: Allows you to define a regular expression pattern that the field value must match.
For Email field type, you can configure the following additional validations:
- **Validate email**: Ensures that the field value is a valid email address.
- **Accept only work email**: Ensures that the field value is a valid work email address.
For Phone number field type, you can configure the following additional validations:
- **Validate phone number**: Ensures that the field value is a valid phone number.
For URL field type, you can configure the following additional validations:
- **Validate URL**: Ensures that the field value is a valid URL.
-----
**Configuration Steps**
1. Click on the required field in the **Form Area**.
2. In the right configuration panel, click on the Settings icon next to `Custom Validations`
3. Click on `Add Validation` to add a new validation rule.
4. `VALIDATOR` Select the type of validation rule to be applied from the dropdown.
5. `VALIDATION VALUE` Enter the value for the validation rule.
6. `WARNING MESSAGE` (Optional) Enter a warning message to be displayed when the validation rule is not met.
![Text Field Validations](/img/v2/views/form-view/text-field-validations.png)
### Numeric Field Types
For numeric field types `Number` `Currency` `Percentage` `Decimal` `Duration`, you can configure the following validations:
- **Minimum**: Specifies the minimum numeric value allowed for the field.
- **Maximum**: Specifies the maximum numeric value allowed for the field.
:::info
- Value configured can be an integer or a decimal number, positive or negative.
- Maximum value should be greater than or equal to the minimum value.
- Value can be left empty to disable the validation.
:::
-----
**Configuration Steps**
1. Click on the required field in the **Form Area**.
2. In the right configuration panel, click on the toggle button next to `Limit number to a range`
3. [Optional] Configure the minimum value allowed for the field.
4. [Optional] Configure the maximum value allowed for the field.
![Numeric Field Validations](/img/v2/views/form-view/numeric-field-validations.png)
### Date-based Field Types
For date-based field types `Date` `Date & Time` `Time` `Year`, you can configure the following validations:
- **Minimum**: Specifies the minimum allowed value for the field.
- **Maximum**: Specifies the maximum allowed value for the field.
:::info
- For both, date & date-time fields, only the date part is considered for validation.
- Value can be left empty to disable the validation.
:::
-----
**Configuration Steps**
1. Click on the required field in the **Form Area**.
2. In the right configuration panel, click on the toggle button next to `Limit date to a range`
3. [Optional] Configure the minimum value allowed for the field.
4. [Optional] Configure the maximum value allowed for the field.
![Date Field Validations](/img/v2/views/form-view/date-field-validations.png)
### Attachment Field Type
For the attachment field type, you can configure the following validations:
- **Limit file types**: Allows you to restrict the file types that can be uploaded by specifying permitted MIME types.
Example
- `image/png` Allows only PNGs
- `application/pdf` Allows PDF documents only
- `image/*` Allows all images
- **Limit file size**: Specifies the maximum file size allowed for the attachment. Size can be either in KB or MB.
- **Limit number of files**: Specifies the maximum number of files that can be uploaded.
:::info
- MIME types should be separated by a comma. Find MIME types for different file formats [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types).
- File size limit specified is the maximum size allowed for each file, not the total size of all files.
:::
-----
**Configuration Steps**
1. Click on the required field in the **Form Area**.
2. `Limit file types` Enter the permitted MIME types separated by a comma.
3. `Limit number of files` Enter the maximum number of files that can be uploaded.
1. Click on the required field in the **Form Area**.
2. In the `Limit file types` section, enter the permitted MIME types separated by a comma.
3. In the `Limit number of files` section, specify the maximum number of files that can be uploaded.
4. In the `Limit file size` section, specify the maximum allowable file size for the attachment.
![Attachment Field Validations](/img/v2/views/form-view/attachment-field-validations.png)
## Prefill Form Fields
Here's a more professional rephrasing of the given content:

BIN
packages/noco-docs/static/img/v2/views/form-view/attachment-field-validations.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

BIN
packages/noco-docs/static/img/v2/views/form-view/date-field-validations.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

BIN
packages/noco-docs/static/img/v2/views/form-view/numeric-field-validations.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

BIN
packages/noco-docs/static/img/v2/views/form-view/text-field-validations.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

252
packages/nocodb-sdk/src/lib/Api.ts

@ -540,7 +540,7 @@ export interface CommentReqType {
* Description for the target row
* @example This is the comment for the row
*/
description?: string;
comment?: string;
/**
* Foreign Key to Model
* @example md_ehn5izr99m7d45
@ -561,7 +561,12 @@ export interface CommentUpdateReqType {
* Description for the target row
* @example This is the comment for the row
*/
description?: string;
comment?: string;
/**
* Foreign Key to Model
* @example md_ehn5izr99m7d45
*/
fk_model_id?: string;
}
/**
@ -2963,6 +2968,144 @@ export interface CalendarColumnReqType {
order?: number;
}
/**
* Model for Comment
*/
export interface CommentType {
/** Unique ID */
id?: IdType;
/**
* Row ID
* @example rec0Adp9PMG9o7uJy
*/
row_id?: string;
/**
* Comment
* @example This is a comment
*/
comment?: string;
/**
* Created By User ID
* @example usr0Adp9PMG9o7uJy
*/
created_by?: IdType;
/**
* Created By User Email
* @example xxx@nocodb.com
*/
created_by_email?: string;
/**
* Resolved By User ID
* @example usr0Adp9PMG9o7uJy
*/
resolved_by?: IdType;
/**
* Resolved By User Email
* @example xxx@nocodb.com
*/
resolved_by_email?: string;
/**
* Parent Comment ID
* @example cmt043cx4r30343ff
*/
parent_comment_id?: IdType;
/**
* Source ID
* @example src0Adp9PMG9o7uJy
*/
source_id?: IdType;
/**
* Base ID
* @example bas0Adp9PMG9o7uJy
*/
base_id?: IdType;
/**
* Model ID
* @example mod0Adp9PMG9o7uJy
*/
fk_model_id?: IdType;
/**
* Created At
* @example 2020-05-20T12:00:00.000000Z
*/
created_at?: string;
/**
* Updated At
* @example 2020-05-20T12:00:00.000000Z
*/
updated_at?: string;
/** Whether the comment has been deleted by the user or not */
is_deleted?: boolean;
}
/**
* Model for User Comment Notification Preference
*/
export interface UserCommentNotificationPreferenceType {
/** Unique ID */
id?: IdType;
/** User ID */
row_id?: string;
/** User ID */
user_id?: IdType;
/**
* Source ID
* @example src0Adp9PMG9o7uJy
*/
source_id?: IdType;
/**
* Base ID
* @example bas0Adp9PMG9o7uJy
*/
base_id?: IdType;
/**
* Model ID
* @example mod0Adp9PMG9o7uJy
*/
fk_model_id?: IdType;
/** Is Read */
preference?: 'ALL_COMMENTS' | 'ONLY_MENTIONS';
/** Created At */
created_at?: string;
/** Updated At */
updated_at?: string;
}
/**
* Model for Comment Reactions
*/
export interface CommentReactionsType {
/** Unique ID */
id?: IdType;
/** Row ID */
row_id?: string;
/** Comment ID */
comment_id?: IdType;
/** Reaction */
reaction?: string;
/** User ID */
user_id?: IdType;
/**
* Source ID
* @example src0Adp9PMG9o7uJy
*/
source_id?: IdType;
/**
* Base ID
* @example bas0Adp9PMG9o7uJy
*/
base_id?: IdType;
/**
* Model ID
* @example mod0Adp9PMG9o7uJy
*/
fk_model_id?: IdType;
/** Created At */
created_at?: string;
/** Updated At */
updated_at?: string;
}
export interface ExtensionType {
/** Unique ID */
id?: IdType;
@ -9671,12 +9814,12 @@ export class Api<
};
utils = {
/**
* @description List all comments
* @description List all audits
*
* @tags Utils
* @name CommentList
* @summary List Comments in Audit
* @request GET:/api/v1/db/meta/audits/comments
* @name AuditList
* @summary List Audits
* @request GET:/api/v1/db/meta/audits
* @response `200` `{
list: (AuditType)[],
@ -9687,7 +9830,7 @@ export class Api<
}`
*/
commentList: (
auditList: (
query: {
/**
* Row ID
@ -9699,24 +9842,67 @@ export class Api<
* @example md_c6csq89tl37jm5
*/
fk_model_id: IdType;
},
params: RequestParams = {}
) =>
this.request<
{
list: AuditType[];
},
{
/** @example BadRequest [Error]: <ERROR MESSAGE> */
msg: string;
}
>({
path: `/api/v1/db/meta/audits`,
method: 'GET',
query: query,
format: 'json',
...params,
}),
/**
* Is showing comments only?
* @example true
* @description List all comments
*
* @tags Utils
* @name CommentList
* @summary List Comments
* @request GET:/api/v1/db/meta/comments
* @response `200` `{
list: (CommentType)[],
}` OK
* @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\
msg: string,
}`
*/
commentList: (
query: {
/**
* Row ID
* @example 10
*/
row_id: string;
/**
* Foreign Key to Model
* @example md_c6csq89tl37jm5
*/
comments_only?: boolean;
fk_model_id: IdType;
},
params: RequestParams = {}
) =>
this.request<
{
list: AuditType[];
list: CommentType[];
},
{
/** @example BadRequest [Error]: <ERROR MESSAGE> */
msg: string;
}
>({
path: `/api/v1/db/meta/audits/comments`,
path: `/api/v1/db/meta/comments`,
method: 'GET',
query: query,
format: 'json',
@ -9724,13 +9910,13 @@ export class Api<
}),
/**
* @description Create a new comment in a row. Logged in Audit.
* @description Create a new comment in a row.
*
* @tags Utils
* @name CommentRow
* @summary Comment Rows
* @request POST:/api/v1/db/meta/audits/comments
* @response `200` `AuditType` OK
* @request POST:/api/v1/db/meta/comments
* @response `200` `CommentType` OK
* @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\
msg: string,
@ -9739,13 +9925,13 @@ export class Api<
*/
commentRow: (data: CommentReqType, params: RequestParams = {}) =>
this.request<
AuditType,
CommentType,
{
/** @example BadRequest [Error]: <ERROR MESSAGE> */
msg: string;
}
>({
path: `/api/v1/db/meta/audits/comments`,
path: `/api/v1/db/meta/comments`,
method: 'POST',
body: data,
type: ContentType.Json,
@ -9754,21 +9940,21 @@ export class Api<
}),
/**
* @description Update comment in Audit
* @description Update comment
*
* @tags Utils
* @name CommentUpdate
* @summary Update Comment in Audit
* @request PATCH:/api/v1/db/meta/audits/{auditId}/comment
* @summary Update Comment
* @request PATCH:/api/v1/db/meta/comment/{commentId}/
* @response `200` `number` OK
*/
commentUpdate: (
auditId: string,
commentId: string,
data: CommentUpdateReqType,
params: RequestParams = {}
) =>
this.request<number, any>({
path: `/api/v1/db/meta/audits/${auditId}/comment`,
path: `/api/v1/db/meta/comment/${commentId}/`,
method: 'PATCH',
body: data,
type: ContentType.Json,
@ -9777,12 +9963,30 @@ export class Api<
}),
/**
* @description Delete comment
*
* @tags Utils
* @name CommentDelete
* @summary Delete Comment
* @request DELETE:/api/v1/db/meta/comment/{commentId}/
* @response `200` `number` OK
*/
commentDelete: (commentId: string, data: any, params: RequestParams = {}) =>
this.request<number, any>({
path: `/api/v1/db/meta/comment/${commentId}/`,
method: 'DELETE',
body: data,
format: 'json',
...params,
}),
/**
* @description Return the number of comments in the given query.
*
* @tags Utils
* @name CommentCount
* @summary Count Comments
* @request GET:/api/v1/db/meta/audits/comments/count
* @request GET:/api/v1/db/meta/comments/count
* @response `200` `({
\**
* The number of comments
@ -9829,7 +10033,7 @@ export class Api<
msg: string;
}
>({
path: `/api/v1/db/meta/audits/comments/count`,
path: `/api/v1/db/meta/comments/count`,
method: 'GET',
query: query,
format: 'json',

13
packages/nocodb-sdk/src/lib/dateTimeHelper.ts

@ -137,3 +137,16 @@ export const timeAgo = (date: any) => {
// show in local time
return dayjs(date).fromNow();
};
export const isValidTimeFormat = (value: string, format: string) => {
const regexValidator = {
[timeFormats[0]]: /^([01]\d|2[0-3]):[0-5]\d$/,
[timeFormats[1]]: /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/,
[timeFormats[2]]: /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}$/,
};
if (regexValidator[format]) {
return regexValidator[format].test(value);
}
return false;
};

4
packages/nocodb-sdk/src/lib/enums.ts

@ -147,6 +147,10 @@ export enum AppEvents {
EXTENSION_CREATE = 'extension.create',
EXTENSION_UPDATE = 'extension.update',
EXTENSION_DELETE = 'extension.delete',
COMMENT_CREATE = 'comment.create',
COMMENT_DELETE = 'comment.delete',
COMMENT_UPDATE = 'comment.update',
}
export enum ClickhouseTables {

6
packages/nocodb/Dockerfile

@ -51,7 +51,6 @@ FROM alpine:3.19
WORKDIR /usr/src/app
ENV LITESTREAM_S3_SKIP_VERIFY=false \
LITESTREAM_S3_PATH=nocodb \
LITESTREAM_RETENTION=1440h \
LITESTREAM_RETENTION_CHECK_INTERVAL=72h \
LITESTREAM_SNAPSHOT_INTERVAL=24h \
@ -62,8 +61,9 @@ ENV LITESTREAM_S3_SKIP_VERIFY=false \
PORT=8080
RUN apk add --update --no-cache \
nodejs \
dumb-init
dasel \
dumb-init \
nodejs
# Copy litestream binary and config file
COPY --link --from=lt-builder /usr/src/lt /usr/local/bin/litestream

2
packages/nocodb/docker/litestream.yml

@ -5,7 +5,7 @@ dbs:
replicas:
- type: s3
endpoint: ${LITESTREAM_S3_ENDPOINT}
force-path-style: true
region: ${LITESTREAM_S3_REGION}
skip-verify: ${LITESTREAM_S3_SKIP_VERIFY}
bucket: ${LITESTREAM_S3_BUCKET}
path: ${LITESTREAM_S3_PATH}

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

@ -4,6 +4,21 @@ if [ ! -d "${NC_TOOL_DIR}" ] ; then
mkdir -p "$NC_TOOL_DIR"
fi
# ensure backwards compatibility of renamed env vars
if [ -z "${LITESTREAM_S3_ACCESS_KEY_ID}" ] && [ -n "${AWS_ACCESS_KEY_ID}" ] ; then
export LITESTREAM_S3_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID}"
fi
if [ -z "${LITESTREAM_S3_SECRET_ACCESS_KEY}" ] && [ -n "${AWS_SECRET_ACCESS_KEY}" ] ; then
export LITESTREAM_S3_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY}"
fi
if [ -z "${LITESTREAM_S3_PATH}" ] && [ -n "${AWS_BUCKET_PATH}" ] ; then
export LITESTREAM_S3_PATH="${AWS_BUCKET_PATH}"
fi
if [ -z "${LITESTREAM_S3_BUCKET}" ] && [ -n "${AWS_BUCKET}" ] ; then
export LITESTREAM_S3_BUCKET="${AWS_BUCKET}"
fi
use_litestream() {
[ -z "${NC_DB}" ] \
&& [ -z "${NC_DB_JSON}" ] \
@ -11,27 +26,48 @@ use_litestream() {
&& [ -z "${DATABASE_URL}" ] \
&& [ -z "${DATABASE_URL_FILE}" ] \
&& [ -z "${NC_MINIMAL_DBS}" ] \
&& [ -n "${LITESTREAM_S3_ENDPOINT}" ] \
&& [ -n "${LITESTREAM_S3_BUCKET}" ] \
&& [ -n "${LITESTREAM_ACCESS_KEY_ID}" ] \
&& [ -n "${LITESTREAM_SECRET_ACCESS_KEY}" ]
&& [ -n "${LITESTREAM_S3_ACCESS_KEY_ID}" ] \
&& [ -n "${LITESTREAM_S3_SECRET_ACCESS_KEY}" ]
}
if use_litestream ; then
# set default bucket path if not provided
: "${LITESTREAM_S3_PATH:=nocodb}"
# enable age encryption in Litestream config if indicated
LITESTREAM_CONFIG_PATH='/etc/litestream.yml'
if [ -n "${LITESTREAM_AGE_PUBLIC_KEY}" ] \
&& [ -n "${LITESTREAM_AGE_SECRET_KEY}" ] \
&& ! dasel --file "${LITESTREAM_CONFIG_PATH}" --read yaml 'dbs.first().replicas.first().age' > /dev/null 2>&1 ; then
# shellcheck disable=SC2016
dasel put --file "${LITESTREAM_CONFIG_PATH}" \
--read yaml \
--type json \
--value '{ "identities": [ "${LITESTREAM_AGE_SECRET_KEY}" ], "recipients": [ "${LITESTREAM_AGE_PUBLIC_KEY}" ] }' \
--selector 'dbs.first().replicas.first().age'
fi
# remove any possible local DB leftovers
if [ -f "${NC_TOOL_DIR}noco.db" ] ; then
rm "${NC_TOOL_DIR}noco.db"
rm -f "${NC_TOOL_DIR}noco.db-shm"
rm -f "${NC_TOOL_DIR}noco.db-wal"
fi
# restore DB from Litestream replica
litestream restore "${NC_TOOL_DIR}noco.db"
# create empty DB file if no Litestream replica exists
if [ ! -f "${NC_TOOL_DIR}noco.db" ] ; then
touch "${NC_TOOL_DIR}noco.db"
fi
# start Litestream replication
litestream replicate &
fi
# start NocoDB
node docker/main.js

24
packages/nocodb/src/app.module.ts

@ -5,35 +5,23 @@ import { ConfigModule } from '@nestjs/config';
import { EventEmitterModule as NestJsEventEmitter } from '@nestjs/event-emitter';
import { SentryModule } from '@ntegral/nestjs-sentry';
import type { MiddlewareConsumer } from '@nestjs/common';
import { NocoModule } from '~/modules/noco.module';
import { AuthModule } from '~/modules/auth/auth.module';
import { GlobalExceptionFilter } from '~/filters/global-exception/global-exception.filter';
import { GlobalMiddleware } from '~/middlewares/global/global.middleware';
import { GuiMiddleware } from '~/middlewares/gui/gui.middleware';
import { DatasModule } from '~/modules/datas/datas.module';
import { EventEmitterModule } from '~/modules/event-emitter/event-emitter.module';
import { AuthService } from '~/services/auth.service';
import { GlobalModule } from '~/modules/global/global.module';
import { LocalStrategy } from '~/strategies/local.strategy';
import { AuthTokenStrategy } from '~/strategies/authtoken.strategy/authtoken.strategy';
import { BaseViewStrategy } from '~/strategies/base-view.strategy/base-view.strategy';
import { MetasModule } from '~/modules/metas/metas.module';
import { JobsModule } from '~/modules/jobs/jobs.module';
import appConfig from '~/app.config';
import { ExtractIdsMiddleware } from '~/middlewares/extract-ids/extract-ids.middleware';
import { HookHandlerService } from '~/services/hook-handler.service';
import { BasicStrategy } from '~/strategies/basic.strategy/basic.strategy';
import { UsersModule } from '~/modules/users/users.module';
import { AuthModule } from '~/modules/auth/auth.module';
import { packageInfo } from '~/utils/packageVersion';
export const ceModuleConfig = {
imports: [
GlobalModule,
UsersModule,
AuthModule,
MetasModule,
DatasModule,
NocoModule,
EventEmitterModule,
JobsModule,
NestJsEventEmitter.forRoot(),
@ -54,7 +42,6 @@ export const ceModuleConfig = {
: []),
],
providers: [
AuthService,
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter,
@ -63,11 +50,6 @@ export const ceModuleConfig = {
provide: APP_GUARD,
useClass: ExtractIdsMiddleware,
},
LocalStrategy,
AuthTokenStrategy,
BaseViewStrategy,
HookHandlerService,
BasicStrategy,
],
};

56
packages/nocodb/src/controllers/audits.controller.ts

@ -4,9 +4,7 @@ import {
Get,
HttpCode,
Param,
Patch,
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
@ -22,14 +20,12 @@ import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
export class AuditsController {
constructor(private readonly auditsService: AuditsService) {}
@Post(['/api/v1/db/meta/audits/comments', '/api/v2/meta/audits/comments'])
@HttpCode(200)
@Acl('commentRow')
async commentRow(@Req() req: Request) {
return await this.auditsService.commentRow({
user: (req as any).user,
body: req.body,
});
@Get(['/api/v1/db/meta/audits/', '/api/v2/meta/audits/'])
@Acl('auditList')
async auditListRow(@Req() req: Request) {
return new PagedResponseImpl(
await this.auditsService.auditOnlyList({ query: req.query as any }),
);
}
@Post([
@ -45,31 +41,6 @@ export class AuditsController {
});
}
@Get(['/api/v1/db/meta/audits/comments', '/api/v2/meta/audits/comments'])
@Acl('commentList')
async commentList(@Req() req: Request) {
return new PagedResponseImpl(
await this.auditsService.commentList({ query: req.query }),
);
}
@Patch([
'/api/v1/db/meta/audits/:auditId/comment',
'/api/v2/meta/audits/:auditId/comment',
])
@Acl('commentUpdate')
async commentUpdate(
@Param('auditId') auditId: string,
@Req() req: Request,
@Body() body: any,
) {
return await this.auditsService.commentUpdate({
auditId,
userEmail: req.user?.email,
body: body,
});
}
@Get([
'/api/v1/db/meta/projects/:baseId/audits/',
'/api/v2/meta/bases/:baseId/audits/',
@ -87,19 +58,4 @@ export class AuditsController {
},
);
}
@Get([
'/api/v1/db/meta/audits/comments/count',
'/api/v2/meta/audits/comments/count',
])
@Acl('commentsCount')
async commentsCount(
@Query('fk_model_id') fk_model_id: string,
@Query('ids') ids: string[],
) {
return await this.auditsService.commentsCount({
fk_model_id,
ids,
});
}
}

89
packages/nocodb/src/controllers/comments.controller.ts

@ -0,0 +1,89 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
Patch,
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { GlobalGuard } from '~/guards/global/global.guard';
import { PagedResponseImpl } from '~/helpers/PagedResponse';
import { CommentsService } from '~/services/comments.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
import { NcRequest } from '~/interface/config';
@Controller()
@UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class CommentsController {
constructor(private readonly commentsService: CommentsService) {}
@Get(['/api/v1/db/meta/comments', '/api/v2/meta/comments'])
@Acl('commentList')
async commentList(@Req() req: any) {
return new PagedResponseImpl(
await this.commentsService.commentList({ query: req.query }),
);
}
@Post(['/api/v1/db/meta/comments', '/api/v2/meta/comments'])
@HttpCode(200)
@Acl('commentRow')
async commentRow(@Req() req: NcRequest, @Body() body: any) {
return await this.commentsService.commentRow({
user: req.user,
body: body,
req,
});
}
@Delete([
'/api/v1/db/meta/comment/:commentId',
'/api/v2/meta/comment/:commentId',
])
@Acl('commentDelete')
async commentDelete(
@Req() req: NcRequest,
@Param('commentId') commentId: string,
) {
return await this.commentsService.commentDelete({
commentId,
user: req.user,
});
}
@Patch([
'/api/v1/db/meta/comment/:commentId',
'/api/v2/meta/comment/:commentId',
])
@Acl('commentUpdate')
async commentUpdate(
@Param('commentId') commentId: string,
@Req() req: any,
@Body() body: any,
) {
return await this.commentsService.commentUpdate({
commentId: commentId,
user: req.user,
body: body,
req,
});
}
@Get(['/api/v1/db/meta/comments/count', '/api/v2/meta/comments/count'])
@Acl('commentsCount')
async commentsCount(
@Query('fk_model_id') fk_model_id: string,
@Query('ids') ids: string[],
) {
return await this.commentsService.commentsCount({
fk_model_id,
ids,
});
}
}

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

Loading…
Cancel
Save