Browse Source

Merge branch 'develop' into feat/yyyy-mm

pull/6870/head
աɨռɢӄաօռɢ 9 months ago
parent
commit
435edfd372
  1. 4
      markdown/readme/languages/spanish.md
  2. 9
      packages/nc-gui/assets/style.scss
  3. 7
      packages/nc-gui/components.d.ts
  4. 1
      packages/nc-gui/components/account/UsersModal.vue
  5. 1
      packages/nc-gui/components/api-client/Headers.vue
  6. 3
      packages/nc-gui/components/cell/DatePicker.vue
  7. 2
      packages/nc-gui/components/cell/DateTimePicker.vue
  8. 2
      packages/nc-gui/components/cell/GeoData.vue
  9. 9
      packages/nc-gui/components/cell/Json.vue
  10. 13
      packages/nc-gui/components/cell/MultiSelect.vue
  11. 87
      packages/nc-gui/components/cell/Percent.vue
  12. 347
      packages/nc-gui/components/cell/RichText.vue
  13. 244
      packages/nc-gui/components/cell/RichText/LinkOptions.vue
  14. 381
      packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue
  15. 68
      packages/nc-gui/components/cell/RichText/SelectedBubbleMenuPopup.vue
  16. 192
      packages/nc-gui/components/cell/TextArea.vue
  17. 2
      packages/nc-gui/components/cell/TimePicker.vue
  18. 2
      packages/nc-gui/components/cell/YearPicker.vue
  19. 22
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  20. 29
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  21. 284
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  22. 37
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  23. 8
      packages/nc-gui/components/dashboard/settings/BaseAudit.vue
  24. 17
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  25. 2
      packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue
  26. 2
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  27. 2
      packages/nc-gui/components/erd/TableNode.vue
  28. 106
      packages/nc-gui/components/general/JoinCloud.vue
  29. 2
      packages/nc-gui/components/general/language/index.vue
  30. 3
      packages/nc-gui/components/nc/Button.vue
  31. 2
      packages/nc-gui/components/nc/Dropdown.vue
  32. 14
      packages/nc-gui/components/nc/Modal.vue
  33. 10
      packages/nc-gui/components/nc/Select.vue
  34. 5
      packages/nc-gui/components/nc/Switch.vue
  35. 11
      packages/nc-gui/components/nc/Tooltip.vue
  36. 2
      packages/nc-gui/components/project/AccessSettings.vue
  37. 54
      packages/nc-gui/components/project/View.vue
  38. 1
      packages/nc-gui/components/smartsheet/Cell.vue
  39. 15
      packages/nc-gui/components/smartsheet/Toolbar.vue
  40. 7
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  41. 2
      packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue
  42. 14
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  43. 8
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  44. 38
      packages/nc-gui/components/smartsheet/column/LongTextOptions.vue
  45. 51
      packages/nc-gui/components/smartsheet/column/PercentOptions.vue
  46. 47
      packages/nc-gui/components/smartsheet/column/PercentOptionsLegacy.vue
  47. 26
      packages/nc-gui/components/smartsheet/column/RichLongTextDefaultValue.vue
  48. 6
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  49. 29
      packages/nc-gui/components/smartsheet/details/Fields.vue
  50. 20
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  51. 21
      packages/nc-gui/components/smartsheet/grid/Table.vue
  52. 8
      packages/nc-gui/components/smartsheet/grid/usePaginationShortcuts.ts
  53. 17
      packages/nc-gui/components/smartsheet/header/Cell.vue
  54. 2
      packages/nc-gui/components/smartsheet/header/Menu.vue
  55. 11
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  56. 11
      packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue
  57. 13
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  58. 43
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  59. 20
      packages/nc-gui/components/smartsheet/toolbar/OpenedViewAction.vue
  60. 2
      packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue
  61. 5
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  62. 2
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  63. 70
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  64. 12
      packages/nc-gui/components/template/Editor.vue
  65. 1
      packages/nc-gui/components/virtual-cell/Links.vue
  66. 8
      packages/nc-gui/components/virtual-cell/Lookup.vue
  67. 3
      packages/nc-gui/components/virtual-cell/QrCode.vue
  68. 1
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  69. 5
      packages/nc-gui/components/virtual-cell/components/ListItem.vue
  70. 1
      packages/nc-gui/composables/useAttachment.ts
  71. 10
      packages/nc-gui/composables/useColumnCreateStore.ts
  72. 4
      packages/nc-gui/composables/useData.ts
  73. 1
      packages/nc-gui/composables/useGlobal/state.ts
  74. 1
      packages/nc-gui/composables/useGlobal/types.ts
  75. 4
      packages/nc-gui/composables/useMultiSelect/index.ts
  76. 4
      packages/nc-gui/composables/useViewColumns.ts
  77. 4
      packages/nc-gui/composables/useViewData.ts
  78. 131
      packages/nc-gui/helpers/dbTiptapExtensions/links.ts
  79. 186
      packages/nc-gui/helpers/dbTiptapExtensions/task-item.ts
  80. 2
      packages/nc-gui/helpers/parsers/CSVTemplateAdapter.ts
  81. 5
      packages/nc-gui/helpers/parsers/ExcelTemplateAdapter.ts
  82. 2
      packages/nc-gui/helpers/parsers/JSONTemplateAdapter.ts
  83. 20
      packages/nc-gui/lang/ar.json
  84. 20
      packages/nc-gui/lang/bn_IN.json
  85. 20
      packages/nc-gui/lang/cs.json
  86. 20
      packages/nc-gui/lang/da.json
  87. 20
      packages/nc-gui/lang/de.json
  88. 19
      packages/nc-gui/lang/en.json
  89. 108
      packages/nc-gui/lang/es.json
  90. 20
      packages/nc-gui/lang/eu.json
  91. 20
      packages/nc-gui/lang/fa.json
  92. 20
      packages/nc-gui/lang/fi.json
  93. 20
      packages/nc-gui/lang/fr.json
  94. 20
      packages/nc-gui/lang/he.json
  95. 20
      packages/nc-gui/lang/hi.json
  96. 20
      packages/nc-gui/lang/hr.json
  97. 20
      packages/nc-gui/lang/id.json
  98. 20
      packages/nc-gui/lang/it.json
  99. 68
      packages/nc-gui/lang/ja.json
  100. 20
      packages/nc-gui/lang/ko.json
  101. Some files were not shown because too many files have changed in this diff Show More

4
markdown/readme/languages/spanish.md

