Browse Source

Merge pull request #8601 from nocodb/develop

pull/8603/head 0.207.3
github-actions[bot] 6 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 isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false)) const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isDateInvalid = ref(false) const isDateInvalid = ref(false)
@ -43,6 +45,8 @@ const isClearedInputMode = ref<boolean>(false)
const open = ref<boolean>(false) const open = ref<boolean>(false)
const tempDate = ref<dayjs.Dayjs | undefined>()
const localState = computed({ const localState = computed({
get() { get() {
if (!modelValue || isClearedInputMode.value) { if (!modelValue || isClearedInputMode.value) {
@ -56,7 +60,9 @@ const localState = computed({
const format = picker.value === 'month' ? dateFormat : 'YYYY-MM-DD' 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) { set(val?: dayjs.Dayjs) {
isClearedInputMode.value = false 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)}` const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
onClickOutside(datePickerRef, (e) => { onClickOutside(datePickerRef, (e) => {
if ((e.target as HTMLElement)?.closest(`.${randomClass}`)) return if ((e.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)) return
datePickerRef.value?.blur?.() datePickerRef.value?.blur?.()
open.value = false open.value = false
}) })
const onBlur = (e) => { 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 open.value = false
} }
const onFocus = () => {
open.value = true
}
watch( watch(
open, open,
(next) => { (next) => {
@ -165,14 +187,20 @@ const clickHandler = () => {
cellClickHandler() cellClickHandler()
} }
const handleKeydown = (e: KeyboardEvent) => { const handleKeydown = (e: KeyboardEvent, _open?: boolean) => {
if (e.key !== 'Enter') { if (e.key !== 'Enter' && e.key !== 'Tab') {
e.stopPropagation() e.stopPropagation()
} }
switch (e.key) { switch (e.key) {
case 'Enter': case 'Enter':
open.value = !open.value e.preventDefault()
if (isSurveyForm.value) {
e.stopPropagation()
}
localState.value = tempDate.value
open.value = !_open
if (!open.value) { if (!open.value) {
editable.value = false editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) { if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
@ -181,7 +209,7 @@ const handleKeydown = (e: KeyboardEvent) => {
} }
return return
case 'Escape': case 'Escape':
if (open.value) { if (_open) {
open.value = false open.value = false
editable.value = false editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) { if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
@ -193,9 +221,18 @@ const handleKeydown = (e: KeyboardEvent) => {
datePickerRef.value?.blur?.() datePickerRef.value?.blur?.()
} }
return
case 'Tab':
open.value = false
if (isGrid.value) {
editable.value = false
datePickerRef.value?.blur?.()
}
return return
default: default:
if (!open.value && /^[0-9a-z]$/i.test(e.key)) { if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true 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> </script>
<template> <template>
<a-date-picker <NcDropdown
ref="datePickerRef" :visible="isOpen"
v-model:value="localState" :auto-close="false"
:disabled="readOnly" :trigger="['click']"
:picker="picker" class="nc-cell-field"
:tabindex="0"
:bordered="false"
class="nc-cell-field !w-full !py-1 !border-none !text-current"
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]" :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" :placeholder="placeholder"
:allow-clear="!readOnly && !isEditColumn" class="nc-date-input border-none outline-none !text-current bg-transparent !focus:(border-none outline-none ring-transparent)"
:input-read-only="!!isMobileMode" :readonly="readOnly || !!isMobileMode"
:dropdown-class-name="`${randomClass} nc-picker-date children:border-1 children:border-gray-200 ${open ? 'active' : ''} `"
:open="isOpen"
@blur="onBlur" @blur="onBlur"
@click="clickHandler" @focus="onFocus"
@keydown="handleKeydown" @keydown="handleKeydown($event, open)"
@mouseup.stop @mouseup.stop
@mousedown.stop @mousedown.stop
> @click="clickHandler"
<template #suffixIcon></template> @input="handleUpdateValue"
</a-date-picker> />
<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> <div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template> </template>

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { dateFormats, isSystemColumn, timeFormats } from 'nocodb-sdk' import { dateFormats, isSystemColumn, isValidTimeFormat, timeFormats } from 'nocodb-sdk'
interface Props { interface Props {
modelValue?: string | null modelValue?: string | null
@ -25,6 +25,8 @@ const isGrid = inject(IsGridInj, ref(false))
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const { t } = useI18n() const { t } = useI18n()
const isEditColumn = inject(EditColumnInj, ref(false)) const isEditColumn = inject(EditColumnInj, ref(false))
@ -37,18 +39,27 @@ const isDateInvalid = ref(false)
const datePickerRef = ref<HTMLInputElement>() const datePickerRef = ref<HTMLInputElement>()
const timePickerRef = ref<HTMLInputElement>()
const dateTimeFormat = computed(() => { const dateTimeFormat = computed(() => {
const dateFormat = parseProp(column?.value?.meta)?.date_format ?? dateFormats[0] const dateFormat = parseProp(column?.value?.meta)?.date_format ?? dateFormats[0]
const timeFormat = parseProp(column?.value?.meta)?.time_format ?? timeFormats[0] const timeFormat = parseProp(column?.value?.meta)?.time_format ?? timeFormats[0]
return `${dateFormat} ${timeFormat}` 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 let localModelValue = modelValue ? dayjs(modelValue).utc().local() : undefined
const isClearedInputMode = ref<boolean>(false) const isClearedInputMode = ref<boolean>(false)
const open = ref(false) const open = ref(false)
const tempDate = ref<dayjs.Dayjs | undefined>()
const isDatePicker = ref<boolean>(true)
const localState = computed({ const localState = computed({
get() { get() {
if (!modelValue || isClearedInputMode.value) { 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(() => { 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) 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)}` const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
onClickOutside(datePickerRef, (e) => { onClickOutside(datePickerRef, (e) => {
if ((e.target as HTMLElement)?.closest(`.${randomClass}`)) return if ((e.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)) return
datePickerRef.value?.blur?.() datePickerRef.value?.blur?.()
timePickerRef.value?.blur?.()
open.value = false open.value = false
}) })
const onBlur = (e) => { const onFocus = (_isDatePicker: boolean) => {
if ((e?.relatedTarget as HTMLElement)?.closest(`.${randomClass}`)) return isDatePicker.value = _isDatePicker
open.value = true
open.value = false
} }
watch( watch(
@ -142,7 +164,8 @@ watch(
(next) => { (next) => {
if (next) { if (next) {
editable.value = true editable.value = true
datePickerRef.value?.focus?.()
isDatePicker.value ? datePickerRef.value?.focus?.() : timePickerRef.value?.focus?.()
onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, (e) => { onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, (e) => {
if ((e?.target as HTMLElement)?.closest(`.nc-${randomClass}`)) { if ((e?.target as HTMLElement)?.closest(`.nc-${randomClass}`)) {
@ -162,7 +185,7 @@ const placeholder = computed(() => {
((isForm.value || isExpandedForm.value) && !isDateInvalid.value) || ((isForm.value || isExpandedForm.value) && !isDateInvalid.value) ||
(isGrid.value && !showNull.value && !isDateInvalid.value && !isSystemColumn(column.value) && active.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)) { } else if (isEditColumn.value && (modelValue === '' || modelValue === null)) {
return t('labels.optional') return t('labels.optional')
} else if (modelValue === null && showNull.value) { } else if (modelValue === null && showNull.value) {
@ -180,25 +203,6 @@ const cellClickHandler = () => {
open.value = active.value || editable.value 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(() => { onMounted(() => {
cellClickHook?.on(cellClickHandler) cellClickHook?.on(cellClickHandler)
}) })
@ -206,13 +210,8 @@ onUnmounted(() => {
cellClickHook?.on(cellClickHandler) cellClickHook?.on(cellClickHandler)
}) })
const clickHandler = (e) => { const clickHandler = (e: MouseEvent, _isDatePicker: boolean = false) => {
if ((e.target as HTMLElement).closest(`.nc-${randomClass} .ant-picker-clear`)) { isDatePicker.value = _isDatePicker
e.stopPropagation()
emit('update:modelValue', null)
open.value = false
return
}
if (cellClickHook) { if (cellClickHook) {
return return
@ -220,46 +219,63 @@ const clickHandler = (e) => {
cellClickHandler() cellClickHandler()
} }
const isColDisabled = computed(() => { const handleKeydown = (e: KeyboardEvent, _open?: boolean, _isDatePicker: boolean = false) => {
return isSystemColumn(column.value) || readOnly.value || (localState.value && isPk)
})
const handleKeydown = (e: KeyboardEvent) => {
if (e.key !== 'Enter') { if (e.key !== 'Enter') {
e.stopPropagation() e.stopPropagation()
} }
switch (e.key) { switch (e.key) {
case 'Enter': case 'Enter':
if (isOpen.value) { e.preventDefault()
return okHandler((e.target as HTMLInputElement).value) if (isSurveyForm.value) {
e.stopPropagation()
}
localState.value = tempDate.value
if (!_isDatePicker) {
e.stopPropagation()
timePickerRef.value?.blur?.()
isDatePicker.value = false
datePickerRef.value?.focus?.()
cellClickHandler()
} else { } else {
open.value = true datePickerRef.value?.blur?.()
open.value = false
editable.value = false
} }
return return
case 'Escape': case 'Escape':
if (open.value) { if (_open) {
open.value = false open.value = false
editable.value = false editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) { if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
datePickerRef.value?.blur?.() _isDatePicker ? datePickerRef.value?.blur?.() : timePickerRef.value?.blur?.()
} }
} else { } else {
editable.value = false editable.value = false
datePickerRef.value?.blur?.() _isDatePicker ? datePickerRef.value?.blur?.() : timePickerRef.value?.blur?.()
} }
return return
case 'Tab': case 'Tab':
open.value = false 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 editable.value = false
datePickerRef.value?.blur() } else if (!e.shiftKey && !_isDatePicker) {
editable.value = false
} else {
e.stopPropagation()
}
} }
return return
default: default:
if (!open.value && /^[0-9a-z]$/i.test(e.key)) { if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true open.value = true
} }
} }
@ -288,9 +304,9 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
e.preventDefault() e.preventDefault()
break break
default: 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 isClearedInputMode.value = true
datePickerRef.value.focus() isDatePicker.value ? datePickerRef.value?.focus() : timePickerRef.value?.focus()
editable.value = true editable.value = true
open.value = true open.value = true
} }
@ -299,35 +315,203 @@ useEventListener(document, 'keydown', (e: KeyboardEvent) => {
watch(editable, (nextValue) => { watch(editable, (nextValue) => {
if (isGrid.value && nextValue && !open.value) { if (isGrid.value && nextValue && !open.value) {
isDatePicker.value = true
open.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> </script>
<template> <template>
<a-date-picker <div class="nc-cell-field group relative">
ref="datePickerRef" <NcDropdown
:value="localState" :visible="isOpen"
:disabled="isColDisabled" :placement="isDatePicker ? 'bottomLeft' : 'bottomRight'"
:show-time="true" :auto-close="false"
:bordered="false" :trigger="['click']"
class="nc-cell-field nc-cell-picker-datetime !w-full !py-1 !border-none !text-current" class="nc-cell-picker-datetime"
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]" :class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]"
:format="dateTimeFormat" :overlay-class-name="`${randomClass} nc-picker-datetime ${open ? 'active' : ''} !min-w-[0] overflow-hidden`"
:placeholder="placeholder" >
:allow-clear="!isColDisabled && !isEditColumn" <div
:input-read-only="!!isMobileMode" :title="localState?.format(dateTimeFormat)"
:dropdown-class-name="`${randomClass} nc-picker-datetime children:border-1 children:border-gray-200 ${open ? 'active' : ''}`" class="nc-date-picker ant-picker-input flex justify-between gap-2 relative group !w-auto"
:open="isOpen" >
@blur="onBlur" <div
@click="clickHandler" class="flex-none hover:bg-gray-100 px-1 rounded-md box-border w-[60%] max-w-[110px]"
@ok="okHandler" :class="{
@keydown="handleKeydown" '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 @mouseup.stop
@mousedown.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> <input
</a-date-picker> 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> <div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template> </template>

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { isSystemColumn } from 'nocodb-sdk' import { isSystemColumn, isValidTimeFormat } from 'nocodb-sdk'
interface Props { interface Props {
modelValue?: string | null | undefined modelValue?: string | null | undefined
@ -27,6 +27,8 @@ const isGrid = inject(IsGridInj, ref(false))
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false)) const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
@ -43,6 +45,8 @@ const { t } = useI18n()
const open = ref(false) const open = ref(false)
const tempDate = ref<dayjs.Dayjs | undefined>()
const localState = computed({ const localState = computed({
get() { get() {
if (!modelValue || isClearedInputMode.value) { 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)}` const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
onClickOutside(datePickerRef, (e) => { onClickOutside(datePickerRef, (e) => {
if ((e.target as HTMLElement)?.closest(`.${randomClass}`)) return if ((e.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)) return
datePickerRef.value?.blur?.() datePickerRef.value?.blur?.()
open.value = false open.value = false
}) })
const onBlur = (e) => { 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 open.value = false
} }
const onFocus = () => {
open.value = true
}
watch( watch(
open, open,
(next) => { (next) => {
@ -146,26 +165,42 @@ const clickHandler = () => {
open.value = active.value || editable.value open.value = active.value || editable.value
} }
const handleKeydown = (e: KeyboardEvent) => { const handleKeydown = (e: KeyboardEvent, _open?: boolean) => {
if (e.key !== 'Enter') { if (e.key !== 'Enter') {
e.stopPropagation() e.stopPropagation()
} }
switch (e.key) { switch (e.key) {
case 'Enter': case 'Enter':
open.value = !open.value e.preventDefault()
if (isSurveyForm.value) {
e.stopPropagation()
}
localState.value = tempDate.value
open.value = !_open
if (!open.value) { if (!open.value) {
editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) { if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
editable.value = false
datePickerRef.value?.blur?.() datePickerRef.value?.blur?.()
} }
} }
return return
case 'Escape':
if (open.value) { case 'Tab':
open.value = false open.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
editable.value = false editable.value = false
datePickerRef.value?.blur?.()
}
return
case 'Escape':
if (_open) {
open.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) { if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
editable.value = false
datePickerRef.value?.blur?.() datePickerRef.value?.blur?.()
} }
} else { } else {
@ -175,7 +210,7 @@ const handleKeydown = (e: KeyboardEvent) => {
} }
return return
default: default:
if (!open.value && /^[0-9a-z]$/i.test(e.key)) { if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true 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> </script>
<template> <template>
<a-time-picker <NcDropdown
ref="datePickerRef" :visible="isOpen"
v-model:value="localState" :auto-close="false"
:tabindex="0" :trigger="['click']"
:disabled="readOnly" class="nc-cell-field"
:show-time="true"
:bordered="false"
use12-hours
format="HH:mm"
class="nc-cell-field !w-full !py-1 !border-none !text-current"
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]" :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" :placeholder="placeholder"
:allow-clear="!readOnly && !isPk && !isEditColumn" class="nc-time-input border-none outline-none !text-current bg-transparent !focus:(border-none outline-none ring-transparent)"
:input-read-only="!!isMobileMode" :readonly="readOnly || !!isMobileMode"
:open="isOpen"
:popup-class-name="`${randomClass} nc-picker-time children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
@blur="onBlur" @blur="onBlur"
@keydown="handleKeydown" @focus="onFocus"
@click="clickHandler" @keydown="handleKeydown($event, isOpen)"
@ok="open = !open"
@mouseup.stop @mouseup.stop
@mousedown.stop @mousedown.stop
> @click="clickHandler"
<template #suffixIcon></template> @input="handleUpdateValue"
</a-time-picker> />
<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> <div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template> </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 isForm = inject(IsFormInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false)) const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isYearInvalid = ref(false) const isYearInvalid = ref(false)
@ -39,6 +41,8 @@ const { t } = useI18n()
const open = ref<boolean>(false) const open = ref<boolean>(false)
const tempDate = ref<dayjs.Dayjs | undefined>()
const localState = computed({ const localState = computed({
get() { get() {
if (!modelValue || isClearedInputMode.value) { 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)}` const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
onClickOutside(datePickerRef, (e) => { onClickOutside(datePickerRef, (e) => {
if ((e.target as HTMLElement)?.closest(`.${randomClass}`)) return if ((e.target as HTMLElement)?.closest(`.${randomClass}, .nc-${randomClass}`)) return
datePickerRef.value?.blur?.() datePickerRef.value?.blur?.()
open.value = false open.value = false
}) })
const onBlur = (e) => { 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 open.value = false
} }
@ -136,14 +151,20 @@ const clickHandler = () => {
open.value = active.value || editable.value open.value = active.value || editable.value
} }
const handleKeydown = (e: KeyboardEvent) => { const handleKeydown = (e: KeyboardEvent, _open?: boolean) => {
if (e.key !== 'Enter') { if (e.key !== 'Enter' && e.key !== 'Tab') {
e.stopPropagation() e.stopPropagation()
} }
switch (e.key) { switch (e.key) {
case 'Enter': case 'Enter':
open.value = !open.value e.preventDefault()
if (isSurveyForm.value) {
e.stopPropagation()
}
localState.value = tempDate.value
open.value = !_open
if (!open.value) { if (!open.value) {
editable.value = false editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) { 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 return
case 'Escape': case 'Escape':
if (open.value) { if (_open) {
open.value = false open.value = false
editable.value = false editable.value = false
if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) { if (isGrid.value && !isExpandedForm.value && !isEditColumn.value) {
@ -166,7 +197,7 @@ const handleKeydown = (e: KeyboardEvent) => {
} }
return return
default: default:
if (!open.value && /^[0-9a-z]$/i.test(e.key)) { if (!_open && /^[0-9a-z]$/i.test(e.key)) {
open.value = true 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> </script>
<template> <template>
<a-date-picker <NcDropdown
ref="datePickerRef" :visible="isOpen"
v-model:value="localState" :auto-close="false"
:trigger="['click']"
:disabled="readOnly" :disabled="readOnly"
:tabindex="0" class="nc-cell-field"
picker="year"
:bordered="false"
class="nc-cell-field !w-full !py-1 !border-none !text-current"
:class="[`nc-${randomClass}`, { 'nc-null': modelValue === null && showNull }]" :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" :placeholder="placeholder"
:allow-clear="!readOnly && !isPk" class="nc-year-input border-none outline-none !text-current bg-transparent !focus:(border-none outline-none ring-transparent)"
:input-read-only="!!isMobileMode" :readonly="readOnly || !!isMobileMode"
:open="isOpen"
:dropdown-class-name="`${randomClass} nc-picker-year children:border-1 children:border-gray-200 ${open ? 'active' : ''}`"
@blur="onBlur" @blur="onBlur"
@keydown="handleKeydown" @keydown="handleKeydown($event, open)"
@click="clickHandler"
@mouseup.stop @mouseup.stop
@mousedown.stop @mousedown.stop
> @click="clickHandler"
<template #suffixIcon></template> @input="handleUpdateValue"
</a-date-picker> />
<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> <div v-if="!editable && isGrid" class="absolute inset-0 z-90 cursor-pointer"></div>
</template> </template>

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

@ -219,7 +219,7 @@ onMounted(() => {
</div> </div>
<div class="flex w-1/2 justify-end text-gray-600"> <div class="flex w-1/2 justify-end text-gray-600">
<div class="flex gap-2 px-2 py-1 rounded-md items-center"> <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"> <a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg">
<template #title> <template #title>
{{ cmdOption.baseName }} {{ cmdOption.baseName }}
@ -230,7 +230,7 @@ onMounted(() => {
</a-tooltip> </a-tooltip>
<span class="text-bold"> / </span> <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"> <a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg">
<template #title> <template #title>
{{ cmdOption.tableName }} {{ cmdOption.tableName }}

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

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

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

@ -103,7 +103,7 @@ const columns = [
</script> </script>
<template> <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 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"> <div class="flex flex-row justify-between items-center">
<h6 class="mb-4 first-letter:capital font-bold">Audit : {{ base.title }}</h6> <h6 class="mb-4 first-letter:capital font-bold">Audit : {{ base.title }}</h6>
@ -116,6 +116,7 @@ const columns = [
</a-button> </a-button>
</div> </div>
<div class="h-[calc(100%_-_102px)] overflow-y-auto nc-scrollbar-thin">
<a-table <a-table
class="nc-audit-table w-full" class="nc-audit-table w-full"
size="small" size="small"
@ -124,12 +125,15 @@ const columns = [
:pagination="false" :pagination="false"
:loading="isLoading" :loading="isLoading"
data-testid="audit-tab-table" data-testid="audit-tab-table"
sticky
bordered
> >
<template #emptyText> <template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template> </template>
</a-table> </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 <a-pagination
v-model:current="currentPage" v-model:current="currentPage"
v-model:page-size="currentLimit" v-model:page-size="currentLimit"
@ -151,6 +155,7 @@ const columns = [
font-size: unset; font-size: unset;
font-family: unset; font-family: unset;
} }
.pagination { .pagination {
.ant-select-dropdown { .ant-select-dropdown {
@apply !border-1 !border-gray-200; @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>{{ $t('title.auditLogs') }}</div>
</div> </div>
</template> </template>
<div class="p-4 h-full overflow-auto"> <div class="p-4 h-full">
<LazyDashboardSettingsBaseAudit :source-id="activeSource.id" /> <LazyDashboardSettingsBaseAudit :source-id="activeSource.id" />
</div> </div>
</a-tab-pane> </a-tab-pane>
@ -330,7 +330,7 @@ const openedTab = ref('erd')
</div> </div>
</template> </template>
<div class="pt-4 h-full overflow-auto"> <div class="pt-4 h-full">
<LazyDashboardSettingsUIAcl :source-id="activeSource.id" /> <LazyDashboardSettingsUIAcl :source-id="activeSource.id" />
</div> </div>
</a-tab-pane> </a-tab-pane>
@ -340,7 +340,7 @@ const openedTab = ref('erd')
<div>{{ $t('labels.metaSync') }}</div> <div>{{ $t('labels.metaSync') }}</div>
</div> </div>
</template> </template>
<div class="pt-4 h-full overflow-auto"> <div class="pt-4 h-full">
<LazyDashboardSettingsMetadata :source-id="activeSource.id" @source-synced="loadBases(true)" /> <LazyDashboardSettingsMetadata :source-id="activeSource.id" @source-synced="loadBases(true)" />
</div> </div>
</a-tab-pane> </a-tab-pane>

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

@ -108,8 +108,8 @@ const columns = [
</script> </script>
<template> <template>
<div class="flex flex-col w-full"> <div class="h-full flex flex-col w-full">
<div class="flex flex-col"> <div class="h-full flex flex-col">
<div class="flex flex-row justify-between items-center w-full mb-4"> <div class="flex flex-row justify-between items-center w-full mb-4">
<div class="flex"> <div class="flex">
<div v-if="isDifferent"> <div v-if="isDifferent">
@ -140,9 +140,9 @@ const columns = [
</div> </div>
</a-button> </a-button>
</div> </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 <a-table
class="w-full" class="nc-metasync-table w-full"
size="small" size="small"
:custom-row=" :custom-row="
(record) => ({ (record) => ({
@ -153,6 +153,7 @@ const columns = [
:columns="columns" :columns="columns"
:pagination="false" :pagination="false"
:loading="isLoading" :loading="isLoading"
sticky
bordered bordered
> >
<template #emptyText> <template #emptyText>
@ -178,3 +179,5 @@ const columns = [
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped></style>

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

@ -130,8 +130,8 @@ const toggleSelectAll = (role: Role) => {
</script> </script>
<template> <template>
<div class="flex flex-row w-full items-center justify-center"> <div class="h-full flex flex-row w-full items-center justify-center">
<div class="flex flex-col"> <div class="h-full flex flex-col">
<NcTooltip class="mb-4 first-letter:capital font-bold max-w-100 truncate" show-on-truncate-only> <NcTooltip class="mb-4 first-letter:capital font-bold max-w-100 truncate" show-on-truncate-only>
<template #title>{{ base.title }}</template> <template #title>{{ base.title }}</template>
<span> UI ACL : {{ base.title }} </span> <span> UI ACL : {{ base.title }} </span>
@ -159,7 +159,7 @@ const toggleSelectAll = (role: Role) => {
</div> </div>
</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 <a-table
class="w-full" class="w-full"
size="small" 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="mr-1 flex items-center justify-center"
:class="[plugin.title === 'SES' ? 'p-2 bg-[#242f3e]' : '']" :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> </div>
<span class="font-semibold text-lg">{{ plugin.formDetails.title }}</span> <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.user': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()], 'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.database': [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()], 'dataSource.searchPath.0': [fieldRequiredValidator()],
} }
@ -342,6 +343,19 @@ onMounted(async () => {
updateSSLUse() 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> </script>
<template> <template>

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

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

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

@ -3,7 +3,7 @@ const props = withDefaults(
defineProps<{ defineProps<{
visible: boolean visible: boolean
width?: string | number width?: string | number
size?: 'small' | 'medium' | 'large' size?: 'small' | 'medium' | 'large' | 'xl'
destroyOnClose?: boolean destroyOnClose?: boolean
maskClosable?: boolean maskClosable?: boolean
closable?: boolean closable?: boolean
@ -40,6 +40,10 @@ const width = computed(() => {
return '80rem' return '80rem'
} }
if (props.size === 'xl') {
return '80rem'
}
return 'max(30vw, 600px)' return 'max(30vw, 600px)'
}) })
@ -55,6 +59,9 @@ const height = computed(() => {
if (props.size === 'large') { if (props.size === 'large') {
return '80vh' return '80vh'
} }
if (props.size === 'xl') {
return '90vh'
}
return 'auto' return 'auto'
}) })
@ -75,7 +82,7 @@ const visible = useVModel(props, 'visible', emits)
:mask-closable="maskClosable" :mask-closable="maskClosable"
@keydown.esc="visible = false" @keydown.esc="visible = false"
> >
<div :class="`nc-modal max-h-[${height}]`"> <div :class="`nc-modal h-[${height}] max-h-[${height}]`">
<slot /> <slot />
</div> </div>
</a-modal> </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 start: dayjs.Dayjs
end: dayjs.Dayjs end: dayjs.Dayjs
} | null } | null
isCellInputField?: boolean
pickerType?: 'date' | 'time' | 'year' | 'month'
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
size: 'medium', size: 'medium',
selectedDate: null, selectedDate: null,
isMondayFirst: true, isMondayFirst: true,
pageDate: dayjs(), pageDate: () => dayjs(),
isWeekPicker: false, isWeekPicker: false,
activeDates: [] as Array<dayjs.Dayjs>, activeDates: () => [] as Array<dayjs.Dayjs>,
selectedWeek: null, selectedWeek: null,
hideCalendar: false, 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 // Page date is the date we use to manage which month/date that is currently being displayed
const pageDate = useVModel(props, 'pageDate', emit) const pageDate = useVModel(props, 'pageDate', emit)
@ -35,6 +39,8 @@ const activeDates = useVModel(props, 'activeDates', emit)
const selectedWeek = useVModel(props, 'selectedWeek', emit) const selectedWeek = useVModel(props, 'selectedWeek', emit)
const pickerType = useVModel(props, 'pickerType', emit)
const days = computed(() => { const days = computed(() => {
if (props.isMondayFirst) { if (props.isMondayFirst) {
return ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'] return ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
@ -47,6 +53,14 @@ const currentMonthYear = computed(() => {
return dayjs(pageDate.value).format('MMMM YYYY') 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 selectWeek = (date: dayjs.Dayjs) => {
const dayOffset = +props.isMondayFirst const dayOffset = +props.isMondayFirst
const dayOfWeek = (date.day() - dayOffset + 7) % 7 const dayOfWeek = (date.day() - dayOffset + 7) % 7
@ -102,6 +116,9 @@ const isDayInPagedMonth = (date: dayjs.Dayjs) => {
const handleSelectDate = (date: dayjs.Dayjs) => { const handleSelectDate = (date: dayjs.Dayjs) => {
if (props.isWeekPicker) { if (props.isWeekPicker) {
selectWeek(date) selectWeek(date)
} else if (props.isCellInputField) {
selectedDate.value = date
emit('update:selectedDate', date)
} else { } else {
if (!isDayInPagedMonth(date)) { if (!isDayInPagedMonth(date)) {
pageDate.value = date pageDate.value = date
@ -137,17 +154,30 @@ const paginate = (action: 'next' | 'prev') => {
<template> <template>
<div class="flex flex-col"> <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> <NcTooltip hide-on-click>
<NcButton class="!border-0" size="small" type="secondary" @click="paginate('prev')"> <NcButton class="!border-0" size="small" type="secondary" @click="paginate('prev')">
<component :is="iconMap.arrowLeft" class="h-4 w-4" /> <component :is="iconMap.arrowLeft" class="h-4 w-4" />
</NcButton> </NcButton>
<template #title> <template #title>
<span>{{ $t('labels.next') }}</span> <span>{{ $t('labels.previous') }}</span>
</template> </template>
</NcTooltip> </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> <NcTooltip hide-on-click>
<NcButton class="!border-0" data-testid="nc-calendar-next-btn" size="small" type="secondary" @click="paginate('next')"> <NcButton class="!border-0" data-testid="nc-calendar-next-btn" size="small" type="secondary" @click="paginate('next')">
@ -159,7 +189,13 @@ const paginate = (action: 'next' | 'prev') => {
</NcTooltip> </NcTooltip>
</div> </div>
<div v-if="!hideCalendar" class="max-w-[320px] rounded-y-xl"> <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 <span
v-for="(day, index) in days" v-for="(day, index) in days"
:key="index" :key="index"
@ -167,13 +203,22 @@ const paginate = (action: 'next' | 'prev') => {
>{{ day[0] }}</span >{{ day[0] }}</span
> >
</div> </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 <span
v-for="(date, index) in dates" v-for="(date, index) in dates"
:key="index" :key="index"
:class="{ :class="{
'rounded-lg': !isWeekPicker, 'rounded-lg': !isWeekPicker && !isCellInputField,
'bg-gray-200 border-1 font-bold ': isSelectedDate(date) && !isWeekPicker && isDayInPagedMonth(date), '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, 'hover:(border-1 border-gray-200 bg-gray-100)': !isSelectedDate(date) && !isWeekPicker,
'nc-selected-week !font-semibold z-1': isDateInSelectedWeek(date) && isWeekPicker, 'nc-selected-week !font-semibold z-1': isDateInSelectedWeek(date) && isWeekPicker,
'border-none': isWeekPicker, 'border-none': isWeekPicker,
@ -183,9 +228,13 @@ const paginate = (action: 'next' | 'prev') => {
'nc-selected-week-end': isSameDate(date, selectedWeek?.end), 'nc-selected-week-end': isSameDate(date, selectedWeek?.end),
'rounded-md text-brand-500 !font-semibold nc-calendar-today': isSameDate(date, dayjs()) && isDateInCurrentMonth(date), '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, '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" data-testid="nc-calendar-date"
:title="isCellInputField ? date.format('YYYY-MM-DD') : undefined"
@click="handleSelectDate(date)" @click="handleSelectDate(date)"
> >
<span <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" class="absolute top-1 transition right-1 h-1.5 w-1.5 z-2 border-1 rounded-full border-white bg-brand-500"
></span> ></span>
<span class="z-2"> <span class="nc-date-item-inner z-2">
{{ date.get('date') }} {{ date.get('date') }}
</span> </span>
</span> </span>
</div> </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>
</div> </div>
</template> </template>

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

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

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

@ -6,20 +6,26 @@ interface Props {
pageDate?: dayjs.Dayjs pageDate?: dayjs.Dayjs
isYearPicker?: boolean isYearPicker?: boolean
hideCalendar?: boolean hideCalendar?: boolean
isCellInputField?: boolean
pickerType?: 'date' | 'time' | 'year' | 'month'
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
selectedDate: null, selectedDate: null,
pageDate: dayjs(), pageDate: () => dayjs(),
isYearPicker: false, isYearPicker: false,
hideCalendar: 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 pageDate = useVModel(props, 'pageDate', emit)
const selectedDate = useVModel(props, 'selectedDate', emit) const selectedDate = useVModel(props, 'selectedDate', emit)
const pickerType = useVModel(props, 'pickerType', emit)
const years = computed(() => { const years = computed(() => {
const date = pageDate.value const date = pageDate.value
const startOfYear = date.startOf('year') const startOfYear = date.startOf('year')
@ -86,24 +92,41 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
<template> <template>
<div class="flex flex-col"> <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"> <div class="flex">
<NcTooltip hide-on-click> <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" /> <component :is="iconMap.arrowLeft" class="h-4 w-4" />
</NcButton> </NcButton>
<template #title> <template #title>
<span>{{ $t('labels.next') }}</span> <span>{{ $t('labels.previous') }}</span>
</template> </template>
</NcTooltip> </NcTooltip>
</div> </div>
<span class="text-gray-700 font-semibold">{{ <span
isYearPicker ? dayjs(selectedDate).year() : dayjs(pageDate).format('YYYY') class="nc-year-picker-btn text-gray-700 font-semibold"
}}</span> :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"> <div class="flex">
<NcTooltip hide-on-click> <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" /> <component :is="iconMap.arrowRight" class="h-4 w-4" />
</NcButton> </NcButton>
<template #title> <template #title>
@ -112,17 +135,29 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
</NcTooltip> </NcTooltip>
</div> </div>
</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"> <div class="grid grid-cols-4 gap-2">
<template v-if="!isYearPicker"> <template v-if="!isYearPicker">
<span <span
v-for="(month, id) in months" v-for="(month, id) in months"
:key="id" :key="id"
:class="{ :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'), '!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" @click="selectedDate = month"
> >
{{ month.format('MMM') }} {{ month.format('MMM') }}
@ -133,10 +168,15 @@ const compareYear = (date1: dayjs.Dayjs, date2: dayjs.Dayjs) => {
v-for="(year, id) in years" v-for="(year, id) in years"
:key="id" :key="id"
:class="{ :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'), '!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" @click="selectedDate = year"
> >
{{ year.format('YYYY') }} {{ 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(ActiveViewInj, sharedView)
provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true)) provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), 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(ActiveViewInj, sharedView)
provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true)) provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), 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(ReadonlyInj, ref(true))
provide(MetaInj, meta) provide(MetaInj, meta)
provide(ActiveViewInj, sharedView) provide(ActiveViewInj, sharedView)
provide(FieldsInj, columns)
provide(IsPublicInj, ref(true)) provide(IsPublicInj, ref(true))
provide(IsLockedInj, isLocked) provide(IsLockedInj, isLocked)

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

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

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

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

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

@ -73,7 +73,7 @@ const snippet = computed(
() => () =>
new HTTPSnippet({ new HTTPSnippet({
method: 'GET', 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, url: apiUrl.value,
queryString: [ queryString: [
...Object.entries(queryParams.value || {}).map(([name, value]) => { ...Object.entries(queryParams.value || {}).map(([name, value]) => {
@ -95,7 +95,7 @@ const code = computed(() => {
const api = new Api({ const api = new Api({
baseURL: "${(appInfo.value && appInfo.value.ncSiteUrl) || '/'}", baseURL: "${(appInfo.value && appInfo.value.ncSiteUrl) || '/'}",
headers: { 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"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { CommentType } from 'nocodb-sdk'
import type { AuditType } from 'nocodb-sdk'
import { timeAgo } from 'nocodb-sdk'
const props = defineProps<{ const props = defineProps<{
loading: boolean 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 { isExpandedFormCommentMode } = storeToRefs(useConfigStore())
const commentsWrapperEl = ref<HTMLDivElement>() const commentsWrapperEl = ref<HTMLDivElement>()
const commentInputRef = ref<any>()
const editRef = ref<any>()
const { user, appInfo } = useGlobal() const { user, appInfo } = useGlobal()
const isExpandedFormLoading = computed(() => props.loading) const isExpandedFormLoading = computed(() => props.loading)
@ -23,25 +35,12 @@ const { isUIAllowed } = useRoles()
const hasEditPermission = computed(() => isUIAllowed('commentEdit')) const hasEditPermission = computed(() => isUIAllowed('commentEdit'))
const editLog = ref<AuditType>() const editComment = ref<CommentType>()
const isEditing = ref<boolean>(false) const isEditing = ref<boolean>(false)
const isCommentMode = ref(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) { function onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
onKeyEsc(event) onKeyEsc(event)
@ -63,26 +62,26 @@ function onKeyEsc(event: KeyboardEvent) {
} }
async function onEditComment() { async function onEditComment() {
if (!isEditing.value || !editLog.value) return if (!isEditing.value || !editComment.value) return
isCommentMode.value = true isCommentMode.value = true
await updateComment(editLog.value.id!, { await updateComment(editComment.value.id!, {
description: editLog.value.description, comment: editComment.value?.comment,
}) })
onStopEdit() onStopEdit()
} }
function onCancel() { function onCancel() {
if (!isEditing.value) return if (!isEditing.value) return
editLog.value = undefined editComment.value = undefined
onStopEdit() onStopEdit()
} }
function onStopEdit() { function onStopEdit() {
loadCommentsAndLogs() loadComments()
isEditing.value = false isEditing.value = false
editLog.value = undefined editComment.value = undefined
} }
onKeyStroke('Enter', (event) => { onKeyStroke('Enter', (event) => {
@ -91,26 +90,28 @@ onKeyStroke('Enter', (event) => {
} }
}) })
const comments = computed(() => commentsAndLogs.value.filter((log) => log.op_type === 'COMMENT')) function editComments(comment: CommentType) {
const audits = computed(() => commentsAndLogs.value.filter((log) => log.op_type !== 'COMMENT' && log.details)) editComment.value = comment
function editComment(log: AuditType) {
editLog.value = log
isEditing.value = true isEditing.value = true
} }
const value = computed({ const value = computed({
get() { get() {
return editLog.value?.description?.substring(editLog.value?.description?.indexOf(':') + 1) ?? '' return editComment.value?.comment || ''
}, },
set(val) { set(val) {
if (!editLog.value) return if (!editComment.value) return
editLog.value.description = val editComment.value.comment = val
}, },
}) })
function scrollComments() { 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) const isSaving = ref(false)
@ -123,7 +124,10 @@ const saveComment = async () => {
try { try {
await _saveComment() await _saveComment()
await nextTick(() => {
commentInputRef?.value?.setEditorContent('', true)
isExpandedFormCommentMode.value = true
})
scrollComments() scrollComments()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@ -133,8 +137,12 @@ const saveComment = async () => {
} }
watch(commentsWrapperEl, () => { watch(commentsWrapperEl, () => {
setTimeout(() => {
nextTick(() => {
scrollComments() scrollComments()
}) })
}, 100)
})
</script> </script>
<template> <template>
@ -164,118 +172,140 @@ watch(commentsWrapperEl, () => {
<div class="font-medium text-center my-6 text-gray-500">{{ $t('activity.startCommenting') }}</div> <div class="font-medium text-center my-6 text-gray-500">{{ $t('activity.startCommenting') }}</div>
</div> </div>
<div v-else ref="commentsWrapperEl" class="flex flex-col h-full py-1 nc-scrollbar-thin"> <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 v-for="comment of comments" :key="comment.id" class="nc-comment-item">
<div class="group gap-3 overflow-hidden hover:bg-gray-200 flex items-start px-3 pt-3 pb-4"> <div
<GeneralUserIcon size="medium" :name="log.display_name" :email="log.user" class="mt-0.5" /> :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="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="w-full flex justify-between gap-3 min-h-7">
<div class="flex items-start max-w-[calc(100%_-_40px)]"> <div class="flex items-center max-w-[calc(100%_-_40px)]">
<div class="w-full flex flex-wrap items-center"> <div class="w-full flex flex-wrap gap-3 items-center">
<NcTooltip class="truncate max-w-42 mr-2" show-on-truncate-only> <NcTooltip
class="truncate capitalize text-gray-800 font-weight-700 !text-[13px] max-w-42"
show-on-truncate-only
>
<template #title> <template #title>
{{ log.display_name?.trim() || log.user || 'Shared source' }} {{ comment.created_display_name?.trim() || comment.created_by_email || 'Shared source' }}
</template> </template>
<span <span
class="text-ellipsis overflow-hidden text-gray-500 font-weight-500 text-small" class="text-ellipsis capitalize overflow-hidden"
:style="{ :style="{
lineHeight: '18px',
wordBreak: 'keep-all', wordBreak: 'keep-all',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
display: 'inline', 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> </span>
</NcTooltip> </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>
</div> </div>
<div class="flex items-center opacity-0 transition-all group-hover:opacity-100 ease-out duration-400 gap-2">
<NcDropdown <NcDropdown
v-if="log.user === user!.email && !editLog" v-if="comment.created_by_email === user!.email && !editComment"
placement="bottomRight"
overlay-class-name="!min-w-[160px]" overlay-class-name="!min-w-[160px]"
placement="bottomRight"
> >
<NcButton <NcButton class="nc-expand-form-more-actions !w-7 !h-7 !bg-transparent" size="xsmall" type="text">
type="text" <GeneralIcon class="text-md" icon="threeDotVertical" />
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> </NcButton>
<template #overlay> <template #overlay>
<NcMenu> <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"> <div class="flex gap-2 items-center">
<component :is="iconMap.rename" class="cursor-pointer" /> <component :is="iconMap.rename" class="cursor-pointer" />
{{ $t('general.edit') }} {{ $t('general.edit') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
<!-- eslint-disable vue/no-constant-condition --> <NcMenuItem
<template v-if="false"> v-e="['c:row-expand:comment:delete']"
<NcDivider /> 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"> <div class="flex gap-2 items-center">
<GeneralIcon icon="delete" /> <component :is="iconMap.delete" class="cursor-pointer" />
{{ $t('general.delete') }} {{ $t('general.delete') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
</template>
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>
</div> </div>
</div>
<a-textarea <SmartsheetExpandedFormRichComment
v-if="log.id === editLog?.id" v-if="comment.id === editComment?.id"
:ref="focusInput" ref="editRef"
v-model:value="value" 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" autofocus
@keydown.stop="onKeyDown($event)" :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>
</div> </div>
</div> </div>
<div v-if="hasEditPermission" class="p-3 gap-2 flex"> <div v-if="hasEditPermission" class="bg-gray-50 nc-comment-input !rounded-br-2xl gap-2 flex">
<div class="flex flex-row w-full items-end gap-2"> <SmartsheetExpandedFormRichComment
<div class="expanded-form-comment-input-wrapper"> ref="commentInputRef"
<a-textarea v-model:value="newComment"
:ref="focusCommentInput" :hide-options="false"
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"
placeholder="Comment..." 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" data-testid="expanded-form-comment-input"
@focus="isExpandedFormCommentMode = false"
@keydown.stop @keydown.stop
@save="saveComment"
@keydown.enter.exact.prevent="saveComment" @keydown.enter.exact.prevent="saveComment"
/> />
</div> </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>
</div> </div>
</a-tab-pane> </a-tab-pane>
@ -304,7 +334,7 @@ watch(commentsWrapperEl, () => {
'pb-1': !appInfo.ee, '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" /> <GeneralLoader class="!mt-16" size="xlarge" />
</div> </div>
@ -318,14 +348,14 @@ watch(commentsWrapperEl, () => {
</div> </div>
</template> </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"> <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-1 flex flex-col gap-1 max-w-[calc(100%_-_24px)]">
<div class="flex flex-wrap items-center min-h-7"> <div class="flex flex-wrap items-center min-h-7">
<NcTooltip class="truncate max-w-42 mr-2" show-on-truncate-only> <NcTooltip class="truncate max-w-42 mr-2" show-on-truncate-only>
<template #title> <template #title>
{{ log.display_name?.trim() || log.user || 'Shared source' }} {{ audit.display_name?.trim() || audit.user || 'Shared source' }}
</template> </template>
<span <span
class="text-ellipsis overflow-hidden font-bold text-gray-800" class="text-ellipsis overflow-hidden font-bold text-gray-800"
@ -335,14 +365,14 @@ watch(commentsWrapperEl, () => {
display: 'inline', display: 'inline',
}" }"
> >
{{ log.display_name?.trim() || log.user || 'Shared source' }} {{ audit.display_name?.trim() || audit.user || 'Shared source' }}
</span> </span>
</NcTooltip> </NcTooltip>
<div v-if="log.id !== editLog?.id" class="text-xs text-gray-400"> <div class="text-xs text-gray-400">
{{ timeAgo(log.created_at) }} {{ timeAgo(audit.created_at) }}
</div> </div>
</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> </div>
</div> </div>
@ -358,6 +388,12 @@ watch(commentsWrapperEl, () => {
@apply max-w-1/2; @apply max-w-1/2;
} }
.nc-comment-input {
:deep(.nc-comment-rich-editor) {
@apply !ml-1;
}
}
.nc-audit-item { .nc-audit-item {
@apply border-b-1 gap-3 border-gray-200; @apply border-b-1 gap-3 border-gray-200;
} }
@ -386,10 +422,11 @@ watch(commentsWrapperEl, () => {
} }
:deep(.ant-tabs) { :deep(.ant-tabs) {
@apply !overflow-visible;
.ant-tabs-nav { .ant-tabs-nav {
@apply px-3; @apply px-3;
.ant-tabs-nav-list { .ant-tabs-nav-list {
@apply w-[calc(100%_-_24px)] gap-6; @apply w-[99%] mx-auto gap-6;
.ant-tabs-tab { .ant-tabs-tab {
@apply flex-1 flex items-center justify-center pt-3 pb-2.5; @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) { :deep(.expanded-form-comment-input) {
@apply transition-all duration-150; @apply transition-all duration-150 min-h-8;
box-shadow: none; box-shadow: none;
&:focus { &:focus,
&:focus-within {
@apply min-h-16; @apply min-h-16;
} }
&::placeholder { &::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, saveRowAndStay,
row: _row, row: _row,
save: _save, save: _save,
loadCommentsAndLogs, loadComments,
loadAudits,
clearColumns, clearColumns,
} = useProvideExpandedFormStore(meta, row) } = useProvideExpandedFormStore(meta, row)
@ -320,13 +321,13 @@ onMounted(async () => {
if (props.loadRow) { if (props.loadRow) {
await _loadRow() await _loadRow()
await loadCommentsAndLogs() await Promise.all([loadComments(), loadAudits()])
} }
if (props.rowId) { if (props.rowId) {
try { try {
await _loadRow(props.rowId) await _loadRow(props.rowId)
await loadCommentsAndLogs() await Promise.all([loadComments(), loadAudits()])
} catch (e: any) { } catch (e: any) {
if (e.response?.status === 404) { if (e.response?.status === 404) {
message.error(t('msg.noRecordFound')) message.error(t('msg.noRecordFound'))
@ -458,7 +459,7 @@ const onConfirmDeleteRowClick = async () => {
watch(rowId, async (nRow) => { watch(rowId, async (nRow) => {
await _loadRow(nRow) await _loadRow(nRow)
await loadCommentsAndLogs() await Promise.all([loadComments(), loadAudits()])
}) })
const showRightSections = computed(() => { const showRightSections = computed(() => {
@ -562,7 +563,7 @@ export default {
<div <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" 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"> <div class="flex gap-2">
<NcTooltip v-if="props.showNextPrevIcons"> <NcTooltip v-if="props.showNextPrevIcons">
<template #title> {{ renderAltOrOptlKey() }} + </template> <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"> <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> <template #title>
{{ meta.title }} {{ meta.title }}
</template> </template>
@ -617,7 +618,7 @@ export default {
{{ props.newRecordHeader ?? $t('activity.newRecord') }} {{ props.newRecordHeader ?? $t('activity.newRecord') }}
</div> </div>
<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" 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"> <span class="truncate">
@ -627,6 +628,21 @@ export default {
</div> </div>
</div> </div>
<div class="flex gap-2"> <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 <NcButton
v-if="!isNew && rowId && !isMobileMode" v-if="!isNew && rowId && !isMobileMode"
:disabled="isLoading" :disabled="isLoading"
@ -645,21 +661,6 @@ export default {
{{ isRecordLinkCopied ? $t('labels.copiedRecordURL') : $t('labels.copyRecordURL') }} {{ isRecordLinkCopied ? $t('labels.copiedRecordURL') : $t('labels.copyRecordURL') }}
</div> </div>
</NcButton> </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"> <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"> <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'" /> <GeneralIcon icon="threeDotVertical" class="text-md" :class="isLoading ? 'text-gray-300' : 'text-gray-700'" />
@ -720,7 +721,7 @@ export default {
</NcButton> </NcButton>
</div> </div>
</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 <div
:class="{ :class="{
'w-full': !showRightSections, 'w-full': !showRightSections,
@ -730,7 +731,7 @@ export default {
> >
<div <div
ref="expandedFormScrollWrapper" 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 <div
v-for="(col, i) of fields" v-for="(col, i) of fields"
@ -937,7 +938,7 @@ export default {
<div <div
v-if="showRightSections" v-if="showRightSections"
:class="{ active: commentsDrawer && isUIAllowed('commentList') }" :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" /> <SmartsheetExpandedFormComments :loading="isLoading" />
</div> </div>
@ -981,10 +982,6 @@ export default {
.nc-drawer-expanded-form { .nc-drawer-expanded-form {
@apply xs:my-0; @apply xs:my-0;
.ant-modal-content {
@apply overflow-hidden;
}
.ant-drawer-content-wrapper { .ant-drawer-content-wrapper {
@apply !h-[90vh]; @apply !h-[90vh];
.ant-drawer-content { .ant-drawer-content {
@ -1024,14 +1021,18 @@ export default {
} }
.nc-data-cell { .nc-data-cell {
box-shadow: 0 0 1px rgba(0, 0, 0, 0.1); @apply !rounded-lg;
&:hover, transition: all 0.3s;
&:hover {
@apply !border-1 !border-brand-400;
}
&:focus-within { &: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 { .nc-data-cell:focus-within {
@apply !border-1 !border-brand-500 !rounded-lg; @apply !border-1 !border-brand-500;
} }
:deep(.nc-system-field input) { :deep(.nc-system-field input) {

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

@ -1136,6 +1136,9 @@ const calculateSlices = () => {
start: 0, start: 0,
end: 0, end: 0,
} }
// try again until the grid is rendered
setTimeout(calculateSlices, 100)
return 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 isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isExpandedBulkUpdateForm = inject(IsExpandedBulkUpdateFormOpenInj, ref(false))
const isDropDownOpen = ref(false) const isDropDownOpen = ref(false)
const isKanban = inject(IsKanbanInj, ref(false)) const isKanban = inject(IsKanbanInj, ref(false))
@ -58,7 +60,7 @@ const closeAddColumnDropdown = () => {
} }
const openHeaderMenu = (e?: MouseEvent) => { 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) { if (!isForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value) {
editColumnDropdown.value = true editColumnDropdown.value = true
@ -83,7 +85,7 @@ const onClick = (e: Event) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
} else { } else {
if (isExpandedForm.value && !editColumnDropdown.value) { if (isExpandedForm.value && !editColumnDropdown.value && !isExpandedBulkUpdateForm.value) {
isDropDownOpen.value = true isDropDownOpen.value = true
return return
} }
@ -99,9 +101,10 @@ const onClick = (e: Event) => {
:class="{ :class="{
'h-full': column, 'h-full': column,
'!text-gray-400': isKanban, '!text-gray-400': isKanban,
'flex-col !items-start justify-center': isExpandedForm && !isMobileMode, 'flex-col !items-start justify-center pt-0.5': isExpandedForm && !isMobileMode && !isExpandedBulkUpdateForm,
'cursor-pointer hover:bg-gray-100': isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit'), 'cursor-pointer hover:bg-gray-100':
'bg-gray-100': isExpandedForm ? editColumnDropdown || isDropDownOpen : false, isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit') && !isExpandedBulkUpdateForm,
'bg-gray-100': isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false,
}" }"
@dblclick="openHeaderMenu" @dblclick="openHeaderMenu"
@click.right="openDropDown" @click.right="openDropDown"
@ -111,7 +114,7 @@ const onClick = (e: Event) => {
class="nc-cell-name-wrapper flex-1 flex items-center" class="nc-cell-name-wrapper flex-1 flex items-center"
:class="{ :class="{
'max-w-[calc(100%_-_23px)]': !isExpandedForm, 'max-w-[calc(100%_-_23px)]': !isExpandedForm,
'max-w-full': isExpandedForm, 'max-w-full': isExpandedForm && !isExpandedBulkUpdateForm,
}" }"
> >
<template v-if="column && !props.hideIcon"> <template v-if="column && !props.hideIcon">
@ -119,7 +122,7 @@ const onClick = (e: Event) => {
v-if="isGrid" v-if="isGrid"
class="flex items-center" class="flex items-center"
placement="bottom" placement="bottom"
:disabled="isExpandedForm ? editColumnDropdown || isDropDownOpen : false" :disabled="isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false"
> >
<template #title> {{ columnTypeName }} </template> <template #title> {{ columnTypeName }} </template>
<SmartsheetHeaderCellIcon <SmartsheetHeaderCellIcon
@ -138,21 +141,21 @@ const onClick = (e: Event) => {
<NcTooltip <NcTooltip
v-if="column" v-if="column"
:class="{ :class="{
'cursor-pointer pt-0.25': !isForm && isUIAllowed('fieldEdit') && !hideMenu, 'cursor-pointer': !isForm && isUIAllowed('fieldEdit') && !hideMenu,
'cursor-default': isForm || !isUIAllowed('fieldEdit') || hideMenu, 'cursor-default': isForm || !isUIAllowed('fieldEdit') || hideMenu,
'truncate': !isForm, 'truncate': !isForm,
}" }"
class="name pl-1 max-w-full" class="name pl-1 max-w-full"
placement="bottom" placement="bottom"
show-on-truncate-only show-on-truncate-only
:disabled="isExpandedForm ? editColumnDropdown || isDropDownOpen : false" :disabled="isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false"
> >
<template #title> {{ column.title }} </template> <template #title> {{ column.title }} </template>
<span <span
:data-test-id="column.title" :data-test-id="column.title"
:class="{ :class="{
'select-none': isExpandedForm, 'select-none': isExpandedForm && !isExpandedBulkUpdateForm,
}" }"
> >
{{ column.title }} {{ column.title }}
@ -162,9 +165,9 @@ const onClick = (e: Event) => {
<span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span> <span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span>
<GeneralIcon <GeneralIcon
v-if="isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit')" v-if="isExpandedForm && !isExpandedBulkUpdateForm && !isMobileMode && isUIAllowed('fieldEdit')"
icon="arrowDown" icon="arrowDown"
class="flex-none text-grey h-full text-grey cursor-pointer ml-1 group-hover:visible" class="flex-none cursor-pointer ml-1 group-hover:visible w-4 h-4"
:class="{ :class="{
visible: editColumnDropdown || isDropDownOpen, visible: editColumnDropdown || isDropDownOpen,
invisible: !(editColumnDropdown || isDropDownOpen), invisible: !(editColumnDropdown || isDropDownOpen),
@ -187,10 +190,10 @@ const onClick = (e: Event) => {
v-model:visible="editColumnDropdown" v-model:visible="editColumnDropdown"
class="h-full" class="h-full"
:trigger="['click']" :trigger="['click']"
:placement="isExpandedForm ? 'bottomLeft' : 'bottomRight'" :placement="isExpandedForm && !isExpandedBulkUpdateForm ? 'bottomLeft' : 'bottomRight'"
overlay-class-name="nc-dropdown-edit-column" 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 /> <div v-else />
<template #overlay> <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 isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isExpandedBulkUpdateForm = inject(IsExpandedBulkUpdateFormOpenInj, ref(false))
const colOptions = computed(() => column.value?.colOptions) const colOptions = computed(() => column.value?.colOptions)
const tableTile = computed(() => meta?.value?.title) const tableTile = computed(() => meta?.value?.title)
@ -140,7 +142,7 @@ const closeAddColumnDropdown = () => {
} }
const openHeaderMenu = (e?: MouseEvent) => { 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) { if (!isForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value) {
editColumnDropdown.value = true editColumnDropdown.value = true
@ -165,7 +167,7 @@ const onClick = (e: Event) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
} else { } else {
if (isExpandedForm.value && !editColumnDropdown.value) { if (isExpandedForm.value && !editColumnDropdown.value && !isExpandedBulkUpdateForm.value) {
isDropDownOpen.value = true isDropDownOpen.value = true
return return
} }
@ -179,9 +181,10 @@ const onClick = (e: Event) => {
<div <div
class="flex items-center w-full h-full text-small text-gray-500 font-weight-medium group" class="flex items-center w-full h-full text-small text-gray-500 font-weight-medium group"
:class="{ :class="{
'flex-col !items-start justify-center': isExpandedForm, 'flex-col !items-start justify-center pt-0.5': isExpandedForm && !isMobileMode && !isExpandedBulkUpdateForm,
'bg-gray-100': isExpandedForm ? editColumnDropdown || isDropDownOpen : false, 'bg-gray-100': isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false,
'cursor-pointer hover:bg-gray-100': isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit'), 'cursor-pointer hover:bg-gray-100':
isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit') && !isExpandedBulkUpdateForm,
}" }"
@dblclick="openHeaderMenu" @dblclick="openHeaderMenu"
@click.right="openDropDown" @click.right="openDropDown"
@ -191,7 +194,7 @@ const onClick = (e: Event) => {
class="nc-virtual-cell-name-wrapper flex-1 flex items-center" class="nc-virtual-cell-name-wrapper flex-1 flex items-center"
:class="{ :class="{
'max-w-[calc(100%_-_23px)]': !isExpandedForm, 'max-w-[calc(100%_-_23px)]': !isExpandedForm,
'max-w-full': isExpandedForm, 'max-w-full': isExpandedForm && !isExpandedBulkUpdateForm,
}" }"
> >
<template v-if="column && !props.hideIcon"> <template v-if="column && !props.hideIcon">
@ -208,7 +211,7 @@ const onClick = (e: Event) => {
<span <span
:data-test-id="column.title" :data-test-id="column.title"
:class="{ :class="{
'select-none': isExpandedForm, 'select-none': isExpandedForm && !isExpandedBulkUpdateForm,
}" }"
> >
{{ column.title }} {{ column.title }}
@ -218,9 +221,9 @@ const onClick = (e: Event) => {
<span v-if="isVirtualColRequired(column, meta?.columns || []) || required" class="text-red-500">&nbsp;*</span> <span v-if="isVirtualColRequired(column, meta?.columns || []) || required" class="text-red-500">&nbsp;*</span>
<GeneralIcon <GeneralIcon
v-if="isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit')" v-if="isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit') && !isExpandedBulkUpdateForm"
icon="arrowDown" 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="{ :class="{
visible: editColumnDropdown || isDropDownOpen, visible: editColumnDropdown || isDropDownOpen,
invisible: !(editColumnDropdown || isDropDownOpen), invisible: !(editColumnDropdown || isDropDownOpen),
@ -244,10 +247,10 @@ const onClick = (e: Event) => {
v-model:visible="editColumnDropdown" v-model:visible="editColumnDropdown"
class="h-full" class="h-full"
:trigger="['click']" :trigger="['click']"
:placement="isExpandedForm ? 'bottomLeft' : 'bottomRight'" :placement="isExpandedForm && !isExpandedBulkUpdateForm ? 'bottomLeft' : 'bottomRight'"
overlay-class-name="nc-dropdown-edit-column" 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 /> <div v-else />
<template #overlay> <template #overlay>
<SmartsheetColumnEditOrAddProvider <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 // We keep the calendar range here and update it when the user selects a new range
const _calendar_ranges = ref<CalendarRangeType[]>(calendarRange.value) 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 () => { const saveCalendarRanges = async () => {
if (activeView.value) { if (activeView.value) {
try { try {
@ -56,8 +76,12 @@ const saveCalendarRanges = async () => {
await $api.dbView.calendarUpdate(activeView.value?.id as string, { await $api.dbView.calendarUpdate(activeView.value?.id as string, {
calendar_range: calRanges as CalendarRangeType[], calendar_range: calRanges as CalendarRangeType[],
}) })
if (activeView.value.view) activeView.value.view.calendar_range = calRanges
await loadCalendarMeta() await loadCalendarMeta()
await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()]) await Promise.all([loadCalendarData(), loadSidebarData(), fetchActiveDates()])
calendarRangeDropdown.value = false
} catch (e) { } catch (e) {
console.log(e) console.log(e)
message.error('There was an error while updating view!') 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) => { const removeRange = async (id: number) => {
_calendar_ranges.value = _calendar_ranges.value.filter((_, i) => i !== id) _calendar_ranges.value = _calendar_ranges.value.filter((_, i) => i !== id)
@ -235,22 +252,14 @@ const saveCalendarRange = async (range: CalendarRangeType, value?) => {
</NcButton> </NcButton>
--> -->
</div> </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> <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> </div>
</template> </template>
</NcDropdown> </NcDropdown>

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

@ -11,6 +11,7 @@ interface Props {
modelValue?: undefined | Filter[] modelValue?: undefined | Filter[]
webHook?: boolean webHook?: boolean
draftFilter?: Partial<FilterType> draftFilter?: Partial<FilterType>
isOpen?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -27,6 +28,7 @@ const emit = defineEmits(['update:filtersLength', 'update:draftFilter', 'update:
const excludedFilterColUidt = [UITypes.QrCode, UITypes.Barcode] const excludedFilterColUidt = [UITypes.QrCode, UITypes.Barcode]
const draftFilter = useVModel(props, 'draftFilter', emit) const draftFilter = useVModel(props, 'draftFilter', emit)
const modelValue = useVModel(props, 'modelValue', emit) const modelValue = useVModel(props, 'modelValue', emit)
const { nestedLevel, parentId, autoSave, hookId, showLoading, webHook } = toRefs(props) const { nestedLevel, parentId, autoSave, hookId, showLoading, webHook } = toRefs(props)
@ -386,6 +388,15 @@ watch(
immediate: true, immediate: true,
}, },
) )
const addFilterBtnRef = ref()
watchEffect(() => {
if (props.isOpen && !nested.value && addFilterBtnRef.value) {
setTimeout(() => {
addFilterBtnRef.value?.$el?.focus()
}, 10)
}
})
</script> </script>
<template> <template>
@ -394,13 +405,63 @@ watch(
:class="{ :class="{
'max-h-[max(80vh,500px)] min-w-112 py-2 pl-4': !nested, 'max-h-[max(80vh,500px)] min-w-112 py-2 pl-4': !nested,
'w-full ': 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 <div
v-if="filters && filters.length" v-if="filters && filters.length"
ref="wrapperDomRef" 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 }" :class="{ 'max-h-420px nc-scrollbar-thin nc-filter-top-wrapper pr-4 my-2 py-1': !nested }"
@click.stop @click.stop
> >
@ -408,17 +469,29 @@ watch(
<template v-if="filter.status !== 'delete'"> <template v-if="filter.status !== 'delete'">
<template v-if="filter.is_group"> <template v-if="filter.is_group">
<div class="flex flex-col w-full gap-y-2"> <div class="flex flex-col w-full gap-y-2">
<div class="flex flex-row w-full justify-between items-center"> <div class="flex rounded-lg p-2 w-full border-1" :class="[`nc-filter-nested-level-${nestedLevel}`]">
<span v-if="!i" class="flex items-center ml-2">{{ $t('labels.where') }}</span> <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"> <div v-else :key="`${i}nested`" class="flex nc-filter-logical-op">
<NcSelect <NcSelect
v-model:value="filter.logical_op" v-model:value="filter.logical_op"
v-e="['c:filter:logical-op:select']" v-e="['c:filter:logical-op:select']"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"
class="min-w-20 capitalize" class="min-w-18 max-w-18 capitalize"
placeholder="Group op" placeholder="Group op"
dropdown-class-name="nc-dropdown-filter-logical-op-group" 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 @click.stop
@change="onLogicalOpUpdate(filter, i)" @change="onLogicalOpUpdate(filter, i)"
> >
@ -435,6 +508,8 @@ watch(
</a-select-option> </a-select-option>
</NcSelect> </NcSelect>
</div> </div>
</template>
<template #end>
<NcButton <NcButton
v-if="!filter.readOnly" v-if="!filter.readOnly"
:key="i" :key="i"
@ -446,33 +521,29 @@ watch(
> >
<component :is="iconMap.deleteListItem" /> <component :is="iconMap.deleteListItem" />
</NcButton> </NcButton>
</div> </template>
<div class="flex border-1 rounded-lg p-2 w-full" :class="nestedLevel % 2 !== 0 ? 'bg-white' : 'bg-gray-100'"> </LazySmartsheetToolbarColumnFilter>
<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"
/>
</div> </div>
</div> </div>
</template> </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 <NcSelect
v-else v-else
v-model:value="filter.logical_op" v-model:value="filter.logical_op"
v-e="['c:filter:logical-op:select']" v-e="['c:filter:logical-op:select']"
:dropdown-match-select-width="false" :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 hide-details
:disabled="filter.readOnly || (visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed)" :disabled="filter.readOnly || (visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed)"
dropdown-class-name="nc-dropdown-filter-logical-op" dropdown-class-name="nc-dropdown-filter-logical-op"
:class="{
'nc-disabled-logical-op': filter.readOnly || (visibleFilters.indexOf(filter) > 1 && !isLogicalOpChangeAllowed),
}"
@change="onLogicalOpUpdate(filter, i)" @change="onLogicalOpUpdate(filter, i)"
@click.stop @click.stop
> >
@ -488,6 +559,7 @@ watch(
</div> </div>
</a-select-option> </a-select-option>
</NcSelect> </NcSelect>
<SmartsheetToolbarFieldListAutoCompleteDropdown <SmartsheetToolbarFieldListAutoCompleteDropdown
:key="`${i}_6`" :key="`${i}_6`"
v-model="filter.fk_column_id" v-model="filter.fk_column_id"
@ -497,6 +569,7 @@ watch(
@click.stop @click.stop
@change="selectFilterField(filter, i)" @change="selectFilterField(filter, i)"
/> />
<NcSelect <NcSelect
v-model:value="filter.comparison_op" v-model:value="filter.comparison_op"
v-e="['c:filter:comparison-op:select']" v-e="['c:filter:comparison-op:select']"
@ -529,6 +602,7 @@ watch(
</NcSelect> </NcSelect>
<div v-if="['blank', 'notblank'].includes(filter.comparison_op)" class="flex flex-grow"></div> <div v-if="['blank', 'notblank'].includes(filter.comparison_op)" class="flex flex-grow"></div>
<NcSelect <NcSelect
v-else-if="isDateType(types[filter.fk_column_id])" v-else-if="isDateType(types[filter.fk_column_id])"
v-model:value="filter.comparison_sub_op" v-model:value="filter.comparison_sub_op"
@ -567,6 +641,7 @@ watch(
</a-select-option> </a-select-option>
</template> </template>
</NcSelect> </NcSelect>
<a-checkbox <a-checkbox
v-if="filter.field && types[filter.field] === 'boolean'" v-if="filter.field && types[filter.field] === 'boolean'"
v-model:checked="filter.value" v-model:checked="filter.value"
@ -583,6 +658,7 @@ watch(
@update-filter-value="(value) => updateFilterValue(value, filter, i)" @update-filter-value="(value) => updateFilterValue(value, filter, i)"
@click.stop @click.stop
/> />
<div v-else-if="!isDateType(types[filter.fk_column_id])" class="flex-grow"></div> <div v-else-if="!isDateType(types[filter.fk_column_id])" class="flex-grow"></div>
<NcButton <NcButton
@ -600,16 +676,16 @@ watch(
</template> </template>
</div> </div>
<template v-if="!nested">
<template v-if="isEeUI && !isPublic"> <template v-if="isEeUI && !isPublic">
<div <div
v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)" v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)"
ref="addFiltersRowDomRef"
class="flex gap-2" class="flex gap-2"
:class="{ :class="{
'mt-1 mb-2': filters.length, '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"> <div class="flex items-center gap-1">
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
<!-- Add Filter --> <!-- Add Filter -->
@ -617,7 +693,7 @@ watch(
</div> </div>
</NcButton> </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"> <div class="flex items-center gap-1">
<!-- Add Filter Group --> <!-- Add Filter Group -->
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
@ -626,6 +702,7 @@ watch(
</NcButton> </NcButton>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div <div
ref="addFiltersRowDomRef" ref="addFiltersRowDomRef"
@ -634,7 +711,7 @@ watch(
'mt-1 mb-2': filters.length, '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"> <div class="flex items-center gap-1">
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
<!-- Add Filter --> <!-- Add Filter -->
@ -642,7 +719,13 @@ watch(
</div> </div>
</NcButton> </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"> <div class="flex items-center gap-1">
<!-- Add Filter Group --> <!-- Add Filter Group -->
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
@ -651,6 +734,7 @@ watch(
</NcButton> </NcButton>
</div> </div>
</template> </template>
</template>
<div <div
v-if="!filters.length" v-if="!filters.length"
class="flex flex-row text-gray-400 mt-2" class="flex flex-row text-gray-400 mt-2"
@ -666,7 +750,7 @@ watch(
</div> </div>
</template> </template>
<style scoped> <style scoped lang="scss">
.nc-filter-item-remove-btn { .nc-filter-item-remove-btn {
@apply text-gray-600 hover:text-gray-800; @apply text-gray-600 hover:text-gray-800;
} }
@ -680,6 +764,112 @@ watch(
} }
:deep(.ant-select-selector) { :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> </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" class="nc-table-toolbar-menu"
:auto-save="true" :auto-save="true"
data-testid="nc-filter-menu" data-testid="nc-filter-menu"
:is-open="open"
@update:filters-length="filtersLength = $event" @update:filters-length="filtersLength = $event"
> >
</SmartsheetToolbarColumnFilter> </SmartsheetToolbarColumnFilter>

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

@ -63,14 +63,15 @@ const options = computed<SelectProps['options']>(() =>
return !isVirtualSystemField 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, value: c.id,
label: c.title, label: c.title,
icon: h( icon: h(
isVirtualCol(c) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), isVirtualCol(c) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'),
{ { columnMeta: c },
columnMeta: c,
},
), ),
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 reloadViewDataHook = inject(ReloadViewDataHookInj, undefined)!
const rootFields = inject(FieldsInj)
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
@ -23,7 +21,6 @@ const { $api, $e } = useNuxtApp()
const { const {
showSystemFields, showSystemFields,
sortedAndFilteredFields,
fields, fields,
filteredFieldList, filteredFieldList,
filterQuery, 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 numberOfHiddenFields = computed(() => filteredFieldList.value?.filter((field) => !field.show)?.length)
const gridDisplayValueField = computed(() => { 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 activeTab = toRef(props, 'activeTab')
const fields = ref<ColumnType[]>([])
const route = useRoute() const route = useRoute()
const meta = computed<TableType | undefined>(() => { const meta = computed<TableType | undefined>(() => {
@ -50,7 +48,6 @@ provide(IsLockedInj, isLocked)
provide(ReloadViewDataHookInj, reloadViewDataEventHook) provide(ReloadViewDataHookInj, reloadViewDataEventHook)
provide(ReloadViewMetaHookInj, reloadViewMetaEventHook) provide(ReloadViewMetaHookInj, reloadViewMetaEventHook)
provide(OpenNewRecordFormHookInj, openNewRecordFormHook) provide(OpenNewRecordFormHookInj, openNewRecordFormHook)
provide(FieldsInj, fields)
provide(IsFormInj, isForm) provide(IsFormInj, isForm)
provide(TabMetaInj, activeTab) provide(TabMetaInj, activeTab)
provide( provide(
@ -60,6 +57,7 @@ provide(
useExpandedFormDetachedProvider() useExpandedFormDetachedProvider()
useProvideViewColumns(activeView, meta, () => reloadViewDataEventHook?.trigger()) useProvideViewColumns(activeView, meta, () => reloadViewDataEventHook?.trigger())
useProvideViewGroupBy(activeView, meta, xWhere) useProvideViewGroupBy(activeView, meta, xWhere)
useProvideSmartsheetLtarHelpers(meta) useProvideSmartsheetLtarHelpers(meta)

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

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

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

@ -6,9 +6,11 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
( (
view: Ref<ViewType | undefined>, view: Ref<ViewType | undefined>,
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>, meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
reloadData?: () => void, reloadData?: (params?: { shouldShowLoading?: boolean }) => void,
isPublic = false, isPublic = false,
) => { ) => {
const rootFields = ref<ColumnType[]>([])
const fields = ref<Field[]>() const fields = ref<Field[]>()
const filterQuery = ref('') 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 { return {
fields, fields,
loadViewColumns, 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) => comparison_op: comparisonOpList(options.value?.[0].uidt as UITypes).filter((compOp) =>
isComparisonOpAllowed({ fk_column_id: options.value?.[0].id }, compOp), isComparisonOpAllowed({ fk_column_id: options.value?.[0].id }, compOp),
)?.[0].value as FilterType['comparison_op'], )?.[0].value as FilterType['comparison_op'],
value: '', value: null,
status: 'create', status: 'create',
logical_op: logicalOps.size === 1 ? logicalOps.values().next().value : 'and', 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 IsKanbanInj: InjectionKey<Ref<boolean>> = Symbol('is-kanban-injection')
export const IsLockedInj: InjectionKey<Ref<boolean>> = Symbol('is-locked-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 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 CellValueInj: InjectionKey<Ref<any>> = Symbol('cell-value-injection')
export const ActiveViewInj: InjectionKey<Ref<ViewType>> = Symbol('active-view-injection') export const ActiveViewInj: InjectionKey<Ref<ViewType>> = Symbol('active-view-injection')
export const ReadonlyInj: InjectionKey<Ref<boolean>> = Symbol('readonly-injection') export const ReadonlyInj: InjectionKey<Ref<boolean>> = Symbol('readonly-injection')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

16
packages/nc-gui/package.json

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

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

@ -24,5 +24,29 @@ export const timeAgo = (date: any) => {
date += '+00:00' date += '+00:00'
} }
// show in local time // 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') { if (typeof value === 'object') {
for (let _ in value) return true if (Object.keys(value).length > 0) {
return true
}
return false 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 NcItalic from '~icons/nc-icons/italic'
import NcBold from '~icons/nc-icons/bold' import NcBold from '~icons/nc-icons/bold'
import NcUnderline from '~icons/nc-icons/underline' import NcUnderline from '~icons/nc-icons/underline'
import NcCrop from '~icons/nc-icons/crop'
import NcLink from '~icons/nc-icons/link' 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 NcControlPanel from '~icons/nc-icons/control-panel'
import NcHome from '~icons/nc-icons/home' import NcHome from '~icons/nc-icons/home'
import NcWorkspace from '~icons/nc-icons/workspace' import NcWorkspace from '~icons/nc-icons/workspace'
@ -337,6 +339,8 @@ import NcMessageCircle from '~icons/nc-icons/message-circle'
} as const */ } as const */
export const iconMap = { export const iconMap = {
strike: NcStrike,
atSign: NcAtSign,
slash: NcSlash, slash: NcSlash,
arrowUpRight: NcArrowUpRight, arrowUpRight: NcArrowUpRight,
ncWorkspace: NcWorkspace, 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` - `NC_REDIS_URL`
| Variable | Description | If absent | | 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` | 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` | 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. | | | `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_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`. | | `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. | | | `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_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_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.* | | `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_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_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_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` 2. **Help Text** `Optional`
3. **Required** : Toggle to mark the field as required 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) ![Field Label & Help Text](/img/v2/views/form-view/field-config.png)
:::info :::info
Formatting options are supported for the `Help Text` field. You can also use Markdown to format the text. Formatting options are supported for the `Help Text` field. You can also use Markdown to format the text.
::: :::
### Field Type Specific Settings ### Select Field Type
For select based field types (`Single-Select`, `Multi-Select`, `User`), you can configure the following additional settings:
#### 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 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 `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. - Use `Reorder` button associated with a field to reorder the options.
![Limit options](/img/v2/views/form-view/limit-options.png) ![Limit options](/img/v2/views/form-view/limit-options.png)
#### Options Layout ## Field Validations ☁
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) :::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 ## Prefill Form Fields
Here's a more professional rephrasing of the given content: 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 * Description for the target row
* @example This is the comment for the row * @example This is the comment for the row
*/ */
description?: string; comment?: string;
/** /**
* Foreign Key to Model * Foreign Key to Model
* @example md_ehn5izr99m7d45 * @example md_ehn5izr99m7d45
@ -561,7 +561,12 @@ export interface CommentUpdateReqType {
* Description for the target row * Description for the target row
* @example This is the comment for the 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; 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 { export interface ExtensionType {
/** Unique ID */ /** Unique ID */
id?: IdType; id?: IdType;
@ -9671,12 +9814,12 @@ export class Api<
}; };
utils = { utils = {
/** /**
* @description List all comments * @description List all audits
* *
* @tags Utils * @tags Utils
* @name CommentList * @name AuditList
* @summary List Comments in Audit * @summary List Audits
* @request GET:/api/v1/db/meta/audits/comments * @request GET:/api/v1/db/meta/audits
* @response `200` `{ * @response `200` `{
list: (AuditType)[], list: (AuditType)[],
@ -9687,7 +9830,7 @@ export class Api<
}` }`
*/ */
commentList: ( auditList: (
query: { query: {
/** /**
* Row ID * Row ID
@ -9699,24 +9842,67 @@ export class Api<
* @example md_c6csq89tl37jm5 * @example md_c6csq89tl37jm5
*/ */
fk_model_id: IdType; 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? * @description List all comments
* @example true *
* @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 = {} params: RequestParams = {}
) => ) =>
this.request< this.request<
{ {
list: AuditType[]; list: CommentType[];
}, },
{ {
/** @example BadRequest [Error]: <ERROR MESSAGE> */ /** @example BadRequest [Error]: <ERROR MESSAGE> */
msg: string; msg: string;
} }
>({ >({
path: `/api/v1/db/meta/audits/comments`, path: `/api/v1/db/meta/comments`,
method: 'GET', method: 'GET',
query: query, query: query,
format: 'json', 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 * @tags Utils
* @name CommentRow * @name CommentRow
* @summary Comment Rows * @summary Comment Rows
* @request POST:/api/v1/db/meta/audits/comments * @request POST:/api/v1/db/meta/comments
* @response `200` `AuditType` OK * @response `200` `CommentType` OK
* @response `400` `{ * @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\ \** @example BadRequest [Error]: <ERROR MESSAGE> *\
msg: string, msg: string,
@ -9739,13 +9925,13 @@ export class Api<
*/ */
commentRow: (data: CommentReqType, params: RequestParams = {}) => commentRow: (data: CommentReqType, params: RequestParams = {}) =>
this.request< this.request<
AuditType, CommentType,
{ {
/** @example BadRequest [Error]: <ERROR MESSAGE> */ /** @example BadRequest [Error]: <ERROR MESSAGE> */
msg: string; msg: string;
} }
>({ >({
path: `/api/v1/db/meta/audits/comments`, path: `/api/v1/db/meta/comments`,
method: 'POST', method: 'POST',
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
@ -9754,21 +9940,21 @@ export class Api<
}), }),
/** /**
* @description Update comment in Audit * @description Update comment
* *
* @tags Utils * @tags Utils
* @name CommentUpdate * @name CommentUpdate
* @summary Update Comment in Audit * @summary Update Comment
* @request PATCH:/api/v1/db/meta/audits/{auditId}/comment * @request PATCH:/api/v1/db/meta/comment/{commentId}/
* @response `200` `number` OK * @response `200` `number` OK
*/ */
commentUpdate: ( commentUpdate: (
auditId: string, commentId: string,
data: CommentUpdateReqType, data: CommentUpdateReqType,
params: RequestParams = {} params: RequestParams = {}
) => ) =>
this.request<number, any>({ this.request<number, any>({
path: `/api/v1/db/meta/audits/${auditId}/comment`, path: `/api/v1/db/meta/comment/${commentId}/`,
method: 'PATCH', method: 'PATCH',
body: data, body: data,
type: ContentType.Json, 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. * @description Return the number of comments in the given query.
* *
* @tags Utils * @tags Utils
* @name CommentCount * @name CommentCount
* @summary Count Comments * @summary Count Comments
* @request GET:/api/v1/db/meta/audits/comments/count * @request GET:/api/v1/db/meta/comments/count
* @response `200` `({ * @response `200` `({
\** \**
* The number of comments * The number of comments
@ -9829,7 +10033,7 @@ export class Api<
msg: string; msg: string;
} }
>({ >({
path: `/api/v1/db/meta/audits/comments/count`, path: `/api/v1/db/meta/comments/count`,
method: 'GET', method: 'GET',
query: query, query: query,
format: 'json', format: 'json',

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

@ -137,3 +137,16 @@ export const timeAgo = (date: any) => {
// show in local time // show in local time
return dayjs(date).fromNow(); 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_CREATE = 'extension.create',
EXTENSION_UPDATE = 'extension.update', EXTENSION_UPDATE = 'extension.update',
EXTENSION_DELETE = 'extension.delete', EXTENSION_DELETE = 'extension.delete',
COMMENT_CREATE = 'comment.create',
COMMENT_DELETE = 'comment.delete',
COMMENT_UPDATE = 'comment.update',
} }
export enum ClickhouseTables { export enum ClickhouseTables {

6
packages/nocodb/Dockerfile

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

2
packages/nocodb/docker/litestream.yml

@ -5,7 +5,7 @@ dbs:
replicas: replicas:
- type: s3 - type: s3
endpoint: ${LITESTREAM_S3_ENDPOINT} endpoint: ${LITESTREAM_S3_ENDPOINT}
force-path-style: true region: ${LITESTREAM_S3_REGION}
skip-verify: ${LITESTREAM_S3_SKIP_VERIFY} skip-verify: ${LITESTREAM_S3_SKIP_VERIFY}
bucket: ${LITESTREAM_S3_BUCKET} bucket: ${LITESTREAM_S3_BUCKET}
path: ${LITESTREAM_S3_PATH} 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" mkdir -p "$NC_TOOL_DIR"
fi 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() { use_litestream() {
[ -z "${NC_DB}" ] \ [ -z "${NC_DB}" ] \
&& [ -z "${NC_DB_JSON}" ] \ && [ -z "${NC_DB_JSON}" ] \
@ -11,27 +26,48 @@ use_litestream() {
&& [ -z "${DATABASE_URL}" ] \ && [ -z "${DATABASE_URL}" ] \
&& [ -z "${DATABASE_URL_FILE}" ] \ && [ -z "${DATABASE_URL_FILE}" ] \
&& [ -z "${NC_MINIMAL_DBS}" ] \ && [ -z "${NC_MINIMAL_DBS}" ] \
&& [ -n "${LITESTREAM_S3_ENDPOINT}" ] \
&& [ -n "${LITESTREAM_S3_BUCKET}" ] \ && [ -n "${LITESTREAM_S3_BUCKET}" ] \
&& [ -n "${LITESTREAM_ACCESS_KEY_ID}" ] \ && [ -n "${LITESTREAM_S3_ACCESS_KEY_ID}" ] \
&& [ -n "${LITESTREAM_SECRET_ACCESS_KEY}" ] && [ -n "${LITESTREAM_S3_SECRET_ACCESS_KEY}" ]
} }
if use_litestream ; then 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 if [ -f "${NC_TOOL_DIR}noco.db" ] ; then
rm "${NC_TOOL_DIR}noco.db" rm "${NC_TOOL_DIR}noco.db"
rm -f "${NC_TOOL_DIR}noco.db-shm" rm -f "${NC_TOOL_DIR}noco.db-shm"
rm -f "${NC_TOOL_DIR}noco.db-wal" rm -f "${NC_TOOL_DIR}noco.db-wal"
fi fi
# restore DB from Litestream replica
litestream restore "${NC_TOOL_DIR}noco.db" litestream restore "${NC_TOOL_DIR}noco.db"
# create empty DB file if no Litestream replica exists
if [ ! -f "${NC_TOOL_DIR}noco.db" ] ; then if [ ! -f "${NC_TOOL_DIR}noco.db" ] ; then
touch "${NC_TOOL_DIR}noco.db" touch "${NC_TOOL_DIR}noco.db"
fi fi
# start Litestream replication
litestream replicate & litestream replicate &
fi fi
# start NocoDB
node docker/main.js 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 { EventEmitterModule as NestJsEventEmitter } from '@nestjs/event-emitter';
import { SentryModule } from '@ntegral/nestjs-sentry'; import { SentryModule } from '@ntegral/nestjs-sentry';
import type { MiddlewareConsumer } from '@nestjs/common'; 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 { GlobalExceptionFilter } from '~/filters/global-exception/global-exception.filter';
import { GlobalMiddleware } from '~/middlewares/global/global.middleware'; import { GlobalMiddleware } from '~/middlewares/global/global.middleware';
import { GuiMiddleware } from '~/middlewares/gui/gui.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 { 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 { JobsModule } from '~/modules/jobs/jobs.module';
import appConfig from '~/app.config'; import appConfig from '~/app.config';
import { ExtractIdsMiddleware } from '~/middlewares/extract-ids/extract-ids.middleware'; 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'; import { packageInfo } from '~/utils/packageVersion';
export const ceModuleConfig = { export const ceModuleConfig = {
imports: [ imports: [
GlobalModule,
UsersModule,
AuthModule, AuthModule,
MetasModule, NocoModule,
DatasModule,
EventEmitterModule, EventEmitterModule,
JobsModule, JobsModule,
NestJsEventEmitter.forRoot(), NestJsEventEmitter.forRoot(),
@ -54,7 +42,6 @@ export const ceModuleConfig = {
: []), : []),
], ],
providers: [ providers: [
AuthService,
{ {
provide: APP_FILTER, provide: APP_FILTER,
useClass: GlobalExceptionFilter, useClass: GlobalExceptionFilter,
@ -63,11 +50,6 @@ export const ceModuleConfig = {
provide: APP_GUARD, provide: APP_GUARD,
useClass: ExtractIdsMiddleware, useClass: ExtractIdsMiddleware,
}, },
LocalStrategy,
AuthTokenStrategy,
BaseViewStrategy,
HookHandlerService,
BasicStrategy,
], ],
}; };

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

@ -4,9 +4,7 @@ import {
Get, Get,
HttpCode, HttpCode,
Param, Param,
Patch,
Post, Post,
Query,
Req, Req,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
@ -22,14 +20,12 @@ import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
export class AuditsController { export class AuditsController {
constructor(private readonly auditsService: AuditsService) {} constructor(private readonly auditsService: AuditsService) {}
@Post(['/api/v1/db/meta/audits/comments', '/api/v2/meta/audits/comments']) @Get(['/api/v1/db/meta/audits/', '/api/v2/meta/audits/'])
@HttpCode(200) @Acl('auditList')
@Acl('commentRow') async auditListRow(@Req() req: Request) {
async commentRow(@Req() req: Request) { return new PagedResponseImpl(
return await this.auditsService.commentRow({ await this.auditsService.auditOnlyList({ query: req.query as any }),
user: (req as any).user, );
body: req.body,
});
} }
@Post([ @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([ @Get([
'/api/v1/db/meta/projects/:baseId/audits/', '/api/v1/db/meta/projects/:baseId/audits/',
'/api/v2/meta/bases/: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