@ -194,8 +194,8 @@ Por favor diríjase a [Contribution Guide](https://github.com/nocodb/nocodb/blob
# Por qué estamos construyendo esto?
La mayoría de las empresas de Internet emplean una hoja de cálculo o una base de datos para resolver sus necesidades comerciales. Las hojas de cálculo son utilizadas por billones de personas o más de manera colaborativa todos los días. Sin embargo, estamos lejos de trabajar a velocidades similares en bases de datos, ya que son herramientas computacionalmente más poderosas. Los intentos de resolver esto con soluciones SaaS han significado horribles controles de acceso, dependencia de un proveedor, dependencia de datos, cambios abruptos de precios y lo que es más importante, un techo de cristal sobre lo que es posible en el futuro."
La mayoría de las empresas de Internet emplean una hoja de cálculo o una base de datos para resolver sus necesidades comerciales. Las hojas de cálculo son utilizadas por billones de personas o más de manera colaborativa todos los días. Sin embargo, estamos lejos de trabajar a velocidades similares en bases de datos, ya que son herramientas computacionalmente más poderosas. Los intentos de resolver esto con soluciones SaaS han significado horribles controles de acceso, dependencia de un proveedor, dependencia de datos, cambios abruptos de precios y lo que es más importante, un techo de cristal sobre lo que es posible en el futuro.
# Nuestra misión
Nuestra misión es proporcionar la interfaz sin-código más potente para bases de datos, la cual es open-source para negocios de Internet en el mundo. Esto no solo democratizaría el acceso a una poderosa herramienta de computación, sino que también producirá a billones de personas o más con habilidades radicales de perfección y construcción en Internet."
Nuestra misión es proporcionar la interfaz sin-código más potente para bases de datos, la cual es open-source para negocios de Internet en el mundo. Esto no solo democratizaría el acceso a una poderosa herramienta de computación, sino que también producirá a miles de millones de personas o más con habilidades radicales de perfección y construcción en Internet.

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

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

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

@ -49,6 +49,7 @@ declare module '@vue/runtime-core' {
AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopover: typeof import('ant-design-vue/es')['Popover']
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARate: typeof import('ant-design-vue/es')['Rate']
@ -76,6 +77,7 @@ declare module '@vue/runtime-core' {
CilFullscreen: typeof import('~icons/cil/fullscreen')['default']
CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
IcBaselineArrowOutward: typeof import('~icons/ic/baseline-arrow-outward')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
@ -123,6 +125,7 @@ declare module '@vue/runtime-core' {
MdiCodeTags: typeof import('~icons/mdi/code-tags')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
MdiDiscord: typeof import('~icons/mdi/discord')['default']
MdiDotsHorizontal: typeof import('~icons/mdi/dots-horizontal')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
@ -130,9 +133,13 @@ declare module '@vue/runtime-core' {
MdiFileDocumentMultipleOutline: typeof import('~icons/mdi/file-document-multiple-outline')['default']
MdiFileDocumentOutline: typeof import('~icons/mdi/file-document-outline')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFormatBold: typeof import('~icons/mdi/format-bold')['default']
MdiFormatItalic: typeof import('~icons/mdi/format-italic')['default']
MdiFormatUnderline: typeof import('~icons/mdi/format-underline')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHistory: typeof import('~icons/mdi/history')['default']
MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLink: typeof import('~icons/mdi/link')['default']
MdiLinkVariant: typeof import('~icons/mdi/link-variant')['default']
MdiLoading: typeof import('~icons/mdi/loading')['default']
MdiLogin: typeof import('~icons/mdi/login')['default']

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -132,9 +132,13 @@ const vModel = computed({
const selectedTitles = computed(() =>
modelValue
? typeof modelValue === 'string'
? isMysql(column.value.source_id)
? modelValue.split(',').sort((a, b) => {
? Array.isArray(modelValue)
? modelValue
: isMysql(column.value.source_id)
? modelValue
.toString()
.split(',')
.sort((a, b) => {
const opa = options.value.find((el) => el.title === a)
const opb = options.value.find((el) => el.title === b)
if (opa && opb) {
@ -142,8 +146,7 @@ const selectedTitles = computed(() =>
}
return 0
})
: modelValue.split(',')
: modelValue
: modelValue.toString().split(',')
: [],
)

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

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

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

@ -0,0 +1,347 @@
<script lang="ts" setup>
import StarterKit from '@tiptap/starter-kit'
import TaskList from '@tiptap/extension-task-list'
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 { TaskItem } from '@/helpers/dbTiptapExtensions/task-item'
import { Link } from '@/helpers/dbTiptapExtensions/links'
const props = defineProps<{
value?: string | null
readonly?: boolean
syncValueChange?: boolean
showMenu?: boolean
fullMode?: boolean
}>()
const emits = defineEmits(['update:value'])
const turndownService = new TurndownService({})
turndownService.addRule('lineBreak', {
filter: (node) => {
return node.nodeName === 'BR'
},
replacement: () => {
return '<br />'
},
})
turndownService.addRule('taskList', {
filter: (node) => {
return node.nodeName === 'LI' && !!node.getAttribute('data-checked')
},
replacement: (content, node: any) => {
// Remove the first \n\n and last \n\n
const processContent = content.replace(/^\n\n/, '').replace(/\n\n$/, '')
const isChecked = node.getAttribute('data-checked') === 'true'
return `[${isChecked ? 'x' : ' '}] ${processContent}\n\n`
},
})
const checkListItem = {
name: 'checkListItem',
level: 'block',
tokenizer(src: string) {
src = src.split('\n\n')[0]
const isMatched = src.startsWith('[ ]') || src.startsWith('[x]') || src.startsWith('[X]')
if (isMatched) {
const isNotChecked = src.startsWith('[ ]')
let text = src.slice(3)
if (text[0] === ' ') text = text.slice(1)
const token = {
// Token to generate
type: 'checkListItem',
raw: src,
text,
tokens: [],
checked: !isNotChecked,
}
;(this as any).lexer.inline(token.text, token.tokens) // Queue this data to be processed for inline tokens
return token
}
return false
},
renderer(token: any) {
return `<ul data-type="taskList"><li data-checked="${
token.checked ? 'true' : 'false'
}" data-type="taskItem"><label><input type="checkbox" ${
token.checked ? 'checked="checked"' : ''
}><span></span></label><div>${(this as any).parser.parseInline(token.tokens)}</div></li></ul>` // parseInline to turn child tokens into HTML
},
}
marked.use({ extensions: [checkListItem] })
const editorDom = ref<HTMLElement | null>(null)
const vModel = useVModel(props, 'value', emits, { defaultValue: '' })
const tiptapExtensions = [
StarterKit,
TaskList,
TaskItem.configure({
nested: true,
}),
Underline,
Link,
]
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
},
editable: !props.readonly,
})
const setEditorContent = (contentMd: any) => {
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(() => {
;(editor.value!.state as any).history$.prevRanges = null
;(editor.value!.state as any).history$.done.eventCount = 0
}, 100)
}
if (props.syncValueChange) {
watch(vModel, () => {
setEditorContent(vModel.value)
})
}
watch(editorDom, () => {
if (!editorDom.value) return
setEditorContent(vModel.value)
// Focus editor after editor is mounted
setTimeout(() => {
editor.value?.chain().focus().run()
}, 50)
})
</script>
<template>
<div
class="h-full"
:class="{
'flex flex-col flex-grow nc-rich-text-full': props.fullMode,
'nc-rich-text-embed': !props.fullMode,
}"
>
<div v-if="props.showMenu" class="absolute top-0 right-0.5">
<CellRichTextSelectedBubbleMenu v-if="editor" :editor="editor" embed-mode />
</div>
<CellRichTextSelectedBubbleMenuPopup v-if="editor" :editor="editor" />
<CellRichTextLinkOptions v-if="editor" :editor="editor" />
<EditorContent
ref="editorDom"
:editor="editor"
class="flex flex-col nc-textarea-rich-editor w-full flex-grow"
:class="{
'ml-1 mt-2.5': props.fullMode,
}"
/>
</div>
</template>
<style lang="scss">
.nc-text-rich-scroll {
&::-webkit-scrollbar-thumb {
@apply bg-transparent;
}
}
.nc-text-rich-scroll:hover {
&::-webkit-scrollbar-thumb {
@apply bg-gray-200;
}
}
.nc-rich-text-embed {
.ProseMirror {
@apply !border-transparent;
}
}
.nc-rich-text-full {
@apply px-1.75;
.ProseMirror {
@apply !p-2;
max-height: calc(min(60vh, 100rem));
min-height: 8rem;
}
}
.nc-textarea-rich-editor {
.ProseMirror {
@apply flex-grow pt-1 border-1 border-gray-200 rounded-lg pr-1 mr-2;
> * {
@apply ml-1;
}
}
.ProseMirror-focused {
// remove all border
outline: none;
@apply border-brand-500;
}
p {
@apply !mb-1;
}
ul {
li {
@apply ml-4;
list-style-type: disc;
}
}
ol {
@apply -ml-6 !pl-4;
li {
list-style-type: decimal;
}
}
ul,
ol {
@apply !my-0;
}
ul[data-type='taskList'] {
@apply;
li {
@apply !ml-0 flex flex-row gap-x-2;
list-style-type: none;
input {
@apply mt-0.75 flex rounded-sm;
z-index: -10;
}
// Unchecked
input:not(:checked) {
// Add border to checkbox
border-width: 1.5px;
@apply border-gray-700;
}
}
}
// 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;
}
h1 {
font-weight: 700;
font-size: 1.85rem;
margin-bottom: 0.1rem;
}
h2 {
font-weight: 600;
font-size: 1.55rem;
margin-bottom: 0.1em;
}
h3 {
font-weight: 600;
font-size: 1.15rem;
margin-bottom: 0.1em;
}
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;
}
}
.nc-rich-text-full {
.ProseMirror {
overflow-y: scroll;
overflow-x: hidden;
scrollbar-width: thin !important;
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
&::-webkit-scrollbar-track {
-webkit-border-radius: 10px;
border-radius: 10px;
margin-top: 4px;
margin-bottom: 4px;
}
&::-webkit-scrollbar-track-piece {
width: 0px;
}
&::-webkit-scrollbar {
@apply bg-transparent;
}
&::-webkit-scrollbar-thumb {
-webkit-border-radius: 10px;
border-radius: 10px;
width: 4px;
@apply bg-gray-300;
}
&::-webkit-scrollbar-thumb:hover {
@apply bg-gray-400;
}
}
}
</style>

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

@ -0,0 +1,244 @@
<script lang="ts" setup>
import type { Editor } from '@tiptap/vue-3'
import { BubbleMenu } from '@tiptap/vue-3'
import { getMarkRange } from '@tiptap/core'
import type { Mark } from 'prosemirror-model'
const props = defineProps<Props>()
interface Props {
editor: Editor
}
const editor = computed(() => props.editor)
const inputRef = ref<HTMLInputElement>()
const linkNodeMark = ref<Mark | undefined>()
const href = ref('')
const isLinkOptionsVisible = ref(false)
// This is used to prevent the menu from showing up after a link is deleted, an edge case when the link with empty placeholder text is deleted.
// This is because checkLinkMark is not called in that case
const justDeleted = ref(false)
// This function is called by BubbleMenu on selection change
// It is used to check if the link mark is active and only show the menu if it is
const checkLinkMark = (editor: Editor) => {
if (!editor.view.editable) return false
if (justDeleted.value) {
setTimeout(() => {
justDeleted.value = false
}, 100)
return false
}
const activeNode = editor?.state?.selection?.$from?.nodeBefore || editor?.state?.selection?.$from?.nodeAfter
const isLinkMarkedStoredInEditor = editor?.state?.storedMarks?.some((mark: Mark) => mark.type.name === 'link')
const isActiveNodeMarkActive = activeNode?.marks?.some((mark: Mark) => mark.type.name === 'link') || isLinkMarkedStoredInEditor
if (isActiveNodeMarkActive) {
linkNodeMark.value = activeNode?.marks.find((mark: Mark) => mark.type.name === 'link')
href.value = linkNodeMark.value?.attrs?.href
}
if (isLinkMarkedStoredInEditor) {
linkNodeMark.value = editor?.state?.storedMarks?.find((mark: Mark) => mark.type.name === 'link')
href.value = linkNodeMark.value?.attrs?.href
}
const isTextSelected = editor?.state?.selection?.from !== editor?.state?.selection?.to
// check if active node is a text node
const showLinkOptions = isActiveNodeMarkActive && !isTextSelected
isLinkOptionsVisible.value = !!showLinkOptions
return showLinkOptions
}
function notStartingWithNetworkProtocol(inputString: string) {
const pattern = /^(?![^:]+:\/\/).*/
const isMatch = pattern.test(inputString)
return isMatch
}
const onChange = () => {
const isLinkMarkedStoredInEditor = editor.value.state?.storedMarks?.some((mark: Mark) => mark.type.name === 'link')
let formatedHref = href.value
if (
isValidURL(href.value) &&
href.value.length > 0 &&
!href.value.startsWith('/') &&
notStartingWithNetworkProtocol(href.value)
) {
formatedHref = `https://${href.value}`
}
if (isLinkMarkedStoredInEditor) {
editor.value.view.dispatch(
editor.value.view.state.tr
.removeStoredMark(editor.value.schema.marks.link)
.addStoredMark(editor.value.schema.marks.link.create({ href: formatedHref })),
)
} else if (linkNodeMark.value) {
const selection = editor.value.state?.selection
const markSelection = getMarkRange(selection.$anchor, editor.value.schema.marks.link) as any
editor.value.view.dispatch(
editor.value.view.state.tr
.removeMark(markSelection.from, markSelection.to, editor.value.schema.marks.link)
.addMark(markSelection.from, markSelection.to, editor.value.schema.marks.link.create({ href: formatedHref })),
)
}
}
const onDelete = () => {
const isLinkMarkedStoredInEditor = editor.value.state?.storedMarks?.some((mark: Mark) => mark.type.name === 'link')
if (isLinkMarkedStoredInEditor) {
editor.value.view.dispatch(editor.value.view.state.tr.removeStoredMark(editor.value.schema.marks.link))
} else if (linkNodeMark.value) {
const selection = editor.value.state.selection
const markSelection = getMarkRange(selection.$anchor, editor.value.schema.marks.link) as any
editor.value.view.dispatch(
editor.value.view.state.tr.removeMark(markSelection.from, markSelection.to, editor.value.schema.marks.link),
)
}
justDeleted.value = true
}
const handleKeyDown = (e: any) => {
const isCtrlPressed = isMac() ? e.metaKey : e.ctrlKey
// Ctrl + Z/ Meta + Z
if (isCtrlPressed && e.key === 'z') {
e.preventDefault()
editor.value.commands.undo()
}
// Ctrl + Shift + Z/ Meta + Shift + Z
if (isCtrlPressed && e.shiftKey && e.key === 'z') {
e.preventDefault()
editor.value.commands.redo()
}
}
const onInputBoxEnter = () => {
inputRef.value?.blur()
editor.value.chain().focus().run()
}
const handleInputBoxKeyDown = (e: any) => {
if (e.key === 'ArrowDown' || e.key === 'Escape') {
editor.value.chain().focus().run()
}
}
watch(isLinkOptionsVisible, (value, oldValue) => {
if (value && !oldValue) {
const isPlaceholderEmpty =
!editor.value.state.selection.$from.nodeBefore?.textContent && !editor.value.state.selection.$from.nodeAfter?.textContent
if (!isPlaceholderEmpty) return
setTimeout(() => {
inputRef.value?.focus()
}, 100)
}
})
const openLink = () => {
if (href.value) {
window.open(href.value, '_blank', 'noopener,noreferrer')
}
}
</script>
<template>
<BubbleMenu :editor="editor" :tippy-options="{ duration: 100, maxWidth: 600 }" :should-show="(checkLinkMark as any)">
<div
v-if="!justDeleted"
ref="wrapperRef"
class="relative bubble-menu nc-text-area-rich-link-options flex flex-col bg-gray-50 py-1 px-1 rounded-lg"
data-testid="nc-text-area-rich-link-options"
@keydown.stop="handleKeyDown"
>
<div class="flex items-center gap-x-1">
<div class="!border-1 !border-gray-200 !py-0.5 bg-gray-100 rounded-md !z-10">
<a-input
ref="inputRef"
v-model:value="href"
class="nc-text-area-rich-link-option-input flex-1 !w-96 !mx-0.5 !px-1.5 !py-0.5 !rounded-md z-10"
:bordered="false"
placeholder="Enter a link"
@change="onChange"
@press-enter="onInputBoxEnter"
@keydown="handleInputBoxKeyDown"
/>
</div>
<NcTooltip overlay-class-name="nc-text-area-rich-link-options">
<template #title> Open link </template>
<NcButton
:class="{
'!text-gray-300 cursor-not-allowed': href.length === 0,
}"
data-testid="nc-text-area-rich-link-options-open-link"
size="small"
type="text"
@click="openLink"
>
<IcBaselineArrowOutward />
</NcButton>
</NcTooltip>
<NcTooltip overlay-class-name="nc-text-area-rich-link-options">
<template #title> Delete link </template>
<NcButton
class="!duration-0 !hover:(text-red-400 bg-red-50)"
data-testid="nc-text-area-rich-link-options-open-delete"
size="small"
type="text"
@click="onDelete"
>
<MdiDeleteOutline />
</NcButton>
</NcTooltip>
<div class="absolute -bottom-1.5 left-0 right-0 w-full flex flex-row justify-center">
<div
class="flex h-2.5 w-2.5 bg-white border-gray-200 border-r-1 border-b-1 transform rotate-45"
:style="{
boxShadow: '1px 1px 3px rgba(231, 231, 233, 1)',
}"
></div>
</div>
</div>
</div>
</BubbleMenu>
</template>
<style lang="scss">
.bubble-menu {
// shadow
@apply shadow-gray-200 shadow-sm;
}
.nc-text-area-rich-link-options {
.ant-popover-inner-content {
@apply !shadow-none !p-0;
}
.ant-popover-arrow {
@apply !shadow-none;
.ant-popover-arrow-content {
@apply !shadow-none !bg-gray-100;
}
}
.ant-popover-inner {
@apply !shadow-none !bg-gray-100 py-1.5 px-2.5 text-xs !rounded-sm;
}
}
</style>

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

@ -0,0 +1,381 @@
<script lang="ts" setup>
import type { Editor } from '@tiptap/vue-3'
import MdiFormatBulletList from '~icons/mdi/format-list-bulleted'
import MdiFormatStrikeThrough from '~icons/mdi/format-strikethrough'
import MdiFormatListNumber from '~icons/mdi/format-list-numbered'
import MdiFormatListCheckbox from '~icons/mdi/format-list-checkbox'
import MsFormatH1 from '~icons/material-symbols/format-h1'
import MsFormatH2 from '~icons/material-symbols/format-h2'
import MsFormatH3 from '~icons/material-symbols/format-h3'
import TablerBlockQuote from '~icons/tabler/blockquote'
import MsCode from '~icons/material-symbols/code'
import MsFormatQuote from '~icons/material-symbols/format-quote'
interface Props {
editor: Editor
embedMode?: boolean
}
const props = defineProps<Props>()
const editor = computed(() => props.editor)
const embedMode = computed(() => props.embedMode)
const cmdOrCtrlKey = computed(() => {
return isMac() ? '⌘' : 'CTRL'
})
const shiftKey = computed(() => {
return isMac() ? '⇧' : 'Shift'
})
const altKey = computed(() => {
return isMac() ? '⌥' : 'Alt'
})
const onToggleLink = () => {
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)
}
}
</script>
<template>
<div
class="bubble-menu flex flex-row gap-x-1 bg-gray-100 py-1 rounded-lg px-1"
:class="{
'embed-mode': embedMode,
'full-mode': !embedMode,
}"
>
<NcTooltip :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.bold') }}
</div>
<div class="text-xs">{{ cmdOrCtrlKey }} B</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('bold') }"
:disabled="editor.isActive('codeBlock')"
@click="editor!.chain().focus().toggleBold().run()"
>
<MdiFormatBold />
</NcButton>
</NcTooltip>
<NcTooltip :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.italic') }}
</div>
<div>{{ cmdOrCtrlKey }} I</div>
</div>
</template>
<NcButton
size="small"
type="text"
:disabled="editor.isActive('codeBlock')"
:class="{ 'is-active': editor.isActive('italic') }"
@click=";(editor!.chain().focus() as any).toggleItalic().run()"
>
<MdiFormatItalic />
</NcButton>
</NcTooltip>
<NcTooltip :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.underline') }}
</div>
<div>{{ cmdOrCtrlKey }} U</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('underline') }"
:disabled="editor.isActive('codeBlock')"
@click="editor!.chain().focus().toggleUnderline().run()"
>
<MdiFormatUnderline />
</NcButton>
</NcTooltip>
<NcTooltip :disabled="editor.isActive('codeBlock')">
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.strike') }}
</div>
<div>{{ shiftKey }} {{ cmdOrCtrlKey }} S</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('strike') }"
:disabled="editor.isActive('codeBlock')"
@click="editor!.chain().focus().toggleStrike().run()"
>
<MdiFormatStrikeThrough />
</NcButton>
</NcTooltip>
<NcTooltip v-if="embedMode">
<template #title> {{ $t('general.code') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('codeBlock') }"
@click="editor!.chain().focus().toggleCodeBlock().run()"
>
<MsCode />
</NcButton>
</NcTooltip>
<NcTooltip v-else :disabled="editor.isActive('codeBlock')">
<template #title> {{ $t('general.quote') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('code') }"
:disabled="editor.isActive('codeBlock')"
@click="editor!.chain().focus().toggleCode().run()"
>
<MsFormatQuote />
</NcButton>
</NcTooltip>
<div class="divider"></div>
<template v-if="embedMode">
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading1') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 1</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 1 }).run()"
>
<MsFormatH1 />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading2') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 2</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 2 }).run()"
>
<MsFormatH2 />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
<div class="flex flex-col items-center">
<div>
{{ $t('labels.heading3') }}
</div>
<div>{{ cmdOrCtrlKey }} {{ altKey }} 3</div>
</div>
</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
@click="editor!.chain().focus().toggleHeading({ level: 3 }).run()"
>
<MsFormatH3 />
</NcButton>
</NcTooltip>
<div class="divider"></div>
</template>
<NcTooltip v-if="embedMode">
<template #title> {{ $t('labels.blockQuote') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('blockquote') }"
@click="editor!.chain().focus().toggleBlockquote().run()"
>
<TablerBlockQuote class="-mt-0.25" />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title> {{ $t('labels.bulletList') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('bulletList') }"
@click="editor!.chain().focus().toggleBulletList().run()"
>
<MdiFormatBulletList />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title> {{ $t('labels.numberedList') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('orderedList') }"
@click="editor!.chain().focus().toggleOrderedList().run()"
>
<MdiFormatListNumber />
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title> {{ $t('labels.taskList') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('taskList') }"
@click="editor!.chain().focus().toggleTaskList().run()"
>
<MdiFormatListCheckbox />
</NcButton>
</NcTooltip>
<div class="divider"></div>
<NcTooltip :disabled="editor.isActive('codeBlock')">
<template #title> {{ $t('general.link') }}</template>
<NcButton
size="small"
type="text"
:class="{ 'is-active': editor.isActive('link') }"
:disabled="editor.isActive('codeBlock')"
@click="onToggleLink"
>
<div class="flex flex-row items-center px-0.5">
<MdiLink />
<div class="!text-xs !ml-1">{{ $t('general.link') }}</div>
</div>
</NcButton>
</NcTooltip>
</div>
</template>
<style lang="scss">
.bubble-menu-hidden {
[data-tippy-root] {
opacity: 0;
height: 0;
overflow: hidden;
z-index: -1;
user-select: none;
}
}
.bubble-text-format-button-icon {
@apply px-1.5 py-0 border-1 border-gray-300 rounded-sm items-center justify-center;
font-size: 0.8rem;
font-weight: 600;
}
.bubble-text-format-button {
@apply rounded-md py-1 my-0 pl-2.5 pr-3 cursor-pointer items-center gap-x-2.5 hover:bg-gray-100;
}
.bubble-menu.full-mode {
@apply border-gray-100
box-shadow: 0px 0px 1.2rem 0 rgb(230, 230, 230) !important;
}
.bubble-menu.embed-mode {
@apply border-transparent !shadow-none;
}
.embed-mode.bubble-menu {
@apply !py-0 !my-0 !border-0;
.divider {
@apply my-0 !h-11 border-gray-100;
}
.nc-button {
@apply !mt-1.65;
}
}
.bubble-menu {
// shadow
@apply bg-white;
border-width: 1px;
.nc-button.is-active {
@apply !hover:outline-gray-200 bg-gray-100 text-brand-500;
outline: 1px;
}
.divider {
@apply border-r-1 border-gray-200 !h-6 !mx-0.5 my-1;
}
.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>

68
packages/nc-gui/components/cell/RichText/SelectedBubbleMenuPopup.vue

@ -0,0 +1,68 @@
<script lang="ts" setup>
import type { Editor } from '@tiptap/vue-3'
import { BubbleMenu } from '@tiptap/vue-3'
const props = defineProps<Props>()
interface Props {
editor: Editor
}
const editor = computed(() => props.editor)
// Debounce show menu to prevent flickering
const showMenu = computed(() => {
if (!editor) return false
return !editor.value.state.selection.empty
})
const showMenuDebounced = ref(false)
watchDebounced(
() => showMenu.value,
(value) => {
showMenuDebounced.value = value
},
{
debounce: 200,
maxWait: 800,
immediate: true,
},
)
const handleEditorMouseDown = (e: MouseEvent) => {
const domsInEvent = document.elementsFromPoint(e.clientX, e.clientY) as HTMLElement[]
const isBubble = domsInEvent.some((dom) => dom?.classList?.contains('bubble-menu'))
if (isBubble) return
const pageContent = document.querySelector('.nc-textarea-rich-editor')
pageContent?.classList.add('bubble-menu-hidden')
}
const handleEditorMouseUp = (e: MouseEvent) => {
const domsInEvent = document.elementsFromPoint(e.clientX, e.clientY) as HTMLElement[]
const isBubble = domsInEvent.some((dom) => dom?.classList?.contains('bubble-menu'))
if (isBubble) return
setTimeout(() => {
const pageContent = document.querySelector('.nc-textarea-rich-editor')
pageContent?.classList.remove('bubble-menu-hidden')
}, 100)
}
onMounted(() => {
document.addEventListener('mouseup', handleEditorMouseUp)
document.addEventListener('mousedown', handleEditorMouseDown)
})
onUnmounted(() => {
document.removeEventListener('mouseup', handleEditorMouseUp)
document.removeEventListener('mousedown', handleEditorMouseDown)
})
</script>
<template>
<BubbleMenu :editor="editor" :update-delay="300" :tippy-options="{ duration: 100, maxWidth: 600 }">
<CellRichTextSelectedBubbleMenu v-if="showMenuDebounced" :editor="editor" />
</BubbleMenu>
</template>

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

@ -1,16 +1,21 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import {
ActiveCellInj,
ColumnInj,
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
ReadonlyInj,
RowHeightInj,
computed,
iconMap,
inject,
onClickOutside,
ref,
useGlobal,
useVModel,
watch,
} from '#imports'
const props = defineProps<{
@ -33,23 +38,36 @@ const isForm = inject(IsFormInj, ref(false))
const { showNull } = useGlobal()
const vModel = useVModel(props, 'modelValue', emits, { defaultValue: '' })
const vModel = useVModel(props, 'modelValue', emits, { defaultValue: column?.value.cdf ? String(column?.value.cdf) : '' })
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const position = ref<
| {
top: number
left: number
}
| undefined
>({
top: 200,
left: 600,
})
const isDragging = ref(false)
const focus: VNodeRef = (el) => !isExpandedFormOpen.value && !isEditColumn.value && (el as HTMLTextAreaElement)?.focus()
const height = computed(() => {
if (!rowHeight.value) return 60
if (!rowHeight.value || rowHeight.value === 1) return 36
return rowHeight.value * 60
return rowHeight.value * 36
})
const isVisible = ref(false)
const inputWrapperRef = ref<HTMLElement | null>(null)
const inputRef = ref<HTMLTextAreaElement | null>(null)
const active = inject(ActiveCellInj, ref(false))
const inputRef = ref<HTMLTextAreaElement | null>(null)
const readOnly = inject(ReadonlyInj)
@ -67,26 +85,116 @@ onClickOutside(inputWrapperRef, (e) => {
isVisible.value = false
})
const onDblClick = () => {
const onTextClick = () => {
if (!props.virtual) return
isVisible.value = true
editEnabled.value = true
}
const isRichMode = computed(() => {
let meta: any = {}
if (typeof column?.value?.meta === 'string') {
meta = JSON.parse(column?.value?.meta)
} else {
meta = column?.value?.meta ?? {}
}
return meta?.richMode
})
const onExpand = () => {
isVisible.value = true
const { top, left } = inputWrapperRef.value?.getBoundingClientRect() ?? { top: 0, left: 0 }
position.value = {
top: top + 42,
left,
}
}
const onMouseMove = (e: MouseEvent) => {
if (!isDragging.value) return
e.stopPropagation()
position.value = {
top: e.clientY - 22,
left: e.clientX - 46,
}
}
const onMouseUp = (e: MouseEvent) => {
if (!isDragging.value) return
e.stopPropagation()
isDragging.value = false
position.value = undefined
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
watch(position, () => {
const dom = document.querySelector('.nc-textarea-dropdown-active') as HTMLElement
if (!dom) return
if (!position.value) return
// Set left and top of dom
setTimeout(() => {
if (!position.value) return
dom.style.left = `${position.value.left}px`
dom.style.top = `${position.value.top}px`
}, 100)
})
const dragStart = () => {
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
isDragging.value = true
}
watch(editEnabled, () => {
if (editEnabled.value) {
isVisible.value = true
}
})
</script>
<template>
<NcDropdown v-model:visible="isVisible" class="overflow-visible" :trigger="[]" placement="bottomLeft">
<NcDropdown
v-model:visible="isVisible"
class="overflow-visible"
:trigger="[]"
placement="bottomLeft"
:overlay-class-name="isVisible ? 'nc-textarea-dropdown-active' : undefined"
>
<div
class="flex flex-row pt-0.5 w-full"
class="flex flex-row pt-0.5 w-full rich-wrapper"
:class="{
'min-h-10': rowHeight !== 1,
'min-h-6.5': rowHeight === 1,
'h-full': isForm,
}"
>
<div
v-if="isRichMode"
class="w-full cursor-pointer"
:style="{
maxHeight: `${height}px !important`,
minHeight: `${height}px !important`,
}"
@dblclick="onExpand"
>
<LazyCellRichText v-model:value="vModel" sync-value-change readonly class="!pointer-events-none" />
</div>
<textarea
v-if="editEnabled && !isVisible"
v-else-if="editEnabled && !isVisible"
:ref="focus"
v-model="vModel"
rows="4"
@ -123,39 +231,50 @@ const onDblClick = () => {
'word-break': 'break-word',
'white-space': 'pre-line',
}"
@click="onDblClick"
@click="onTextClick"
/>
<span v-else>{{ vModel }}</span>
<div
v-if="active && !isExpandedFormOpen"
class="!absolute right-0 bottom-0 h-6 w-5 group cursor-pointer flex justify-end gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
:class="{ 'right-2 bottom-2': editEnabled }"
data-testid="attachment-cell-file-picker-button"
@click.stop="isVisible = !isVisible"
<NcTooltip
v-if="!isVisible"
placement="bottom"
class="!absolute right-0 bottom-1 nc-text-area-expand-btn"
:class="{ 'right-0 bottom-1': editEnabled, '!bottom-0': !isRichMode }"
>
<NcTooltip placement="bottom">
<template #title>{{ $t('title.expand') }}</template>
<component
:is="iconMap.expand"
class="transform dark:(!text-white) group-hover:(!text-grey-800 scale-120) text-gray-500 text-xs"
/>
</NcTooltip>
</div>
<template #title>{{ $t('title.expand') }}</template>
<NcButton type="secondary" size="xsmall" data-testid="attachment-cell-file-picker-button" @click.stop="onExpand">
<component :is="iconMap.expand" class="transform group-hover:(!text-grey-800 ) scale-120 text-gray-700 text-xs" />
</NcButton>
</NcTooltip>
</div>
<template #overlay>
<div ref="inputWrapperRef" class="flex flex-col min-w-120 min-h-70 py-3 pl-3 pr-1 expanded-cell-input">
<div
v-if="isVisible"
ref="inputWrapperRef"
class="flex flex-col min-w-200 min-h-70 py-3 expanded-cell-input relative"
:class="{
'cursor-move': isDragging,
}"
>
<div
v-if="column"
class="flex flex-row gap-x-1 items-center font-medium pb-2.5 mb-1 py-1 mr-3 ml-1 border-b-1 border-gray-100"
class="flex flex-row gap-x-1 items-center font-medium pl-3 pb-2.5 border-b-1 border-gray-100 cursor-move"
:class="{
'select-none': isDragging,
}"
@mousedown="dragStart"
>
<SmartsheetHeaderCellIcon class="flex" />
<div class="flex">
{{ column.title }}
<div class="flex max-w-38">
<span class="truncate">
{{ column.title }}
</span>
</div>
</div>
<a-textarea
v-if="!isRichMode"
ref="inputRef"
v-model:value="vModel"
class="p-1 !pt-1 !pr-3 !border-0 !border-r-0 !focus:outline-transparent nc-scrollbar-md !text-black !cursor-text"
@ -166,13 +285,24 @@ const onDblClick = () => {
@keydown.stop
@keydown.escape="isVisible = false"
/>
<LazyCellRichText v-else-if="isVisible" v-model:value="vModel" show-menu full-mode :read-only="readOnly" />
</div>
</template>
</NcDropdown>
</template>
<style>
<style lang="scss" scoped>
textarea:focus {
box-shadow: none;
}
:deep(.nc-text-area-expand-btn) {
@apply !hidden;
}
.rich-wrapper:hover,
.rich-wrapper:active {
:deep(.nc-text-area-expand-btn) {
@apply !block cursor-pointer;
}
}
</style>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3
packages/nc-gui/components/nc/Button.vue

@ -172,7 +172,8 @@ useEventListener(NcButton, 'mousedown', () => {
@apply p-0 h-5.75 min-w-5.75 rounded-md;
}
.nc-button.ant-btn[disabled] {
.nc-button.ant-btn[disabled],
.ant-btn-text.nc-button.ant-btn[disabled] {
box-shadow: none !important;
@apply bg-gray-50 border-0 text-gray-300 cursor-not-allowed md:(hover:bg-gray-50);
}

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

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

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

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

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

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

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

@ -11,7 +11,8 @@ const onChange = (e: boolean) => {
</script>
<template>
<a-switch v-model:checked="checked" :disabled="props.disabled" class="nc-switch" size="small" @change="onChange">
<a-switch v-model:checked="checked" :disabled="props.disabled" class="nc-switch" size="small" @change="onChange"> </a-switch>
<span v-if="$slots.default" class="cursor-pointer pl-2" @click="checked = !checked">
<slot />
</a-switch>
</span>
</template>

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

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

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

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

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

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

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

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

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

@ -1,5 +1,14 @@
<script setup lang="ts">
import { IsPublicInj, inject, ref, useRoles, useSharedView, useSmartsheetStoreOrThrow, useViewsStore } from '#imports'
import {
IsPublicInj,
inject,
ref,
storeToRefs,
useGlobal,
useSharedView,
useSmartsheetStoreOrThrow,
useViewsStore,
} from '#imports'
const { isGrid, isGallery, isKanban, isMap } = useSmartsheetStoreOrThrow()
@ -9,8 +18,6 @@ const { isViewsLoading } = storeToRefs(useViewsStore())
const { isMobileMode } = useGlobal()
const { isUIAllowed } = useRoles()
const { allowCSVDownload } = useSharedView()
</script>
@ -39,7 +46,7 @@ const { allowCSVDownload } = useSharedView()
<!-- <LazySmartsheetToolbarQrScannerButton v-if="isMobileMode && (isGrid || isKanban || isGallery)" /> -->
<LazySmartsheetToolbarExport v-if="(!isPublic && !isUIAllowed('dataInsert')) || (isPublic && allowCSVDownload)" />
<LazySmartsheetToolbarExport v-if="isPublic && allowCSVDownload" />
<div class="flex-1" />
</template>

7
packages/nc-gui/components/smartsheet/VirtualCell.vue

@ -31,6 +31,7 @@ const props = defineProps<{
modelValue: any
row?: Row
active?: boolean
readOnly?: boolean
}>()
const emit = defineEmits(['update:modelValue', 'navigate', 'save'])
@ -38,12 +39,14 @@ const emit = defineEmits(['update:modelValue', 'navigate', 'save'])
const column = toRef(props, 'column')
const active = toRef(props, 'active', false)
const row = toRef(props, 'row')
const readOnly = toRef(props, 'readOnly', false)
provide(ColumnInj, column)
provide(ActiveCellInj, active)
provide(RowInj, row)
provide(CellValueInj, toRef(props, 'modelValue'))
provide(SaveRowInj, () => emit('save'))
provide(ReadonlyInj, readOnly)
const isGrid = inject(IsGridInj, ref(false))
@ -94,7 +97,9 @@ onUnmounted(() => {
<div
ref="elementToObserve"
class="nc-virtual-cell w-full flex items-center"
:class="{ 'text-right justify-end': isGrid && !isForm && isRollup(column) && !isExpandedForm }"
:class="{
'text-right justify-end': isGrid && !isForm && isRollup(column) && !isExpandedForm,
}"
@keydown.enter.exact="onNavigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="onNavigate(NavigateDir.PREV, $event)"
>

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

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

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

@ -220,9 +220,10 @@ if (props.fromTableExplorer) {
:class="{
'bg-white': !props.fromTableExplorer,
'w-[400px]': !props.embedMode,
'!w-146': isTextArea(formState) && formState.meta.richMode,
'!w-[600px]': formState.uidt === UITypes.Formula && !props.embedMode,
'!w-[500px]': formState.uidt === UITypes.Attachment && !props.embedMode && !appInfo.ee,
'shadow-lg border-1 border-gray-50 shadow-gray-100 rounded-md p-6': !embedMode,
'shadow-lg border-1 border-gray-200 shadow-gray-300 rounded-xl p-6': !embedMode,
}"
@keydown="handleEscape"
@click.stop
@ -270,7 +271,7 @@ if (props.fromTableExplorer) {
show-search
class="nc-column-type-input !rounded"
:disabled="isKanban || readOnly"
dropdown-class-name="nc-dropdown-column-type "
dropdown-class-name="nc-dropdown-column-type border-1 border-gray-200"
@change="onUidtOrIdTypeChange"
@dblclick="showDeprecated = !showDeprecated"
>
@ -296,6 +297,7 @@ if (props.fromTableExplorer) {
<LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<LazySmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" />
<LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<LazySmartsheetColumnLongTextOptions v-if="formState.uidt === UITypes.LongText" v-model:value="formState" />
<LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
@ -306,9 +308,11 @@ if (props.fromTableExplorer) {
<LazySmartsheetColumnRollupOptions v-if="formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && (formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links)"
:key="formState.uidt"
v-model:value="formState"
/>
<LazySmartsheetColumnLinkOptions v-if="isEdit && formState.uidt === UITypes.Links" v-model:value="formState" />
<LazySmartsheetColumnPercentOptions v-if="formState.uidt === UITypes.Percent" v-model:value="formState" />
<LazySmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<SmartsheetColumnSelectOptions
v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect"
@ -330,8 +334,12 @@ if (props.fromTableExplorer) {
<!--
Default Value for JSON & LongText is not supported in MySQL
Default Value is Disabled for MSSQL -->
<LazySmartsheetColumnRichLongTextDefaultValue
v-if="isTextArea(formState) && formState.meta.richMode"
v-model:value="formState"
/>
<LazySmartsheetColumnDefaultValue
v-if="
v-else-if="
!isVirtualCol(formState) &&
!isAttachment(formState) &&
!isMssql(meta!.source_id) &&

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

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

38
packages/nc-gui/components/smartsheet/column/LongTextOptions.vue

@ -0,0 +1,38 @@
<!-- File not in use for now -->
<script setup lang="ts">
import { useVModel } from '#imports'
const props = defineProps<{
value: any
}>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const richMode = computed({
get: () => vModel.value.meta?.richMode,
set: (value) => {
if (!vModel.value.meta) vModel.value.meta = {}
vModel.value.meta.richMode = value
},
})
watch(richMode, () => {
vModel.value.cdf = null
})
</script>
<template>
<div class="flex flex-col mt-2 gap-2">
<a-form-item>
<div class="flex flex-row items-center">
<NcSwitch v-model:checked="richMode" :name="$t('labels.enableRichText')" size="small">
<div class="text-xs">{{ $t('labels.enableRichText') }}</div>
</NcSwitch>
</div>
</a-form-item>
</div>
</template>

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

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

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

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

26
packages/nc-gui/components/smartsheet/column/RichLongTextDefaultValue.vue

@ -0,0 +1,26 @@
<script lang="ts" setup>
const props = defineProps<{
value: any
}>()
const emits = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emits)
const cdfValue = computed({
get: () => vModel.value.cdf,
set: (value) => {
vModel.value.cdf = value
},
})
</script>
<template>
<div>
<div class="!my-3 text-xs">{{ $t('placeholder.defaultValue') }}</div>
<div class="flex flex-row gap-2">
<div class="border-1 relative pt-11 flex items-center w-full px-0 my-[-4px] border-gray-300 rounded-md !max-h-70 !pb-2.5">
<LazyCellRichText v-model:value="cdfValue" class="border-t-1 border-gray-100" show-menu full-mode />
</div>
</div>
</div>
</template>

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

@ -113,7 +113,7 @@ onMounted(() => {
op.title = op.title.replace(/^'/, '').replace(/'$/, '')
}
if (vModel.value.cdf) {
if (vModel.value.cdf && typeof vModel.value.cdf === 'string') {
const fndDefaultOption = options.value.find((el) => el.title === vModel.value.cdf)
if (!fndDefaultOption) {
vModel.value.cdf = vModel.value.cdf.replace(/^'/, '').replace(/'$/, '')
@ -249,7 +249,7 @@ const undoRemoveRenderedOption = (index: number) => {
// Removes the Select Option from cdf if the option is removed
watch(vModel.value, (next) => {
const cdfs = (next.cdf ?? '').split(',')
const cdfs = (next.cdf ?? '').toString().split(',')
const values = (next.colOptions.options ?? []).map((col) => {
return col.title.replace(/^'/, '').replace(/'$/, '')
})
@ -339,7 +339,7 @@ const loadListData = async ($state: any) => {
<a-dropdown
v-model:visible="colorMenus[index]"
:trigger="['click']"
overlay-class-name="nc-dropdown-select-color-options"
overlay-class-name="nc-dropdown-select-color-options rounded-md overflow-hidden border-1 border-gray-200 "
>
<template #overlay>
<LazyGeneralColorPicker

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

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

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

@ -88,6 +88,8 @@ const isRecordLinkCopied = ref(false)
const { isUIAllowed } = useRoles()
const readOnly = computed(() => !isUIAllowed('dataEdit') || isPublic.value)
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const { addOrEditStackRow } = useKanbanViewStoreOrThrow()
@ -643,11 +645,11 @@ export default {
<template v-if="isLoading">
<div
v-if="isMobileMode"
class="!h-8.5 !xs:h-12 !xs:bg-white !sm:mr-21 !w-60 mt-0.75 !rounded-lg !overflow-hidden"
class="!h-8.5 !xs:h-12 !xs:bg-white sm:mr-21 w-60 mt-0.75 !rounded-lg !overflow-hidden"
></div>
<a-skeleton-input
v-else
class="!h-8.5 !xs:h-9.5 !xs:bg-white !sm:mr-21 !w-60 mt-0.75 !rounded-lg !overflow-hidden"
class="!h-8.5 !xs:h-9.5 !xs:bg-white sm:mr-21 !w-60 mt-0.75 !rounded-lg !overflow-hidden"
active
size="small"
/>
@ -656,7 +658,7 @@ export default {
<SmartsheetDivDataCell
v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="bg-white rounded-lg !w-[20rem] !xs:w-full border-1 border-gray-200 overflow-hidden px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
class="bg-white rounded-lg w-80 xs:w-full border-1 border-gray-200 overflow-hidden px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
:class="{
'!bg-gray-50 !px-0 !select-text': isReadOnlyVirtualCell(col),
}"
@ -669,6 +671,7 @@ export default {
:class="{
'px-1': isReadOnlyVirtualCell(col),
}"
:read-only="readOnly"
/>
<LazySmartsheetCell
@ -677,7 +680,7 @@ export default {
:column="col"
:edit-enabled="true"
:active="true"
:read-only="isPublic"
:read-only="readOnly"
@update:model-value="changedColumns.add(col.title)"
/>
</SmartsheetDivDataCell>
@ -717,11 +720,11 @@ export default {
<template v-if="isLoading">
<div
v-if="isMobileMode"
class="!h-8.5 !xs:h-9.5 !xs:bg-white !sm:mr-21 !w-60 mt-0.75 !rounded-lg !overflow-hidden"
class="!h-8.5 !xs:h-9.5 !xs:bg-white sm:mr-21 w-60 mt-0.75 !rounded-lg !overflow-hidden"
></div>
<a-skeleton-input
v-else
class="!h-8.5 !xs:h-12 !xs:bg-white !sm:mr-21 !w-60 mt-0.75 !rounded-lg !overflow-hidden"
class="!h-8.5 !xs:h-12 !xs:bg-white sm:mr-21 w-60 mt-0.75 !rounded-lg !overflow-hidden"
active
size="small"
/>
@ -730,13 +733,14 @@ export default {
<LazySmartsheetDivDataCell
v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="!bg-white rounded-lg !w-[20rem] border-1 overflow-hidden border-gray-200 px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
class="bg-white rounded-lg w-80 border-1 overflow-hidden border-gray-200 px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(col)"
v-model="_row.row[col.title]"
:row="_row"
:column="col"
:read-only="readOnly"
/>
<LazySmartsheetCell
@ -745,7 +749,7 @@ export default {
:column="col"
:edit-enabled="true"
:active="true"
:read-only="isPublic"
:read-only="readOnly"
@update:model-value="changedColumns.add(col.title)"
/>
</LazySmartsheetDivDataCell>

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

@ -557,6 +557,8 @@ const {
return true
}
if (isExpandedCellInputExist()) return
// skip keyboard event handling if there is a drawer / modal
if (isDrawerOrModalExist()) {
return true
@ -565,7 +567,9 @@ const {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
const altOrOptionKey = e.altKey
if (e.key === ' ') {
if (isCellActive.value && !editEnabled.value && hasEditPermission.value && activeCell.row !== null) {
const isRichModalOpen = isExpandedCellInputExist()
if (isCellActive.value && !editEnabled.value && hasEditPermission.value && activeCell.row !== null && !isRichModalOpen) {
e.preventDefault()
const row = dataRef.value[activeCell.row]
expandForm?.(row)
@ -761,6 +765,9 @@ onClickOutside(tableBodyEl, (e) => {
if (activeCell.row === null || activeCell.col === null) return
const isRichModalOpen = isExpandedCellInputExist()
if (isRichModalOpen) return
const activeCol = fields.value[activeCell.col]
if (editEnabled.value && (isVirtualCol(activeCol) || activeCol.uidt === UITypes.JSON)) return
@ -1061,14 +1068,18 @@ useEventListener(document, 'mouseup', () => {
/** handle keypress events */
useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
if (e.key === 'Alt') {
const isRichModalOpen = isExpandedCellInputExist()
if (e.key === 'Alt' && !isRichModalOpen) {
altModifier.value = true
}
})
/** handle keypress events */
useEventListener(document, 'keyup', async (e: KeyboardEvent) => {
if (e.key === 'Alt') {
const isRichModalOpen = isExpandedCellInputExist()
if (e.key === 'Alt' && !isRichModalOpen) {
altModifier.value = false
disableUrlOverlay.value = false
}
@ -1793,7 +1804,7 @@ onKeyStroke('ArrowDown', onDown)
<template #overlay>
<div class="relative overflow-visible min-h-17 w-10">
<div
class="absolute -top-19 flex flex-col h-34.5 w-70 bg-white rounded-lg justify-start overflow-hidden"
class="absolute -top-19 flex flex-col h-34.5 w-70 bg-white rounded-lg border-1 border-gray-200 justify-start overflow-hidden"
style="box-shadow: 0px 4px 6px -2px rgba(0, 0, 0, 0.06), 0px -12px 16px -4px rgba(0, 0, 0, 0.1)"
:class="{
'-left-44': !isAddNewRecordGridMode,
@ -1836,7 +1847,7 @@ onKeyStroke('ArrowDown', onDown)
</div>
</template>
<template #icon>
<component :is="iconMap.arrowUp" class="text-gray-600 h-4" />
<component :is="iconMap.arrowUp" class="text-gray-600 h-4 w-4" />
</template>
</a-dropdown-button>
</div>

8
packages/nc-gui/components/smartsheet/grid/usePaginationShortcuts.ts

@ -26,6 +26,8 @@ const usePaginationShortcuts = ({
}
const onLeft = async (e: KeyboardEvent) => {
if (isExpandedCellInputExist()) return
if (!e.altKey) return
e.preventDefault()
@ -36,6 +38,8 @@ const usePaginationShortcuts = ({
}
const onRight = async (e: KeyboardEvent) => {
if (isExpandedCellInputExist()) return
if (!e.altKey) return
e.preventDefault()
@ -47,6 +51,8 @@ const usePaginationShortcuts = ({
}
const onDown = async (e: KeyboardEvent) => {
if (isExpandedCellInputExist()) return
if (!e.altKey) return
e.preventDefault()
@ -56,6 +62,8 @@ const usePaginationShortcuts = ({
}
const onUp = async (e: KeyboardEvent) => {
if (isExpandedCellInputExist()) return
if (!e.altKey) return
e.preventDefault()

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

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

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

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

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

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

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

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

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

@ -82,13 +82,16 @@ if (!localValue.value && allowEmpty !== true) {
<a-select-option v-for="option in options" :key="option.value" :label="option.label" :value="option.value">
<div class="flex gap-2 items-center items-center h-full">
<component :is="option.icon" class="min-w-5 !mx-0" />
<div
class="min-w-0 text-ellipsis overflow-hidden select-none"
<NcTooltip
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
class="max-w-[15rem] truncate select-none"
show-on-truncate-only
>
{{ option.label }}
</div>
<template #title> {{ option.label }}</template>
<template #default>
{{ option.label }}
</template>
</NcTooltip>
</div>
</a-select-option>
</NcSelect>

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

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

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

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

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

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

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

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

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

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

70
packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue

@ -239,44 +239,48 @@ const onDelete = async () => {
<LazySmartsheetToolbarExportSubActions />
</NcSubMenu>
<NcDivider />
</template>
<NcSubMenu v-if="isUIAllowed('viewCreateOrEdit')" key="lock-type" class="scrollbar-thin-dull max-h-90vh overflow-auto !py-0">
<template #title>
<div
v-e="[
'c:navdraw:preview-as',
{
sidebar: props.inSidebar,
},
]"
class="flex flex-row items-center gap-x-3"
>
<div>
{{ $t('labels.viewMode') }}
</div>
<div class="nc-base-menu-item flex !flex-shrink group !py-1 !px-1 rounded-md bg-brand-50">
<LazySmartsheetToolbarLockType
hide-tick
:type="lockType"
class="flex nc-view-actions-lock-type !text-brand-500 !flex-shrink"
/>
<template v-if="isUIAllowed('viewCreateOrEdit')">
<NcDivider />
<NcSubMenu key="lock-type" class="scrollbar-thin-dull max-h-90vh overflow-auto !py-0">
<template #title>
<div
v-e="[
'c:navdraw:preview-as',
{
sidebar: props.inSidebar,
},
]"
class="flex flex-row items-center gap-x-3"
>
<div>
{{ $t('labels.viewMode') }}
</div>
<div class="nc-base-menu-item flex !flex-shrink group !py-1 !px-1 rounded-md bg-brand-50">
<LazySmartsheetToolbarLockType
hide-tick
:type="lockType"
class="flex nc-view-actions-lock-type !text-brand-500 !flex-shrink"
/>
</div>
<div class="flex flex-grow"></div>
</div>
<div class="flex flex-grow"></div>
</div>
</template>
</template>
<template #expandIcon></template>
<div class="flex py-3 px-4 font-bold uppercase text-xs text-gray-500">{{ $t('labels.viewMode') }}</div>
<a-menu-item class="!mx-1 !py-2 !rounded-md nc-view-action-lock-subaction">
<LazySmartsheetToolbarLockType :type="LockType.Collaborative" @click="changeLockType(LockType.Collaborative)" />
</a-menu-item>
<template #expandIcon></template>
<div class="flex py-3 px-4 font-bold uppercase text-xs text-gray-500">{{ $t('labels.viewMode') }}</div>
<a-menu-item class="!mx-1 !py-2 !rounded-md nc-view-action-lock-subaction">
<LazySmartsheetToolbarLockType :type="LockType.Collaborative" @click="changeLockType(LockType.Collaborative)" />
</a-menu-item>
<a-menu-item class="!mx-1 !py-2 !rounded-md nc-view-action-lock-subaction">
<LazySmartsheetToolbarLockType :type="LockType.Locked" @click="changeLockType(LockType.Locked)" />
</a-menu-item>
</NcSubMenu>
</template>
<a-menu-item class="!mx-1 !py-2 !rounded-md nc-view-action-lock-subaction">
<LazySmartsheetToolbarLockType :type="LockType.Locked" @click="changeLockType(LockType.Locked)" />
</a-menu-item>
</NcSubMenu>
<template v-if="!view.is_default">
<NcDivider />
<NcTooltip v-if="lockType === LockType.Locked">

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

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

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

@ -127,7 +127,6 @@ const openListDlg = () => {
@click.stop="openListDlg"
/>
</div>
<LazyVirtualCellComponentsListItems
v-if="listItemsDlg || childListDlg"
v-model="listItemsDlg"

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

@ -110,9 +110,13 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
>
<template v-if="lookupColumn">
<!-- Render virtual cell -->
<div v-if="isVirtualCol(lookupColumn)">
<div v-if="isVirtualCol(lookupColumn)" class="flex">
<!-- If non-belongs-to LTAR column then pass the array value, else iterate and render -->
<template
v-if="lookupColumn.uidt === UITypes.LinkToAnotherRecord && lookupColumn.colOptions.type === RelationTypes.BELONGS_TO"
v-if="
lookupColumn.uidt !== UITypes.LinkToAnotherRecord ||
(lookupColumn.uidt === UITypes.LinkToAnotherRecord && lookupColumn.colOptions.type === RelationTypes.BELONGS_TO)
"
>
<LazySmartsheetVirtualCell
v-for="(v, i) of arrValue"

3
packages/nc-gui/components/virtual-cell/QrCode.vue

@ -92,9 +92,10 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning } = us
:style="{ height: rowHeight ? `${rowHeight * 1.4}rem` : `1.4rem` }"
:src="qrCode"
:alt="$t('title.qrCode')"
class="min-w-[1.4em]"
@click="showQrModal"
/>
<img v-else-if="showQrCode" class="mx-auto" :src="qrCode" :alt="$t('title.qrCode')" @click="showQrModal" />
<img v-else-if="showQrCode" class="mx-auto min-w-[1.4em]" :src="qrCode" :alt="$t('title.qrCode')" @click="showQrModal" />
</div>
<div v-if="showEditNonEditableFieldWarning" class="text-left text-wrap mt-2 text-[#e65100] text-xs">
{{ $t('msg.warning.nonEditableFields.computedFieldUnableToClear') }}

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

@ -166,6 +166,7 @@ const isDataExist = computed<boolean>(() => {
const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
if (isSharedBase.value) return
if (readonly.value) return
if (isPublic.value && !isForm.value) return
if (isNew.value || isChildrenListLinked.value[parseInt(id)]) {

5
packages/nc-gui/components/virtual-cell/components/ListItem.vue

@ -77,6 +77,7 @@ const attachments: ComputedRef<Attachment[]> = computed(() => {
'!bg-white': isLoading,
'!border-1': isLinked && !isLoading,
'!hover:border-gray-400': !isLinked,
'!cursor-auto !hover:bg-white': readonly,
}"
:body-style="{ padding: 0 }"
:hoverable="false"
@ -109,7 +110,7 @@ const attachments: ComputedRef<Attachment[]> = computed(() => {
v-if="isLinked && !isLoading"
class="text-brand-500 text-0.875"
:class="{
'!group-hover:mr-12': fields.length === 0,
'!group-hover:mr-12': fields.length === 0 && !readonly,
}"
>
<LinkIcon class="w-4 h-4" />
@ -118,7 +119,7 @@ const attachments: ComputedRef<Attachment[]> = computed(() => {
<MdiLoading
v-else-if="isLoading"
:class="{
'!group-hover:mr-8': fields.length === 0,
'!group-hover:mr-8': fields.length === 0 && !readonly,
}"
class="w-6 h-6 !text-brand-500 animate-spin"
/>

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

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

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

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

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

@ -34,10 +34,6 @@ export function useData(args: {
}) {
const { meta, viewMeta, formattedData, paginationData, callbacks } = args
if (!meta) {
throw new Error('Table meta is not available')
}
const { t } = useI18n()
const { getMeta, metas } = useMetas()

1
packages/nc-gui/composables/useGlobal/state.ts

@ -109,6 +109,7 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
automationLogLevel: 'OFF',
disableEmailAuth: false,
dashboardPath: '/dashboard',
inviteOnlySignup: false,
})
/** reactive token payload */

1
packages/nc-gui/composables/useGlobal/types.ts

@ -34,6 +34,7 @@ export interface AppInfo {
disableEmailAuth: boolean
mainSubDomain?: string
dashboardPath: string
inviteOnlySignup: boolean
}
export interface StoredState {

4
packages/nc-gui/composables/useMultiSelect/index.ts

@ -495,6 +495,10 @@ export function useMultiSelect(
return true
}
if (isExpandedCellInputExist()) {
return
}
if (!isCellActive.value || activeCell.row === null || activeCell.col === null) {
return
}

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

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

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

@ -42,10 +42,6 @@ export function useViewData(
const meta = computed(() => _meta.value || activeTable.value)
const metaId = computed(() => _meta.value?.id || activeTableId.value)
if (!meta.value) {
throw new Error('Table meta is not available')
}
const { t } = useI18n()
const optimisedQuery = useState('optimisedQuery', () => true)

131
packages/nc-gui/helpers/dbTiptapExtensions/links.ts

@ -0,0 +1,131 @@
import TiptapLink from '@tiptap/extension-link'
import { mergeAttributes } from '@tiptap/core'
import { Plugin, TextSelection } from 'prosemirror-state'
import type { AddMarkStep, Step } from 'prosemirror-transform'
export const Link = TiptapLink.extend({
addOptions() {
return {
openOnClick: true,
linkOnPaste: true,
autolink: true,
protocols: [],
HTMLAttributes: {
target: '_blank',
rel: 'noopener noreferrer nofollow',
class: null,
},
validate: undefined,
internal: false,
}
},
addAttributes() {
return {
href: {
default: null,
},
target: {
default: this.options.HTMLAttributes.target,
},
class: {
default: this.options.HTMLAttributes.class,
},
}
},
renderHTML({ HTMLAttributes }) {
const attr = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)
return ['a', attr, 0]
},
addKeyboardShortcuts() {
return {
'Mod-j': () => {
const selection = this.editor.view.state.selection
this.editor
.chain()
.toggleLink({
href: '',
})
.setTextSelection(selection.to)
.run()
setTimeout(() => {
const linkInput = document.querySelector('.nc-text-area-rich-link-option-input')
if (linkInput) {
;(linkInput as any).focus()
}
}, 100)
},
'Space': () => {
// If we press space twice we stop the link mark and have normal text
const editor = this.editor
const selection = editor.view.state.selection
const nodeBefore = selection.$to.nodeBefore
const nodeAfter = selection.$to.nodeAfter
if (!nodeBefore) return false
const nodeBeforeText = nodeBefore.text!
// If we are not inside a link, we don't do anything
if (
!nodeBefore?.marks.some((mark) => mark.type.name === 'link') ||
nodeAfter?.marks.some((mark) => mark.type.name === 'link')
) {
return false
}
// Last text character should be a space
if (nodeBeforeText[nodeBeforeText.length - 1] !== ' ') {
return false
}
editor.view.dispatch(
editor.view.state.tr.removeMark(selection.$to.pos - 1, selection.$to.pos, editor.view.state.schema.marks.link),
)
return true
},
} as any
},
addProseMirrorPlugins() {
return [
// To have proseMirror plugins from the parent extension
...(this.parent?.() ?? []),
new Plugin({
//
// Put cursor at the end of the link when we add a link
//
appendTransaction: (transactions, _, newState) => {
try {
if (transactions.length !== 1) return null
const steps = transactions[0].steps
if (steps.length !== 1) return null
const step: Step = steps[0] as Step
const stepJson = step.toJSON()
// Ignore we are not adding a mark(i.e link, bold, etc)
if (stepJson.stepType !== 'addMark') return null
const addMarkStep: AddMarkStep = step as AddMarkStep
if (!addMarkStep) return null
if (addMarkStep.from === addMarkStep.to) return null
if (addMarkStep.mark.type.name !== 'link') return null
const { tr } = newState
return tr.setSelection(new TextSelection(tr.doc.resolve(addMarkStep.to)))
} catch (e) {
console.error(e)
return null
}
},
}),
]
},
}).configure({
openOnClick: false,
})

186
packages/nc-gui/helpers/dbTiptapExtensions/task-item.ts

@ -0,0 +1,186 @@
import type { KeyboardShortcutCommand } from '@tiptap/core'
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core'
import type { Node as ProseMirrorNode } from '@tiptap/pm/model'
export interface TaskItemOptions {
onReadOnlyChecked?: (node: ProseMirrorNode, checked: boolean) => boolean
nested: boolean
HTMLAttributes: Record<string, any>
taskListTypeName: string
}
export const inputRegex = /^\s*\[( |x)?\]\s$/i
export const TaskItem = Node.create<TaskItemOptions>({
name: 'taskItem',
addOptions() {
return {
nested: false,
HTMLAttributes: {},
taskListTypeName: 'taskList',
}
},
content() {
return this.options.nested ? 'paragraph block*' : 'paragraph+'
},
defining: true,
addAttributes() {
return {
checked: {
default: false,
keepOnSplit: false,
parseHTML: (element) => element.getAttribute('data-checked') === 'true',
renderHTML: (attributes) => ({
'data-checked': attributes.checked,
}),
},
}
},
parseHTML() {
return [
{
tag: `li[data-type="${this.name}"]`,
priority: 51,
},
]
},
renderHTML({ node, HTMLAttributes }) {
return [
'li',
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
'data-type': this.name,
}),
[
'label',
[
'input',
{
type: 'checkbox',
checked: node.attrs.checked ? 'checked' : null,
},
],
['span'],
],
['div', 0],
]
},
addKeyboardShortcuts() {
const shortcuts: {
[key: string]: KeyboardShortcutCommand
} = {
'Enter': () => this.editor.commands.splitListItem(this.name),
'Shift-Tab': () => this.editor.commands.liftListItem(this.name),
}
if (!this.options.nested) {
return shortcuts
}
return {
...shortcuts,
Tab: () => this.editor.commands.sinkListItem(this.name),
}
},
addNodeView() {
return ({ node, HTMLAttributes, getPos, editor }) => {
const listItem = document.createElement('li')
const checkboxWrapper = document.createElement('label')
const checkboxStyler = document.createElement('span')
const checkbox = document.createElement('input')
const content = document.createElement('div')
checkboxWrapper.contentEditable = 'false'
checkbox.type = 'checkbox'
checkbox.addEventListener('change', (event) => {
// if the editor isn’t editable and we don't have a handler for
// readonly checks we have to undo the latest change
if (!editor.isEditable && !this.options.onReadOnlyChecked) {
checkbox.checked = !checkbox.checked
return
}
const { checked } = event.target as any
if (editor.isEditable && typeof getPos === 'function') {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.command(({ tr }) => {
const position = getPos()
const currentNode = tr.doc.nodeAt(position)
tr.setNodeMarkup(position, undefined, {
...currentNode?.attrs,
checked,
})
return true
})
.run()
}
if (!editor.isEditable && this.options.onReadOnlyChecked) {
// Reset state if onReadOnlyChecked returns false
if (!this.options.onReadOnlyChecked(node, checked)) {
checkbox.checked = !checkbox.checked
}
}
})
Object.entries(this.options.HTMLAttributes).forEach(([key, value]) => {
listItem.setAttribute(key, value)
})
listItem.dataset.checked = node.attrs.checked
if (node.attrs.checked) {
checkbox.setAttribute('checked', 'checked')
}
checkboxWrapper.append(checkbox, checkboxStyler)
listItem.append(checkboxWrapper, content)
Object.entries(HTMLAttributes).forEach(([key, value]) => {
listItem.setAttribute(key, value)
})
return {
dom: listItem,
contentDOM: content,
update: (updatedNode) => {
if (updatedNode.type !== this.type) {
return false
}
listItem.dataset.checked = updatedNode.attrs.checked
if (updatedNode.attrs.checked) {
checkbox.setAttribute('checked', 'checked')
} else {
checkbox.removeAttribute('checked')
}
return true
},
}
}
},
addInputRules() {
return [
wrappingInputRule({
find: inputRegex,
type: this.type,
getAttributes: (match) => ({
checked: match[match.length - 1].toLowerCase() === 'x',
}),
}),
]
},
})

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

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

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

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

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

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

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "إرسال",
"create": "إنشاء",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Duplicate",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "جدول فرعي",
"childColumn": "عمود فرعي",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record",
"links": "Links",
"onUpdate": "عند التحديث",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "تعطيل قاعدة مشتركة",
"enable": "أي شخص لديه الرابط",
"link": "رابط قاعدة مشتركة"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Paste operation is not supported on the active cell",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "জমিন",
"create": "সি",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Duplicate",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "Child table",
"childColumn": "Child column",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record",
"links": "Links",
"onUpdate": "আপড",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "ভগ করস অকষম করন",
"enable": "লিক সহ যউ",
"link": "ভগ করস লিক"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Paste operation is not supported on the active cell",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "Potvrdit",
"create": "Vytvořit",
"createEntity": "Vytvořit {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Vytvářím {entity}",
"details": "Podrobnosti",
"skip": "Skip",
"code": "Code",
"duplicate": "Duplikovat",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Stáhnout data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Kopírovat URL záznamu",
@ -567,6 +581,7 @@
"childTable": "Podřízená tabulka",
"childColumn": "Podřízený sloupec",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Odkaz na jiný záznam",
"links": "Links",
"onUpdate": "Při aktualizaci",
@ -651,7 +666,7 @@
"enablePublicAccess": "Povolit veřejný přístup",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Správa přístupu k projektu",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Seskupit dle",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Sdílet projekt",
"label": "Share Base",
"disable": "Zakázat sdílenou základnu",
"enable": "Kdokoli s odkazem",
"link": "Sdílený základní odkaz"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Operace Vložit není v aktivní buňce podporována.",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "Indsend",
"create": "Opret",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Duplikat",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "Undertabel",
"childColumn": "Underkolonner",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link til en anden post",
"links": "Links",
"onUpdate": "På opdatering",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "Deaktiver delt base",
"enable": "Nogen med linket",
"link": "Shared Base Link."
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Indsæt er ikke understøttet på den aktive celle",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "Bestätigen",
"create": "Erstellen",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Duplizieren",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "Child-Tabelle",
"childColumn": "Child-Spalte",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link zu einem anderen Datensatz",
"links": "Links",
"onUpdate": "Update",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "Freigegebene Datenbank deaktivieren",
"enable": "Jeder mit dem Link",
"link": "Freigegebene Datenbank-Link"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Der Vorgang Einfügen wird auf der aktiven Zelle nicht unterstützt",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "Submit",
"create": "Create",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Duplicate",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "Child table",
"childColumn": "Child field",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record",
"links": "Links",
"onUpdate": "On Update",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "Disable shared base",
"enable": "Anyone with the link",
"link": "Shared base link"

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

@ -1,45 +1,45 @@
{
"dashboards": {
"create_new_dashboard_project": "Create New Interface",
"connect_data_sources": "Connect data sources",
"alert": "Alert",
"alert-message": "No databases have been connected. Connect database bases to build interfaces. Skip this step and add databases from the base home page later.",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "Select Database Bases that you want to link to this Interface.",
"create_interface": "Create interface",
"project_name": "Base Name",
"connect": "Connect",
"create_new_dashboard_project": "Crear nueva interfaz",
"connect_data_sources": "Conectar bases de datos",
"alert": "Alerta",
"alert-message": "No se ha conectado ninguna base de datos. Conecte las bases de datos para crear interfaces. Omita este paso y luego añada bases de datos desde la página de inicio de la base.",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "Seleccione las bases de datos que desea vincular a esta interfaz.",
"create_interface": "Crear interfaz",
"project_name": "Nombre de la base",
"connect": "Conectar",
"buttonActionTypes": {
"open_external_url": "Open external link",
"delete_record": "Delete record",
"update_record": "Update record",
"open_layout": "Open layout"
"open_external_url": "Abrir enlace externo",
"delete_record": "Borrar fila",
"update_record": "Actualizar fila",
"open_layout": "Abrir diagrama"
},
"widgets": {
"static_text": "Text",
"chart": "Chart",
"table": "Table",
"image": "Image",
"map": "Map",
"button": "Button",
"number": "Number",
"bar_chart": "Bar Chart",
"line_chart": "Line Chart",
"area_chart": "Area Chart",
"pie_chart": "Pie Chart",
"donut_chart": "Donut Chart",
"scatter_plot": "Scatter Plot",
"bubble_chart": "Bubble Chart",
"radar_chart": "Radar Chart",
"polar_area_chart": "Polar Area Chart",
"radial_bar_chart": "Radial Bar Chart",
"heatmap_chart": "Heatmap Chart",
"treemap_chart": "Treemap Chart",
"box_plot_chart": "Box Plot Chart",
"candlestick_chart": "Candlestick Chart"
"static_text": "Texto",
"chart": "Gráfico",
"table": "Tabla",
"image": "Imagen",
"map": "Mapa",
"button": "Botón",
"number": "Número",
"bar_chart": "Gráfico de barras",
"line_chart": "Gráfico de Líneas",
"area_chart": "Gráfico de área",
"pie_chart": "Gráfico de tarta",
"donut_chart": "Gráfico de dona",
"scatter_plot": "Gráfico de dispersión",
"bubble_chart": "Gráfico de burbujas",
"radar_chart": "Gráfico de radar",
"polar_area_chart": "Gráfico de área polar",
"radial_bar_chart": "Gráfico de barra radial",
"heatmap_chart": "Mapa de calor",
"treemap_chart": "Gráfico de árbol",
"box_plot_chart": "Gráfico de cajas",
"candlestick_chart": "Gráfico de velas"
}
},
"general": {
"quit": "Quit",
"quit": "Salir",
"home": "Inicio",
"load": "Cargar",
"open": "Abrir",
@ -47,24 +47,24 @@
"yes": "Sí",
"no": "No",
"ok": "Ok",
"back": "Back",
"back": "Volver",
"and": "Y",
"or": "O",
"add": "Agregar",
"edit": "Editar",
"link": "Link",
"links": "Links",
"link": "Enlace",
"links": "Enlaces",
"remove": "Eliminar",
"import": "Import",
"logout": "Log Out",
"import": "Importar",
"logout": "Cerrar sesión",
"empty": "Empty",
"changeIcon": "Change Icon",
"changeIcon": "Cambiar icono",
"save": "Salvar",
"available": "Available",
"abort": "Abort",
"saving": "Saving",
"available": "Disponible",
"abort": "Cancelar",
"saving": "Guardando",
"cancel": "Cancelar",
"null": "Null",
"null": "Nulo",
"escape": "Escape",
"hex": "Hex",
"clear": "Clear",
@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "Enviar",
"create": "Crear",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Duplicar",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "Tabla hija",
"childColumn": "Columna hija",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Enlace a otro registro",
"links": "Links",
"onUpdate": "En actualización",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "Deshabilitar la base compartida",
"enable": "Cualquiera con el link",
"link": "Enlace de base compartida"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "No se admite la operación de pegado en la celda activa",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "Bidali",
"create": "Sortu",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Bikoiztu",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "Child table",
"childColumn": "Child column",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record",
"links": "Links",
"onUpdate": "On Update",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "Disable shared base",
"enable": "Anyone with the link",
"link": "Shared base link"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Paste operation is not supported on the active cell",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "ارسال",
"create": "ایجاد",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "کپی کردن",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "جدول فرزند",
"childColumn": "ستون فرزند",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "لینک به رکورد دیگر",
"links": "Links",
"onUpdate": "هنگام به روز رسانی",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "غیرفعال کردن پایگاه مشترک",
"enable": "هر کسی لینک را داشته باشد",
"link": "لینک پایگاه مشترک"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "عملیات جایگذاری در سلول فعال پشتیبانی نمیشود",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "Lähetä",
"create": "Luoda",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Duplikaatti",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "Lapsipöytä",
"childColumn": "Lapsipylväs",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Linkki toiseen tietueeseen",
"links": "Links",
"onUpdate": "Päivitys",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "Poista jaettu pohja",
"enable": "Jokainen, jolla on linkki",
"link": "Jaettu peruslinkki"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Liitä-toimintoa ei tueta aktiivisessa solussa.",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "Soumettre",
"create": "Créer",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Dupliquer",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "Table enfant",
"childColumn": "Colonne enfant",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Lien vers un autre enregistrement",
"links": "Links",
"onUpdate": "Mise à jour en cours",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "Désactiver la base partagée",
"enable": "N'importe qui disposant du lien",
"link": "Partager le lien de la base"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "L'opération de collage n'est pas prise en charge sur la cellule active",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "שלח",
"create": "צור",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "שכפל",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "טבלת ילדים",
"childColumn": "טור ילדים",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record",
"links": "Links",
"onUpdate": "על עדכון",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "השבת בסיס משותף",
"enable": "כל אחד עם הקישור",
"link": "קישור בסיס משותף"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Paste operation is not supported on the active cell",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "परसत करन",
"create": "सजन करन",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Duplicate",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "Child table",
"childColumn": "Child column",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Link to another record",
"links": "Links",
"onUpdate": "On Update",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "स आधर अकषम कर",
"enable": "कई भयकििसकस लिक ह",
"link": "स आधर लिक"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Paste operation is not supported on the active cell",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "podnijeti",
"create": "Stvoriti",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Dupliciraj",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "Dječji stol",
"childColumn": "Dječji stupac",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Poveži s drugim zapisom",
"links": "Links",
"onUpdate": "Na ažuriranje",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "Onemogući zajedničku bazu",
"enable": "Bilo tko s linkom",
"link": "Zajednička baza veza"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Operacija \"lijepljenje\" nije podržana u aktivnoj ćeliji",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "Kirim",
"create": "Membuat",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Duplikat",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "Tabel Anak",
"childColumn": "Kolom anak.",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Tautan ke catatan lain",
"links": "Links",
"onUpdate": "Pada pembaruan",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "Nonaktifkan basis bersama",
"enable": "Siapa pun dengan tautannya",
"link": "Tautan dasar bersama"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Operasi tempel tidak didukung pada sel aktif",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "Invia",
"create": "Crea",
"createEntity": "Create {entity}",
@ -82,6 +83,7 @@
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "Duplicato",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "Sottotabella",
"childColumn": "Sottocolonna",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "Collegamento a un altro record",
"links": "Links",
"onUpdate": "All'aggiornamento",
@ -651,7 +666,7 @@
"enablePublicAccess": "Enable Public Access",
"doYouWantToSaveTheChanges": "Do you want to save the changes ?",
"editingAccess": "Editing access",
"enabledPublicViewing": "Enable public viewing",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "Restrict access with password",
"manageProjectAccess": "Manage Base Access",
"allowDownload": "Allow Download",
@ -704,7 +719,7 @@
"groupBy": "Group By",
"addSubGroup": "Add subgroup",
"shareBase": {
"label": "Share base",
"label": "Share Base",
"disable": "Disabilitare la base condivisa",
"enable": "Chiunque con il link",
"link": "Collegamento base condiviso"
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "L'operazione di incollamento non è supportata sulla cella attiva.",
"roles": {

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

@ -1,32 +1,32 @@
{
"dashboards": {
"create_new_dashboard_project": "Create New Interface",
"connect_data_sources": "Connect data sources",
"alert": "Alert",
"alert-message": "No databases have been connected. Connect database bases to build interfaces. Skip this step and add databases from the base home page later.",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "Select Database Bases that you want to link to this Interface.",
"create_interface": "Create interface",
"project_name": "Base Name",
"connect": "Connect",
"create_new_dashboard_project": "インターフェースを作成",
"connect_data_sources": "データソースに接続",
"alert": "アラート",
"alert-message": "データベースが接続されていません。DBベースを接続してインターフェースを構成してください。スキップして後でベースのホームページを追加することもできます。",
"select_database_projects_that_you_want_to_link_to_this_dashboard_projects": "このインターフェースに接続するプロジェクトを選択します。",
"create_interface": "インターフェースを作成",
"project_name": "プロジェクト名",
"connect": "接続",
"buttonActionTypes": {
"open_external_url": "Open external link",
"delete_record": "Delete record",
"update_record": "Update record",
"open_layout": "Open layout"
"open_external_url": "外部リンクを開く",
"delete_record": "レコードを削除",
"update_record": "レコードを更新",
"open_layout": "レイアウトを開く"
},
"widgets": {
"static_text": "Text",
"chart": "Chart",
"table": "Table",
"static_text": "テキスト",
"chart": "チャート",
"table": "",
"image": "Image",
"map": "Map",
"button": "Button",
"number": "Number",
"bar_chart": "Bar Chart",
"line_chart": "Line Chart",
"area_chart": "Area Chart",
"pie_chart": "Pie Chart",
"donut_chart": "Donut Chart",
"map": "マップ",
"button": "ボタン",
"number": "ナンバー",
"bar_chart": "棒グラフ",
"line_chart": "折れ線グラフ",
"area_chart": "面グラフ",
"pie_chart": "円グラフ",
"donut_chart": "ドーナツグラフ",
"scatter_plot": "Scatter Plot",
"bubble_chart": "Bubble Chart",
"radar_chart": "Radar Chart",
@ -39,7 +39,7 @@
}
},
"general": {
"quit": "Quit",
"quit": "終了",
"home": "ホーム",
"load": "読み込み",
"open": "開く",
@ -47,7 +47,7 @@
"yes": "はい",
"no": "いいえ",
"ok": "OK",
"back": "Back",
"back": "戻る",
"and": "と",
"or": "または",
"add": "追加",
@ -75,13 +75,15 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "送信",
"create": "作成",
"createEntity": "Create {entity}",
"createEntity": "{entity}を作成",
"creating": "Creating",
"creatingEntity": "Creating {entity}",
"details": "Details",
"skip": "Skip",
"code": "Code",
"duplicate": "複製",
"duplicating": "Duplicating",
"activate": "Activate",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "Download Data",
"blockQuote": "Block Quote",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
@ -423,6 +436,7 @@
"headerName": "Header Name",
"icon": "Icon",
"max": "Max",
"enableRichText": "Enable Rich Text",
"idColon": "Id:",
"copiedRecordURL": "Copied Record URL",
"copyRecordURL": "Copy Record URL",
@ -567,6 +581,7 @@
"childTable": "子テーブル",
"childColumn": "子カラム",
"childField": "Child field",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "他のレコードへのリンク",
"links": "Links",
"onUpdate": "更新中",
@ -1033,6 +1048,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "現在アクティブなセルでは貼り付けはサポートされていません",
"roles": {

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

@ -75,6 +75,7 @@
"matterMost": "Mattermost",
"twilio": "Twilio",
"whatsappTwilio": "WhatsApp Twilio",
"quote": "Quote",
"submit": "제출",
"create": "생성",
"createEntity": "{entity} 생성",
@ -82,6 +83,7 @@
"creatingEntity": "{entity} 생성 중",
"details": "세부사항",
"skip": "건너뛰기",
"code": "Code",
"duplicate": "복사",
"duplicating": "복사 중",
"activate": "활성화",
@ -411,7 +413,18 @@
}
},
"labels": {
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strike": "Strike",
"taskList": "Task List",
"bulletList": "Bullet List",
"numberedList": "Numbered List",
"downloadData": "데이터 다운로드",
"blockQuote": "Block Quote",
"noToken": "토큰 없음",
"tokenLimit": "사용자당 하나의 토큰만 허용됩니다",
"duplicateAttachment": "이름이 {filename}인 파일이 이미 연결됨",
@ -423,6 +436,7 @@
"headerName": "헤더 이름",
"icon": "아이콘",
"max": "최대",
"enableRichText": "Enable Rich Text",
"idColon": "ID:",
"copiedRecordURL": "레코드 URL을 복사했습니다.",
"copyRecordURL": "레코드 URL 복사",
@ -567,6 +581,7 @@
"childTable": "자식 테이블",
"childColumn": "자식 컬럼",
"childField": "자식 필드",
"joinCloudForFree": "Join Cloud for Free",
"linkToAnotherRecord": "다른 레코드에 링크",
"links": "링크",
"onUpdate": "업데이트 시 ",
@ -651,7 +666,7 @@
"enablePublicAccess": "전체 공개",
"doYouWantToSaveTheChanges": "바뀐 내용을 저장할까요?",
"editingAccess": "수정 권한",
"enabledPublicViewing": "공개 보기 사용 설정",
"enabledPublicViewing": "Enable Public Viewing",
"restrictAccessWithPassword": "비밀번호로 액세스 제한",
"manageProjectAccess": "프로젝트 접근 권한",
"allowDownload": "다운로드 허용",
@ -704,7 +719,7 @@
"groupBy": "분류 기준",
"addSubGroup": "하위 그룹 추가",
"shareBase": {
"label": "프로젝트 공유",
"label": "Share Base",
"disable": "공유 베이스 비활성화",
"enable": "링크가 있는 모든 사용자",
"link": "공유 링크"
@ -1033,6 +1048,7 @@
"duplicateTable": "테이블을 복제하려고 합니다. 계속하시겠습니까?"
},
"info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "베이스가 마이그레이션되었습니다. 다시 시도하십시오.",
"pasteNotSupported": "활성 셀에서는 붙여넣기 작업이 지원되지 않습니다.",
"roles": {

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

Loading…
Cancel
Save