Browse Source

Merge branch 'develop' into fix/audit-log-changes

pull/9992/head
Raju Udava 2 days ago
parent
commit
35f0418ce1
  1. 13
      .github/workflows/release-pr.yml
  2. 29
      .github/workflows/uffizzi-preview.yml
  3. 2
      build-local-docker-image.sh
  4. 8
      packages/nc-gui/assets/style.scss
  5. 83
      packages/nc-gui/components/ai/PromptWithFields.vue
  6. 36
      packages/nc-gui/components/ai/Settings.vue
  7. 53
      packages/nc-gui/components/cell/AI.vue
  8. 23
      packages/nc-gui/components/cell/RichText.vue
  9. 8
      packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue
  10. 8
      packages/nc-gui/components/cell/SingleSelect.vue
  11. 170
      packages/nc-gui/components/cell/TextArea.vue
  12. 2
      packages/nc-gui/components/cell/attachment/index.vue
  13. 3
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  14. 14
      packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue
  15. 12
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  16. 2
      packages/nc-gui/components/general/IntegrationIcon.vue
  17. 6
      packages/nc-gui/components/general/Loader.vue
  18. 30
      packages/nc-gui/components/general/WorkspaceIcon.vue
  19. 9
      packages/nc-gui/components/nc/Button.vue
  20. 2
      packages/nc-gui/components/nc/List/RecordItem.vue
  21. 4
      packages/nc-gui/components/nc/Switch.vue
  22. 82
      packages/nc-gui/components/smartsheet/Cell.vue
  23. 29
      packages/nc-gui/components/smartsheet/Form.vue
  24. 2
      packages/nc-gui/components/smartsheet/Gallery.vue
  25. 2
      packages/nc-gui/components/smartsheet/Kanban.vue
  26. 7
      packages/nc-gui/components/smartsheet/PlainCell.vue
  27. 39
      packages/nc-gui/components/smartsheet/column/AiButtonOptions.vue
  28. 85
      packages/nc-gui/components/smartsheet/column/ButtonOptions.vue
  29. 90
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  30. 2
      packages/nc-gui/components/smartsheet/column/FormulaInputHelper.vue
  31. 24
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  32. 351
      packages/nc-gui/components/smartsheet/column/LongTextOptions.vue
  33. 10
      packages/nc-gui/components/smartsheet/column/UITypesOptionsWithSearch.vue
  34. 38
      packages/nc-gui/components/smartsheet/details/Fields.vue
  35. 4
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  36. 1
      packages/nc-gui/components/smartsheet/grid/GroupByTable.vue
  37. 75
      packages/nc-gui/components/smartsheet/grid/InfiniteTable.vue
  38. 16
      packages/nc-gui/components/smartsheet/grid/Table.vue
  39. 11
      packages/nc-gui/components/smartsheet/header/Cell.vue
  40. 4
      packages/nc-gui/components/smartsheet/header/CellIcon.ts
  41. 27
      packages/nc-gui/components/smartsheet/header/Menu.vue
  42. 7
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  43. 4
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  44. 22
      packages/nc-gui/components/smartsheet/topbar/ViewListDropdown.vue
  45. 2
      packages/nc-gui/components/virtual-cell/Button.vue
  46. 4
      packages/nc-gui/components/virtual-cell/HasMany.vue
  47. 4
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  48. 4
      packages/nc-gui/components/virtual-cell/components/ListItem.vue
  49. 6
      packages/nc-gui/components/workspace/integrations/ConnectionsTab.vue
  50. 106
      packages/nc-gui/components/workspace/integrations/IntegrationsTab.vue
  51. 8
      packages/nc-gui/components/workspace/integrations/forms/EditOrAddCommon.vue
  52. 12
      packages/nc-gui/components/workspace/integrations/forms/EditOrAddDatabase.vue
  53. 31
      packages/nc-gui/components/workspace/project/AiCreateProject.vue
  54. 7
      packages/nc-gui/composables/useBetaFeatureToggle.ts
  55. 6
      packages/nc-gui/composables/useColumnCreateStore.ts
  56. 3
      packages/nc-gui/composables/useData.ts
  57. 3
      packages/nc-gui/composables/useInfiniteData.ts
  58. 63
      packages/nc-gui/composables/useIntegrationsStore.ts
  59. 11
      packages/nc-gui/composables/useMultiSelect/index.ts
  60. 38
      packages/nc-gui/composables/useNocoAi.ts
  61. 33
      packages/nc-gui/composables/useViewAggregate.ts
  62. 12
      packages/nc-gui/helpers/parsers/parserHelpers.ts
  63. 18
      packages/nc-gui/helpers/tiptapExtensions/mention/FieldList.vue
  64. 1
      packages/nc-gui/lang/ar.json
  65. 1
      packages/nc-gui/lang/bn_IN.json
  66. 1
      packages/nc-gui/lang/cs.json
  67. 1
      packages/nc-gui/lang/da.json
  68. 1
      packages/nc-gui/lang/de.json
  69. 1
      packages/nc-gui/lang/es.json
  70. 1
      packages/nc-gui/lang/eu.json
  71. 1
      packages/nc-gui/lang/fa.json
  72. 1
      packages/nc-gui/lang/fi.json
  73. 3
      packages/nc-gui/lang/fr.json
  74. 1
      packages/nc-gui/lang/he.json
  75. 1
      packages/nc-gui/lang/hi.json
  76. 1
      packages/nc-gui/lang/hr.json
  77. 1
      packages/nc-gui/lang/hu.json
  78. 1
      packages/nc-gui/lang/id.json
  79. 1
      packages/nc-gui/lang/it.json
  80. 1
      packages/nc-gui/lang/ja.json
  81. 1
      packages/nc-gui/lang/km.json
  82. 1
      packages/nc-gui/lang/kn.json
  83. 1
      packages/nc-gui/lang/ko.json
  84. 1
      packages/nc-gui/lang/lv.json
  85. 1
      packages/nc-gui/lang/ml.json
  86. 1
      packages/nc-gui/lang/ne.json
  87. 23
      packages/nc-gui/lang/nl.json
  88. 1
      packages/nc-gui/lang/no.json
  89. 1
      packages/nc-gui/lang/pl.json
  90. 1
      packages/nc-gui/lang/pt.json
  91. 1
      packages/nc-gui/lang/pt_BR.json
  92. 1
      packages/nc-gui/lang/ro.json
  93. 1
      packages/nc-gui/lang/ru.json
  94. 1
      packages/nc-gui/lang/sk.json
  95. 1
      packages/nc-gui/lang/sl.json
  96. 1
      packages/nc-gui/lang/sv.json
  97. 1
      packages/nc-gui/lang/th.json
  98. 1
      packages/nc-gui/lang/tr.json
  99. 1
      packages/nc-gui/lang/uk.json
  100. 1
      packages/nc-gui/lang/vi.json
  101. Some files were not shown because too many files have changed in this diff Show More

13
.github/workflows/release-pr.yml

@ -107,9 +107,9 @@ jobs:
envsubst < .github/uffizzi/docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: preview-spec
name: preview-spec-docker-compose
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
@ -118,9 +118,9 @@ jobs:
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: preview-spec
name: preview-spec-event
path: event.json
retention-days: 2
@ -174,8 +174,9 @@ jobs:
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: preview-spec
name: preview-spec-event
path: event.json
retention-days: 2
overwrite: true

29
.github/workflows/uffizzi-preview.yml

@ -20,31 +20,14 @@ jobs:
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6
uses: actions/download-artifact@v4
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
if (matchArtifact === undefined) {
throw TypeError('Build Artifact not found!');
}
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
path: preview-spec
pattern: preview-spec-*
merge-multiple: true
- name: 'Accept event from first stage'
run: unzip preview-spec.zip event.json
run: cp preview-spec/event.json .
- name: Read Event into ENV
id: event
@ -58,7 +41,7 @@ jobs:
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ steps.event.outputs.ACTION != 'closed' }}
run: |
unzip preview-spec.zip docker-compose.rendered.yml
cp preview-spec/docker-compose.rendered.yml .
echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_OUTPUT
- name: Cache Rendered Compose File

2
build-local-docker-image.sh

@ -45,7 +45,7 @@ function copy_gui_artifacts() {
function package_nocodb() {
# build nocodb ( pack nocodb-sdk and nc-gui )
cd ${SCRIPT_DIR}/packages/nocodb
EE=true ${SCRIPT_DIR}/node_modules/@rspack/cli/bin --config ${SCRIPT_DIR}/packages/nocodb/rspack.config.js || ERROR="package_nocodb failed"
EE=true ${SCRIPT_DIR}/node_modules/@rspack/cli/bin/rspack.js --config ${SCRIPT_DIR}/packages/nocodb/rspack.config.js || ERROR="package_nocodb failed"
}
function build_image() {

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

@ -20,6 +20,14 @@
font-synthesis: none;
}
i {
font-synthesis: initial !important;
& * {
font-synthesis: initial !important;
}
}
html {
overflow: hidden;
}

83
packages/nc-gui/components/ai/PromptWithFields.vue

@ -3,6 +3,7 @@ import Placeholder from '@tiptap/extension-placeholder'
import StarterKit from '@tiptap/starter-kit'
import Mention from '@tiptap/extension-mention'
import { EditorContent, useEditor } from '@tiptap/vue-3'
import tippy from 'tippy.js'
import { type ColumnType, UITypes } from 'nocodb-sdk'
import FieldList from '~/helpers/tiptapExtensions/mention/FieldList'
import suggestion from '~/helpers/tiptapExtensions/mention/suggestion.ts'
@ -15,6 +16,7 @@ const props = withDefaults(
promptFieldTagClassName?: string
suggestionIconClassName?: string
placeholder?: string
readOnly?: boolean
}>(),
{
options: () => [],
@ -26,6 +28,7 @@ const props = withDefaults(
* @example: :placeholder="`Enter prompt here...\n\neg : Categorise this {Notes}`"
*/
placeholder: 'Write your prompt here...',
readOnly: false,
},
)
@ -38,7 +41,9 @@ const vModel = computed({
},
})
const { autoFocus } = toRefs(props)
const { autoFocus, readOnly } = toRefs(props)
const debouncedLoadMentionFieldTagTooltip = useDebounceFn(loadMentionFieldTagTooltip, 1000)
const editor = useEditor({
content: vModel.value,
@ -55,18 +60,25 @@ const editor = useEditor({
...suggestion(FieldList),
items: ({ query }) => {
if (query.length === 0) return props.options ?? []
return props.options?.filter((o) => o.title?.toLowerCase()?.includes(query.toLowerCase())) ?? []
return (
props.options?.filter(
(o) =>
o.title?.toLowerCase()?.includes(query.toLowerCase()) || `${o.title?.toLowerCase()}}` === query.toLowerCase(),
) ?? []
)
},
char: '{',
allowSpaces: true,
},
renderHTML: ({ node }) => {
const matchedOption = props.options?.find((option) => option.title === node.attrs.id)
const isAttachment = matchedOption?.uidt === UITypes.Attachment
return [
'span',
{
class: `prompt-field-tag ${
props.options?.find((option) => option.title === node.attrs.id)?.uidt === UITypes.Attachment ? '!bg-green-200' : ''
} ${props.promptFieldTagClassName}`,
'class': `prompt-field-tag ${isAttachment ? '!bg-green-200' : ''} ${props.promptFieldTagClassName}`,
'style': 'max-width: 100px; white-space: nowrap; overflow: hidden; display: inline-block; text-overflow: ellipsis;', // Enforces truncation
'data-tooltip': node.attrs.id, // Tooltip content
},
`${node.attrs.id}`,
]
@ -93,8 +105,10 @@ const editor = useEditor({
text = text.trim()
vModel.value = text
debouncedLoadMentionFieldTagTooltip()
},
editable: true,
editable: !readOnly.value,
autofocus: autoFocus.value,
editorProps: { scrollThreshold: 100 },
})
@ -141,15 +155,60 @@ onMounted(async () => {
}, 100)
}
})
const tooltipInstances: any[] = []
function loadMentionFieldTagTooltip() {
document.querySelectorAll('.nc-ai-prompt-with-fields .prompt-field-tag').forEach((el) => {
const tooltip = Object.values(el.attributes).find((attr) => attr.name === 'data-tooltip')
if (!tooltip || el.scrollWidth <= el.clientWidth) return
// Show tooltip only on truncate
const instance = tippy(el, {
content: `<div class="tooltip text-xs">${tooltip.value}</div>`,
placement: 'top',
allowHTML: true,
arrow: true,
animation: 'fade',
duration: 0,
maxWidth: '200px',
})
tooltipInstances.push(instance)
})
}
onMounted(() => {
debouncedLoadMentionFieldTagTooltip()
})
onBeforeUnmount(() => {
tooltipInstances.forEach((instance) => instance?.destroy())
tooltipInstances.length = 0
})
</script>
<template>
<div class="nc-ai-prompt-with-fields w-full">
<EditorContent ref="editorDom" :editor="editor" @keydown.alt.enter.stop @keydown.shift.enter.stop />
<NcButton size="xs" type="text" class="nc-prompt-with-field-suggestion-btn !px-1" @click.stop="newFieldSuggestionNode">
<NcButton
size="xs"
type="text"
class="nc-prompt-with-field-suggestion-btn !px-1"
:disabled="readOnly"
@click.stop="newFieldSuggestionNode"
>
<slot name="triggerIcon">
<GeneralIcon icon="ncPlusSquareSolid" class="text-nc-content-brand" :class="`${suggestionIconClassName}`" />
<GeneralIcon
icon="ncPlusSquareSolid"
class="text-nc-content-brand"
:class="[
`${suggestionIconClassName}`,
{
'opacity-75': readOnly,
},
]"
/>
</slot>
</NcButton>
</div>
@ -160,11 +219,11 @@ onMounted(async () => {
@apply relative;
.nc-prompt-with-field-suggestion-btn {
@apply absolute top-[1px] right-[1px];
@apply absolute top-[2px] right-[1px];
}
.prompt-field-tag {
@apply bg-gray-100 rounded-md px-1;
@apply bg-gray-100 rounded-md px-1 align-middle;
}
.ProseMirror {
@ -172,6 +231,10 @@ onMounted(async () => {
resize: vertical;
min-width: 100%;
max-height: min(800px, calc(100vh - 200px)) !important;
& > p {
@apply mr-3;
}
}
.ProseMirror-focused {

36
packages/nc-gui/components/ai/Settings.vue

@ -7,9 +7,11 @@ const props = withDefaults(
workspaceId: string
scope?: string
showTooltip?: boolean
isEditColumn?: boolean
}>(),
{
showTooltip: true,
isEditColumn: false,
},
)
@ -19,6 +21,8 @@ const vFkIntegrationId = useVModel(props, 'fkIntegrationId', emits)
const vModel = useVModel(props, 'model', emits)
const { isEditColumn } = toRefs(props)
// const vRandomness = useVModel(props, 'randomness', emits)
const { $api } = useNuxtApp()
@ -29,7 +33,7 @@ const lastIntegrationId = ref<string | null>(null)
const isDropdownOpen = ref(false)
const availableModels = ref<string[]>([])
const availableModels = ref<{ value: string; label: string }[]>([])
const isLoadingAvailableModels = ref<boolean>(false)
@ -50,10 +54,10 @@ const onIntegrationChange = async (newFkINtegrationId?: string) => {
try {
const response = await $api.integrations.endpoint(newFkINtegrationId, 'availableModels', {})
availableModels.value = response as string[]
availableModels.value = (response || []) as { value: string; label: string }[]
if (!vModel.value && availableModels.value.length > 0) {
vModel.value = availableModels.value[0]
vModel.value = availableModels.value[0].value
}
} catch (error) {
console.error(error)
@ -63,15 +67,19 @@ const onIntegrationChange = async (newFkINtegrationId?: string) => {
}
onMounted(async () => {
if (!vFkIntegrationId.value) {
if (!vFkIntegrationId.value && !isEditColumn.value) {
if (aiIntegrations.value.length > 0 && aiIntegrations.value[0].id) {
vFkIntegrationId.value = aiIntegrations.value[0].id
nextTick(() => {
onIntegrationChange()
})
}
} else {
} else if (vFkIntegrationId.value) {
lastIntegrationId.value = vFkIntegrationId.value
if (!vModel.value || !availableModels.value.length) {
onIntegrationChange()
}
}
})
</script>
@ -111,6 +119,7 @@ onMounted(async () => {
v-model:value="vFkIntegrationId"
class="w-full nc-select-shadow nc-ai-input"
size="middle"
placeholder="- select integration -"
@change="onIntegrationChange"
>
<a-select-option v-for="integration in aiIntegrations" :key="integration.id" :value="integration.id">
@ -150,20 +159,21 @@ onMounted(async () => {
v-model:value="vModel"
class="w-full nc-select-shadow nc-ai-input"
size="middle"
placeholder="- select model -"
:disabled="!vFkIntegrationId || availableModels.length === 0"
:loading="isLoadingAvailableModels"
>
<a-select-option v-for="md in availableModels" :key="md" :value="md">
<a-select-option v-for="md in availableModels" :key="md.label" :value="md.value">
<div class="w-full flex gap-2 items-center">
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>
{{ md }}
{{ md.label }}
</template>
{{ md }}
{{ md.label }}
</NcTooltip>
<component
:is="iconMap.check"
v-if="vModel === md"
v-if="vModel === md.value"
id="nc-selected-item-icon"
class="text-nc-content-purple-medium w-4 h-4"
/>
@ -198,4 +208,10 @@ onMounted(async () => {
</NcDropdown>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
:deep(.nc-select.ant-select) {
.ant-select-selector {
@apply !rounded-lg;
}
}
</style>

53
packages/nc-gui/components/cell/AI.vue

@ -13,6 +13,10 @@ const { generateRows, generatingRows, generatingColumnRows, aiIntegrations } = u
const { row } = useSmartsheetRowStoreOrThrow()
const { isUIAllowed } = useRoles()
const isPublic = inject(IsPublicInj, ref(false))
const meta = inject(MetaInj, ref())
const column = inject(ColumnInj) as Ref<
@ -40,9 +44,7 @@ const isAiEdited = ref(false)
const isFieldAiIntegrationAvailable = computed(() => {
const fkIntegrationId = column.value?.colOptions?.fk_integration_id
if (!fkIntegrationId) return false
return ncIsArrayIncludes(aiIntegrations.value, fkIntegrationId, 'id')
return !!fkIntegrationId
})
const pk = computed(() => {
@ -58,7 +60,7 @@ const generate = async () => {
ncIsString(column.value.colOptions?.output_column_ids) && column.value.colOptions.output_column_ids.split(',').length > 1
? column.value.colOptions.output_column_ids.split(',')
: []
const outputColumns = outputColumnIds.map((id) => meta.value?.columnsById[id])
const outputColumns = outputColumnIds.map((id) => meta.value?.columnsById?.[id]).filter(Boolean)
generatingRows.value.push(pk.value)
generatingColumnRows.value.push(column.value.id)
@ -76,11 +78,18 @@ const generate = async () => {
}
} else {
const obj: AIRecordType = resRow[column.value.title!]
if (obj && typeof obj === 'object') {
vModel.value = obj
setTimeout(() => {
isAiEdited.value = false
}, 100)
} else {
vModel.value = {
...(ncIsObject(vModel.value) ? vModel.value : {}),
isStale: false,
value: resRow[column.value.title!],
}
}
}
}
@ -99,10 +108,16 @@ const isLoading = computed(() => {
})
const handleSave = () => {
vModel.value = { ...vModel.value }
emits('save')
}
const debouncedSave = useDebounceFn(handleSave, 1000)
const isDisabledAiButton = computed(() => {
return !isFieldAiIntegrationAvailable.value || isLoading.value || isPublic.value || !isUIAllowed('dataEdit')
})
</script>
<template>
@ -113,20 +128,15 @@ const debouncedSave = useDebounceFn(handleSave, 1000)
'justify-center': isGrid && !isExpandedForm,
}"
>
<NcTooltip :disabled="isFieldAiIntegrationAvailable" class="flex">
<NcTooltip :disabled="isFieldAiIntegrationAvailable || isPublic || isUIAllowed('dataEdit')" class="flex">
<template #title>
{{ aiIntegrations.length ? $t('tooltip.aiIntegrationReConfigure') : $t('tooltip.aiIntegrationAddAndReConfigure') }}
</template>
<button
class="nc-cell-ai-button nc-cell-button h-7"
size="small"
:disabled="!isFieldAiIntegrationAvailable || isLoading"
@click.stop="generate"
>
<button class="nc-cell-ai-button nc-cell-button h-6" size="small" :disabled="isDisabledAiButton" @click.stop="generate">
<div class="flex items-center gap-1">
<GeneralLoader v-if="isLoading" size="regular" class="!text-nc-content-purple-dark" />
<GeneralIcon v-else icon="ncAutoAwesome" class="text-nc-content-purple-dark h-4 w-4" />
<span class="text-sm font-semibold">Generate</span>
<GeneralLoader v-if="isLoading" size="regular" />
<GeneralIcon v-else icon="ncAutoAwesome" class="h-4 w-4" />
<span class="text-small leading-[18px] truncate font-medium">Generate</span>
</div>
</button>
</NcTooltip>
@ -147,10 +157,19 @@ const debouncedSave = useDebounceFn(handleSave, 1000)
<style scoped lang="scss">
.nc-cell-button {
@apply rounded-lg px-2 flex items-center gap-2 transition-all justify-center border-1 border-nc-border-gray-medium;
@apply rounded-md px-2 flex items-center gap-2 transition-all justify-center bg-purple-100 hover:bg-purple-200 text-gray-700;
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.06), 0px 5px 3px -2px rgba(0, 0, 0, 0.02);
.nc-loader {
@apply !text-purple-600;
}
&:focus-within {
@apply outline-none ring-0;
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
&[disabled] {
@apply !bg-gray-100 opacity-50;
}
@ -162,6 +181,10 @@ const debouncedSave = useDebounceFn(handleSave, 1000)
&:has(.nc-cell-ai-button) {
@apply !border-none;
box-shadow: none !important;
&:focus-within:not(.nc-readonly-div-data-cell):not(.nc-system-field) {
box-shadow: none !important;
}
}
}
</style>

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

@ -11,6 +11,7 @@ import { TaskItem } from '~/helpers/dbTiptapExtensions/task-item'
import { Link } from '~/helpers/dbTiptapExtensions/links'
import { Mention } from '~/helpers/tiptapExtensions/mention'
import suggestion from '~/helpers/tiptapExtensions/mention/suggestion'
import UserMentionList from '~/helpers/tiptapExtensions/mention/UserMentionList.vue'
const props = withDefaults(
defineProps<{
@ -145,7 +146,7 @@ if (appInfo.value.ee) {
isSameUser: bUser?.id === user.value?.id,
}),
)
span.setAttribute('class', `${colorStyles} mention font-semibold m-0.5 rounded-md px-1`)
span.setAttribute('class', `${colorStyles} mention font-semibold m-0.5 rounded-md px-1 inline-block`)
span.textContent = `@${processedContent}`
return span.outerHTML
}
@ -224,7 +225,7 @@ const tiptapExtensions = [
? [
Mention.configure({
suggestion: {
...suggestion,
...suggestion(UserMentionList),
items: ({ query }) =>
baseUsers.value
.filter((user) => user.deleted !== true)
@ -424,7 +425,7 @@ onClickOutside(editorDom, (e) => {
'justify-end xs:hidden': !isForm,
}"
>
<div class="scrollbar-thin scrollbar-thumb-gray-200 hover:scrollbar-thumb-gray-300 scrollbar-track-transparent">
<div class="scrollbar-thin scrollbar-thumb-gray-200 hover:scrollbar-thumb-gray-300 scrollbar-track-transparent relative">
<CellRichTextSelectedBubbleMenu
v-if="editor"
:editor="editor"
@ -706,8 +707,20 @@ onClickOutside(editorDom, (e) => {
height: fit-content;
}
.mention span {
display: none;
.mention {
@apply inline-block my-2px;
span {
display: none;
}
}
em {
font-synthesis: initial !important;
& * {
font-synthesis: initial !important;
}
}
}
.nc-form-field-bubble-menu-wrapper {

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

@ -427,9 +427,11 @@ const closeTextArea = () => {
</NcButton>
</NcTooltip>
<NcButton v-if="enableCloseButton" class="mr-2" type="text" size="small" @click="closeTextArea">
<GeneralIcon icon="close" />
</NcButton>
<div class="!sticky right-0 pr-0.5 bg-white">
<NcButton v-if="enableCloseButton" type="text" size="small" @click="closeTextArea">
<GeneralIcon icon="close" />
</NcButton>
</div>
</div>
</template>

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

@ -482,8 +482,12 @@ const onFocus = () => {
text-overflow: clip;
}
:deep(.ant-select-selection-search-input) {
@apply !text-xs;
:deep(.ant-select-selection-search) {
@apply flex items-center;
.ant-select-selection-search-input {
@apply !text-xs;
}
}
:deep(.ant-select-clear > span) {

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

@ -14,6 +14,8 @@ const props = defineProps<{
const emits = defineEmits(['update:modelValue', 'update:isAiEdited', 'generate', 'close'])
const meta = inject(MetaInj, ref())
const column = inject(ColumnInj)
const editEnabled = inject(EditModeInj, ref(false))
@ -34,7 +36,9 @@ const readOnly = inject(ReadonlyInj, ref(false))
const { showNull, user } = useGlobal()
const { aiLoading, aiIntegrations } = useNocoAi()
const { currentRow } = useSmartsheetRowStoreOrThrow()
const { aiLoading, aiIntegrations, generatingRows, generatingColumnRows } = useNocoAi()
const baseStore = useBase()
@ -93,6 +97,19 @@ const aiWarningRef = ref<HTMLDivElement>()
const { height: aiWarningRefHeight } = useElementSize(aiWarningRef)
const rowId = computed(() => {
return extractPkFromRow(currentRow.value?.row, meta.value!.columns!)
})
const isAiGenerating = computed(() => {
return !!(
rowId.value &&
column?.value.id &&
generatingRows.value.includes(rowId.value) &&
generatingColumnRows.value.includes(column.value.id)
)
})
watch(isVisible, () => {
if (isVisible.value) {
setTimeout(() => {
@ -272,33 +289,43 @@ const updateSize = () => {
}
}
watch([isVisible, inputRef], (value) => {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect
watch(
[isVisible, inputRef],
(value) => {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect
if (!isVisible.value) {
return
}
if (!isVisible.value) {
return
localStorage.setItem(STORAGE_KEY, JSON.stringify({ width, height }))
}
})
localStorage.setItem(STORAGE_KEY, JSON.stringify({ width, height }))
}
})
if (value) {
if (isRichMode.value && isVisible.value) {
setTimeout(() => {
const el = document.querySelector('.nc-long-text-expanded-modal .nc-textarea-rich-editor .tiptap') as HTMLElement
if (value) {
if (isRichMode.value) {
setTimeout(() => {
observer.observe(document.querySelector('.nc-long-text-expanded-modal .nc-textarea-rich-editor .tiptap') as HTMLElement)
if (!el) return
observer.observe(el)
updateSize()
}, 50)
} else {
updateSize()
}, 50)
}
} else {
updateSize()
observer.disconnect()
}
} else {
observer.disconnect()
}
})
},
{
immediate: true,
},
)
</script>
<template>
@ -342,8 +369,16 @@ watch([isVisible, inputRef], (value) => {
'nc-readonly-rich-text-sort-height': localRowHeight === 1 && !isExpandedFormOpen && !isForm,
}"
:style="{
maxHeight: isForm ? undefined : isExpandedFormOpen ? `${height}px` : `${21 * rowHeightTruncateLines(localRowHeight)}px`,
minHeight: isForm ? undefined : isExpandedFormOpen ? `${height}px` : `${21 * rowHeightTruncateLines(localRowHeight)}px`,
maxHeight: isForm
? undefined
: isExpandedFormOpen
? `${height}px`
: `${16.6 * rowHeightTruncateLines(localRowHeight)}px`,
minHeight: isForm
? undefined
: isExpandedFormOpen
? `${height}px`
: `${16.5 * rowHeightTruncateLines(localRowHeight)}px`,
}"
@dblclick="onExpand"
@keydown.enter="onExpand"
@ -388,8 +423,8 @@ watch([isVisible, inputRef], (value) => {
@selectstart.capture.stop
@mousedown.stop
/>
<div v-if="!readOnly" class="-mt-1">
<div v-if="props.isAi && props.aiMeta?.isStale" ref="aiWarningRef">
<div v-if="!readOnly && props.isAi && isExpandedFormOpen" class="-mt-1">
<div v-if="props.aiMeta?.isStale" ref="aiWarningRef">
<div class="flex items-start p-3 bg-nc-bg-purple-light gap-4">
<GeneralIcon icon="alertTriangleSolid" class="text-nc-content-purple-medium h-4 w-4 flex-none" />
<div class="flex flex-col">
@ -401,8 +436,8 @@ watch([isVisible, inputRef], (value) => {
</div>
</div>
<div v-if="props.isAi && !isEditColumn" class="flex items-center gap-2 px-3 py-0.5 !text-small leading-[18px]">
<span class="text-nc-content-purple-dark truncate">Generated by AI</span>
<div v-if="!isEditColumn" class="flex items-center gap-2 px-3 py-0.5 !text-small leading-[18px]">
<span class="text-nc-content-purple-light truncate">Generated by AI</span>
<NcTooltip v-if="isAiEdited" class="text-nc-content-green-dark flex-1 truncate" show-on-truncate-only>
<template #title> Edited by you </template>
Edited by you
@ -440,12 +475,13 @@ watch([isVisible, inputRef], (value) => {
theme="ai"
size="xs"
:disabled="!isFieldAiIntegrationAvailable"
:loading="aiLoading"
:loading="isAiGenerating"
@click.stop="generate"
>
<template #icon>
<GeneralIcon icon="ncAutoAwesome" />
<GeneralIcon icon="ncAutoAwesome" class="h-4 w-4" />
</template>
<template #loading> Re-generating... </template>
Re-generate
</NcButton>
</NcTooltip>
@ -470,10 +506,8 @@ watch([isVisible, inputRef], (value) => {
<span v-else>{{ vModel }}</span>
<NcTooltip
v-if="!isVisible && !isForm"
placement="bottom"
class="nc-action-icon !absolute !hidden nc-text-area-expand-btn group-hover:block z-3"
<div
class="!absolute !hidden nc-text-area-expand-btn group-hover:block z-3 flex items-center gap-1"
:class="{
'right-1': isForm,
'right-0': !isForm,
@ -486,17 +520,45 @@ watch([isVisible, inputRef], (value) => {
: undefined
"
>
<template #title>{{ $t('title.expand') }}</template>
<NcButton
type="secondary"
size="xsmall"
data-testid="attachment-cell-file-picker-button"
class="!p-0 !w-5 !h-5 !min-w-[fit-content]"
@click.stop="onExpand"
<NcTooltip
v-if="!isVisible && !isForm && !readOnly && props.isAi && !isExpandedFormOpen && !isEditColumn"
placement="bottom"
class="nc-action-icon"
>
<component :is="iconMap.expand" class="transform group-hover:(!text-grey-800) text-gray-700 text-xs" />
</NcButton>
</NcTooltip>
<template #title>
{{ isAiGenerating ? 'Re-generating...' : 'Re-generate' }}
</template>
<NcButton
type="secondary"
size="xsmall"
class="!p-0 !w-5 !h-5 !min-w-[fit-content]"
:disabled="isAiGenerating"
loader-size="small"
icon-only
@click.stop="generate"
>
<template #icon>
<GeneralIcon
icon="refresh"
class="transform group-hover:(!text-grey-800) text-gray-700 w-3 h-3"
:class="{ 'animate-infinite animate-spin': isAiGenerating }"
/>
</template>
</NcButton>
</NcTooltip>
<NcTooltip v-if="!isVisible && !isForm" placement="bottom" class="nc-action-icon">
<template #title>{{ $t('title.expand') }}</template>
<NcButton
type="secondary"
size="xsmall"
data-testid="attachment-cell-file-picker-button"
class="!p-0 !w-5 !h-5 !min-w-[fit-content]"
@click.stop="onExpand"
>
<component :is="iconMap.maximize" class="transform group-hover:(!text-grey-800) text-gray-700 w-3 h-3" />
</NcButton>
</NcTooltip>
</div>
</div>
<a-modal
v-if="isVisible"
@ -543,7 +605,7 @@ watch([isVisible, inputRef], (value) => {
{{ column.title }}
</span>
</div>
<template v-if="!props.isAi">
<template v-if="!props.isAi && !isRichMode">
<div class="flex-1" />
<NcButton class="mr-2" type="text" size="small" @click="isVisible = false">
@ -582,15 +644,15 @@ watch([isVisible, inputRef], (value) => {
@click.stop="generate"
>
<div class="flex items-center gap-2">
<GeneralIcon icon="refresh" :class="{ 'animate-infinite animate-spin': aiLoading }" />
<span class="text-sm font-bold">Re-generate</span>
<GeneralIcon icon="refresh" :class="{ 'animate-infinite animate-spin': isAiGenerating }" />
<span class="text-sm font-bold"> {{ isAiGenerating ? 'Re-generating...' : 'Re-generate' }} </span>
</div>
</NcButton>
</NcTooltip>
</div>
</template>
</div>
<div v-if="props.isAi && props.aiMeta?.isStale && !readOnly" ref="aiWarningRef">
<div v-if="props.isAi && props.aiMeta?.isStale && !readOnly" ref="aiWarningRef" class="border-b-1 border-gray-100">
<div class="flex items-center p-4 bg-nc-bg-purple-light gap-4">
<GeneralIcon icon="alertTriangleSolid" class="text-nc-content-purple-medium h-6 w-6 flex-none" />
<div class="flex flex-col">
@ -661,11 +723,23 @@ textarea:focus {
<style lang="scss">
.cell:hover .nc-text-area-expand-btn,
.long-text-wrapper:hover .nc-text-area-expand-btn {
@apply !block cursor-pointer;
@apply !flex cursor-pointer;
}
.nc-grid-cell {
&.align-top {
.long-text-wrapper {
@apply items-start;
}
}
&:not(.align-top) {
@apply items-center;
}
}
.nc-data-cell {
&:has(.nc-cell-ai .nc-expanded-form-open) {
&:has(.nc-cell-longtext-ai .nc-expanded-form-open) {
@apply !border-none -mx-1 -my-1;
box-shadow: none !important;

2
packages/nc-gui/components/cell/attachment/index.vue

@ -424,7 +424,7 @@ defineExpose({
>
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<component :is="iconMap.expand" v-else class="transform group-hover:(!text-grey-800) text-gray-700 text-xs" />
<component :is="iconMap.maximize" v-else class="transform group-hover:(!text-grey-800) text-gray-700 w-3 h-3" />
</NcButton>
</NcTooltip>

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

@ -16,7 +16,6 @@ const { refreshCommandPalette } = useCommandPalette()
const viewsStore = useViewsStore()
const { loadViews, navigateToView } = viewsStore
const { aiIntegrationAvailable } = useNocoAi()
const { isFeatureEnabled } = useBetaFeatureToggle()
const table = inject(SidebarTableInj)!
@ -197,7 +196,7 @@ async function onOpenModal({
<GeneralIcon v-else class="plus" icon="plus" />
</div>
</NcMenuItem>
<template v-if="aiIntegrationAvailable && isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)">
<template v-if="isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)">
<NcDivider />
<NcMenuItem data-testid="sidebar-view-create-ai" @click="onOpenModal({ type: 'AI' })">
<div class="item">

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

@ -18,22 +18,12 @@ const sharedBase = ref<null | ShareBase>(null)
const { base } = storeToRefs(useBase())
const { getBaseUrl, appInfo } = useGlobal()
const workspaceStore = useWorkspace()
const { appInfo } = useGlobal()
const url = computed(() => {
if (!sharedBase.value || !sharedBase.value.uuid) return ''
// get base url for workspace
const baseUrl = getBaseUrl(workspaceStore.activeWorkspaceId)
let dashboardUrl1 = dashboardUrl.value
if (baseUrl) {
dashboardUrl1 = `${baseUrl}${appInfo.value?.dashboardPath}`
}
return encodeURI(`${dashboardUrl1}#/base/${sharedBase.value.uuid}`)
return encodeURI(`${dashboardUrl.value}#/base/${sharedBase.value.uuid}`)
})
const loadBase = async () => {

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

@ -4,7 +4,7 @@ import { ViewTypes } from 'nocodb-sdk'
const { view: _view, $api } = useSmartsheetStoreOrThrow()
const { $e } = useNuxtApp()
const { getBaseUrl, appInfo } = useGlobal()
const { appInfo } = useGlobal()
const { dashboardUrl } = useDashboard()
@ -203,15 +203,7 @@ function sharedViewUrl() {
viewType = 'view'
}
// get base url for workspace
const baseUrl = getBaseUrl(workspaceStore.activeWorkspaceId)
let dashboardUrl1 = dashboardUrl.value
if (baseUrl) {
dashboardUrl1 = `${baseUrl}${appInfo.value?.dashboardPath}`
}
return `${encodeURI(`${dashboardUrl1}#/nc/${viewType}/${activeView.value.uuid}${surveyMode.value ? '/survey' : ''}`)}${
return `${encodeURI(`${dashboardUrl.value}#/nc/${viewType}/${activeView.value.uuid}${surveyMode.value ? '/survey' : ''}`)}${
viewStore.preFillFormSearchParams && formPreFill.value.preFillEnabled ? `?${viewStore.preFillFormSearchParams}` : ''
}`
}

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

@ -2,7 +2,7 @@
const props = withDefaults(
defineProps<{
type: string
size: 'sx' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
size?: 'sx' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
}>(),
{
size: 'sm',

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

@ -1,10 +1,12 @@
<script lang="ts" setup>
import { LoadingOutlined } from '@ant-design/icons-vue'
const props = defineProps<{
export interface GeneralLoaderProps {
size?: 'small' | 'medium' | 'large' | 'xlarge' | 'regular' | number
loaderClass?: string
}>()
}
const props = defineProps<GeneralLoaderProps>()
function getFontSize() {
const { size = 'medium' } = props

30
packages/nc-gui/components/general/WorkspaceIcon.vue

@ -4,16 +4,22 @@ import 'emoji-mart-vue-fast/css/emoji-mart.css'
import { Icon } from '@iconify/vue'
import { WorkspaceIconType, isColorDark, stringToColor } from '#imports'
const props = defineProps<{
workspace: WorkspaceType | undefined
workspaceIcon?: {
icon: string | Record<string, any>
iconType: WorkspaceIconType | string
}
hideLabel?: boolean
size?: 'small' | 'medium' | 'large' | 'xlarge'
isRounded?: boolean
}>()
const props = withDefaults(
defineProps<{
workspace: WorkspaceType | undefined
workspaceIcon?: {
icon: string | Record<string, any>
iconType: WorkspaceIconType | string
}
hideLabel?: boolean
size?: 'small' | 'medium' | 'large' | 'xlarge'
isRounded?: boolean
iconBgColor?: string
}>(),
{
iconBgColor: '#F4F4F5',
},
)
const { workspace } = toRefs(props)
@ -50,10 +56,10 @@ const workspaceColor = computed(() => {
return ''
}
case WorkspaceIconType.EMOJI: {
return '#F4F4F5'
return props.iconBgColor
}
case WorkspaceIconType.ICON: {
return '#F4F4F5'
return props.iconBgColor
}
default: {

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

@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { ButtonType } from 'ant-design-vue/lib/button'
import { useSlots } from 'vue'
import type { GeneralLoaderProps } from '../general/Loader.vue'
/**
* @description
@ -19,6 +20,7 @@ interface Props {
showAsDisabled?: boolean
type?: ButtonType | 'danger' | 'secondary' | undefined
size?: NcButtonSize
loaderSize?: GeneralLoaderProps['size']
centered?: boolean
fullWidth?: boolean
iconOnly?: boolean
@ -32,6 +34,7 @@ const props = withDefaults(defineProps<Props>(), {
disabled: false,
showAsDisabled: false,
size: 'medium',
loaderSize: 'medium',
type: 'primary',
fullWidth: false,
centered: true,
@ -47,7 +50,7 @@ const slots = useSlots()
const NcButton = ref<HTMLElement | null>(null)
const { size, type, theme, bordered } = toRefs(props)
const { size, loaderSize, type, theme, bordered } = toRefs(props)
const loading = useVModel(props, 'loading', emits)
@ -112,7 +115,7 @@ useEventListener(NcButton, 'mousedown', () => {
>
<template v-if="iconPosition === 'left'">
<slot v-if="loading" name="loadingIcon">
<GeneralLoader class="flex !bg-inherit !text-inherit" size="medium" />
<GeneralLoader class="flex !bg-inherit !text-inherit" :size="loaderSize" />
</slot>
<slot v-else name="icon" />
@ -131,7 +134,7 @@ useEventListener(NcButton, 'mousedown', () => {
</div>
<template v-if="iconPosition === 'right'">
<slot v-if="loading" name="loadingIcon">
<GeneralLoader v-if="loading" class="flex !bg-inherit !text-inherit" size="medium" />
<GeneralLoader class="flex !bg-inherit !text-inherit" :size="loaderSize" />
</slot>
<slot v-else name="icon" />

2
packages/nc-gui/components/nc/List/RecordItem.vue

@ -142,7 +142,7 @@ const columnsToRender = computed(() => {
<div v-if="columnsToRender.length > 0" class="flex ml-[-0.25rem] sm:flex-row xs:(flex-col mt-2) gap-4 min-h-5">
<div v-for="column in columnsToRender" :key="column.id" class="sm:(w-1/3 max-w-1/3 overflow-hidden)">
<div v-if="!isRowEmpty({ row }, column)" class="flex flex-col gap-[-1]">
<div v-if="!isRowEmpty(currentRow, column)" class="flex flex-col gap-[-1]">
<NcTooltip class="z-10 flex" placement="bottomLeft" :arrow-point-at-center="false">
<template #title>
<LazySmartsheetHeaderVirtualCell

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

@ -42,7 +42,7 @@ const onChange = (e: boolean, updateValue = false) => {
:class="[
contentWrapperClass,
{
'cursor-not-allowed': disabled,
'cursor-not-allowed opacity-60': disabled,
'cursor-pointer': !disabled,
},
]"
@ -69,7 +69,7 @@ const onChange = (e: boolean, updateValue = false) => {
:class="[
contentWrapperClass,
{
'cursor-not-allowed': disabled,
'cursor-not-allowed opacity-60': disabled,
'cursor-pointer': !disabled,
},
]"

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

@ -146,6 +146,34 @@ const showCurrentDateOption = computed(() => {
const currentDate = () => {
vModel.value = sqlUi.value?.getCurrentDateDefault?.(column.value)
}
const cellType = computed(() => {
if (isAI(column.value)) return 'ai'
if (isTextArea(column.value)) return 'textarea'
if (isGeoData(column.value)) return 'geoData'
if (isBoolean(column.value, abstractType.value)) return 'checkbox'
if (isAttachment(column.value)) return 'attachment'
if (isSingleSelect(column.value)) return 'singleSelect'
if (isMultiSelect(column.value)) return 'multiSelect'
if (isDate(column.value, abstractType.value)) return 'datePicker'
if (isYear(column.value, abstractType.value)) return 'yearPicker'
if (isDateTime(column.value, abstractType.value)) return 'dateTimePicker'
if (isTime(column.value, abstractType.value)) return 'timePicker'
if (isRating(column.value)) return 'rating'
if (isDuration(column.value)) return 'duration'
if (isEmail(column.value)) return 'email'
if (isURL(column.value)) return 'url'
if (isPhoneNumber(column.value)) return 'phoneNumber'
if (isPercent(column.value)) return 'percent'
if (isCurrency(column.value)) return 'currency'
if (isUser(column.value)) return 'user'
if (isDecimal(column.value)) return 'decimal'
if (isFloat(column.value, abstractType.value)) return 'float'
if (isString(column.value, abstractType.value)) return 'text'
if (isInt(column.value, abstractType.value)) return 'integer'
if (isJSON(column.value)) return 'json'
return 'text'
})
</script>
<template>
@ -165,6 +193,7 @@ const currentDate = () => {
'h-10': !isEditColumnMenu && isForm && !isAttachment(column) && !isTextArea(column) && !isJSON(column) && !props.virtual,
'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu,
'!min-h-30': isTextArea(column) && (isForm || isSurveyForm),
'nc-cell-longtext-ai': cellType === 'ai',
},
]"
class="nc-cell w-full h-full relative"
@ -180,53 +209,58 @@ const currentDate = () => {
{{ $t('general.generating') }}
</NcTooltip>
</div>
<LazyCellTextArea v-else-if="isTextArea(column)" v-model="vModel" :virtual="props.virtual" />
<LazyCellGeoData v-else-if="isGeoData(column)" v-model="vModel" />
<LazyCellCheckbox v-else-if="isBoolean(column, abstractType)" v-model="vModel" />
<LazyCellAttachment v-else-if="isAttachment(column)" ref="attachmentCell" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellAI v-else-if="cellType === 'ai'" v-model="vModel" @save="emit('save')" />
<LazyCellTextArea v-else-if="cellType === 'textarea'" v-model="vModel" :virtual="props.virtual" />
<LazyCellGeoData v-else-if="cellType === 'geoData'" v-model="vModel" />
<LazyCellCheckbox v-else-if="cellType === 'checkbox'" v-model="vModel" />
<LazyCellAttachment
v-else-if="cellType === 'attachment'"
ref="attachmentCell"
v-model="vModel"
:row-index="props.rowIndex"
/>
<LazyCellSingleSelect
v-else-if="isSingleSelect(column)"
v-else-if="cellType === 'singleSelect'"
v-model="vModel"
:disable-option-creation="!!isEditColumnMenu"
:row-index="props.rowIndex"
/>
<LazyCellMultiSelect
v-else-if="isMultiSelect(column)"
v-else-if="cellType === 'multiSelect'"
v-model="vModel"
:disable-option-creation="!!isEditColumnMenu"
:row-index="props.rowIndex"
/>
<LazyCellDatePicker
v-else-if="isDate(column, abstractType)"
v-else-if="cellType === 'datePicker'"
v-model="vModel"
:is-pk="isPrimaryKey(column)"
:show-current-date-option="showCurrentDateOption"
@current-date="currentDate"
/>
<LazyCellYearPicker v-else-if="isYear(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellYearPicker v-else-if="cellType === 'yearPicker'" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellDateTimePicker
v-else-if="isDateTime(column, abstractType)"
v-else-if="cellType === 'dateTimePicker'"
v-model="vModel"
:is-pk="isPrimaryKey(column)"
:is-updated-from-copy-n-paste="currentRow.rowMeta.isUpdatedFromCopyNPaste"
:show-current-date-option="showCurrentDateOption"
@current-date="currentDate"
/>
<LazyCellTimePicker v-else-if="isTime(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellRating v-else-if="isRating(column)" v-model="vModel" />
<LazyCellDuration v-else-if="isDuration(column)" v-model="vModel" />
<LazyCellEmail v-else-if="isEmail(column)" v-model="vModel" />
<LazyCellUrl v-else-if="isURL(column)" v-model="vModel" />
<LazyCellPhoneNumber v-else-if="isPhoneNumber(column)" v-model="vModel" />
<LazyCellPercent v-else-if="isPercent(column)" v-model="vModel" />
<LazyCellCurrency v-else-if="isCurrency(column)" v-model="vModel" @save="emit('save')" />
<LazyCellUser v-else-if="isUser(column)" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellAI v-else-if="isAI(column)" v-model="vModel" @save="emit('save')" />
<LazyCellDecimal v-else-if="isDecimal(column)" v-model="vModel" />
<LazyCellFloat v-else-if="isFloat(column, abstractType)" v-model="vModel" />
<LazyCellText v-else-if="isString(column, abstractType)" v-model="vModel" />
<LazyCellInteger v-else-if="isInt(column, abstractType)" v-model="vModel" />
<LazyCellJson v-else-if="isJSON(column)" v-model="vModel" />
<LazyCellTimePicker v-else-if="cellType === 'timePicker'" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellRating v-else-if="cellType === 'rating'" v-model="vModel" />
<LazyCellDuration v-else-if="cellType === 'duration'" v-model="vModel" />
<LazyCellEmail v-else-if="cellType === 'email'" v-model="vModel" />
<LazyCellUrl v-else-if="cellType === 'url'" v-model="vModel" />
<LazyCellPhoneNumber v-else-if="cellType === 'phoneNumber'" v-model="vModel" />
<LazyCellPercent v-else-if="cellType === 'percent'" v-model="vModel" />
<LazyCellCurrency v-else-if="cellType === 'currency'" v-model="vModel" @save="emit('save')" />
<LazyCellUser v-else-if="cellType === 'user'" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellDecimal v-else-if="cellType === 'decimal'" v-model="vModel" />
<LazyCellFloat v-else-if="cellType === 'float'" v-model="vModel" />
<LazyCellText v-else-if="cellType === 'text'" v-model="vModel" />
<LazyCellInteger v-else-if="cellType === 'integer'" v-model="vModel" />
<LazyCellJson v-else-if="cellType === 'json'" v-model="vModel" />
<LazyCellText v-else v-model="vModel" />
<div
v-if="

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

@ -655,18 +655,34 @@ const handleOnUploadImage = (data: AttachmentResType = null) => {
updateView()
}
const isFocusedFieldLabel = ref(false)
const onFocusActiveFieldLabel = (e: FocusEvent) => {
isFocusedFieldLabel.value = true
if (activeField.value && !activeField.value.label) {
activeField.value.label = activeField.value?.title ?? ''
}
;(e.target as HTMLTextAreaElement).select()
}
const activeFieldLabel = computed(() => {
if (!isFocusedFieldLabel.value && !activeField.value?.label) {
return activeField.value?.title
}
return activeField.value?.label ?? ''
})
onClickOutside(focusLabel, () => {
isFocusedFieldLabel.value = false
})
const updateFieldTitle = (value: string) => {
if (!activeField.value) return
if (activeField.value.title === value) {
activeField.value.label = null
} else {
activeField.value.label = value
}
activeField.value.label = value.trimStart()
}
const handleAutoScrollFormField = (title: string, isSidebar: boolean) => {
@ -1459,7 +1475,7 @@ useEventListener(
<a-textarea
ref="focusLabel"
:value="activeField.label || activeField.title"
:value="activeFieldLabel"
:rows="1"
auto-size
hide-details
@ -1467,6 +1483,7 @@ useEventListener(
data-testid="nc-form-input-label"
:placeholder="$t('msg.info.formInput')"
@focus="onFocusActiveFieldLabel"
@blur="isFocusedFieldLabel = false"
@keydown.enter.prevent
@input="updateFieldTitle($event.target.value)"
@change="updateColMeta(activeField)"

2
packages/nc-gui/components/smartsheet/Gallery.vue

@ -373,7 +373,7 @@ reloadViewDataHook?.on(async () => {
<NcMenu @click="contextMenu = false">
<NcMenuItem v-if="contextMenuTarget" @click="expandForm(contextMenuTarget.row)">
<div v-e="['a:row:expand-record']" class="flex items-center gap-2">
<component :is="iconMap.expand" class="flex" />
<component :is="iconMap.maximize" class="flex" />
{{ $t('activity.expandRecord') }}
</div>
</NcMenuItem>

2
packages/nc-gui/components/smartsheet/Kanban.vue

@ -1143,7 +1143,7 @@ const handleSubmitRenameOrNewStack = async (loadMeta: boolean, stack?: any, stac
<NcMenu @click="contextMenu = false">
<NcMenuItem v-if="contextMenuTarget" @click="expandForm(contextMenuTarget)">
<div v-e="['a:kanban:expand-record']" class="flex items-center gap-2 nc-kanban-context-menu-item">
<component :is="iconMap.expand" class="flex" />
<component :is="iconMap.maximize" class="flex" />
<!-- Expand Record -->
{{ $t('activity.expandRecord') }}
</div>

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

@ -1,11 +1,13 @@
<script lang="ts" setup>
import {
type AIRecordType,
type BoolType,
type ButtonType,
type ColumnType,
type LookupType,
type RollupType,
dateFormats,
isAIPromptCol,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
timeFormats,
@ -202,6 +204,11 @@ const getTextAreaValue = (modelValue: string | null, col: ColumnType) => {
if (isRichMode) {
return modelValue?.replace(/[*_~\[\]]|<\/?[^>]+(>|$)/g, '') || ''
}
if (isAIPromptCol(col)) {
return (modelValue as AIRecordType)?.value || ''
}
return modelValue || ''
}

39
packages/nc-gui/components/smartsheet/column/AiButtonOptions.vue

@ -32,10 +32,13 @@ const {
formattedData,
disableSubmitBtn,
tableExplorerColumns,
fromTableExplorer,
} = useColumnCreateStoreOrThrow()
const { aiIntegrationAvailable, aiLoading, aiError, generateRows } = useNocoAi()
const { isFeatureEnabled } = useBetaFeatureToggle()
const isOpenConfigModal = ref<boolean>(false)
const isOpenSelectOutputFieldDropdown = ref<boolean>(false)
@ -243,6 +246,14 @@ watch(isOpenSelectRecordDropdown, (newValue) => {
}
})
const isAiButtonEnabled = computed(() => {
if (isEdit.value) {
return true
}
return isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)
})
const previewPanelDom = ref<HTMLElement>()
const isPreviewPanelOnScrollTop = ref(false)
@ -294,7 +305,7 @@ onBeforeUnmount(() => {
</script>
<template>
<div class="relative flex flex-col gap-4">
<div v-if="isAiButtonEnabled" class="relative flex flex-col gap-4">
<AiIntegrationNotFound v-if="!aiIntegrationAvailable" />
<template v-else-if="!!aiError"> </template>
<template v-else>
@ -332,6 +343,7 @@ onBeforeUnmount(() => {
</NcButton>
<NcButton
v-if="!fromTableExplorer"
size="small"
type="primary"
:disabled="disableSubmitBtn || saving"
@ -366,6 +378,7 @@ onBeforeUnmount(() => {
v-model:randomness="vModel.randomness"
:workspace-id="activeWorkspaceId"
:show-tooltip="false"
:is-edit-column="isEdit"
placement="bottomRight"
>
<NcButton size="xs" theme="ai" class="!px-1" type="text">
@ -467,7 +480,11 @@ onBeforeUnmount(() => {
<a-tag v-if="outputColumnIds.includes(op.id)" :key="op.id" class="nc-ai-button-output-field">
<div class="flex flex-row items-center gap-1 py-[2px] text-sm">
<component :is="cellIcon(op)" class="!mx-0 !mr-1 opacity-80" />
<span>{{ op.title }}</span>
<NcTooltip show-on-truncate-only class="truncate max-w-[150px]">
<template #title>{{ op.title }}</template>
{{ op.title }}
</NcTooltip>
<div class="flex items-center p-0.5 mt-0.5">
<GeneralIcon
icon="close"
@ -599,7 +616,7 @@ onBeforeUnmount(() => {
show-search-always
search-input-placeholder="Search records"
:item-height="60"
class="!w-auto min-w-[500px] max-w-[576px]"
class="!w-auto min-w-[550px] max-w-[550px]"
container-class-name="!px-0 !pb-0"
item-class-name="!rounded-none !p-0 !bg-none !hover:bg-none"
@update:value="handleResetOutput"
@ -679,18 +696,19 @@ onBeforeUnmount(() => {
<div
class="flex justify-center nc-ai-button-test-generate-wrapper"
:class="{
'text-nc-border-gray-dark': !(selectedRecordPk && outputColumnIds.length && vModel.formula_raw),
'text-nc-content-purple-dark': !!(selectedRecordPk && outputColumnIds.length && vModel.formula_raw),
'text-nc-border-gray-dark': !(selectedRecordPk && outputColumnIds.length && inputColumns.length),
'text-nc-content-purple-dark': !!(selectedRecordPk && outputColumnIds.length && inputColumns.length),
}"
>
<div class="h-2.5 w-2.5 flex-none absolute -top-[30px] border-1 border-current rounded-full bg-current"></div>
<NcTooltip :disabled="!!(selectedRecordPk && outputColumnIds.length && inputColumns.length)">
<template #title>
<div class="flex flex-col gap-2">
<div>Preview checklist</div>
<div class="flex flex-col gap-2 py-1 px-0.5">
<div class="text-[10px] leading-[14px] text-gray-300 uppercase mb-1">Preview checklist</div>
<div class="flex gap-2">
<div
class="h-4 w-4 mt-0.5 rounded-full grid place-items-center children:(h-3.5 w-3.5 flex-none)"
class="h-4 w-4 rounded-full grid place-items-center children:(h-3.5 w-3.5 flex-none)"
:class="
inputColumns.length
? 'bg-nc-bg-green-dark text-nc-content-green-dark'
@ -703,7 +721,7 @@ onBeforeUnmount(() => {
</div>
<div class="flex gap-2">
<div
class="h-4 w-4 mt-0.5 rounded-full grid place-items-center children:(h-3.5 w-3.5 flex-none)"
class="h-4 w-4 rounded-full grid place-items-center children:(h-3.5 w-3.5 flex-none)"
:class="
outputColumnIds.length
? 'bg-nc-bg-green-dark text-nc-content-green-dark'
@ -716,7 +734,7 @@ onBeforeUnmount(() => {
</div>
<div class="flex gap-2">
<div
class="h-4 w-4 mt-0.5 rounded-full grid place-items-center children:(h-3.5 w-3.5 flex-none)"
class="h-4 w-4 rounded-full grid place-items-center children:(h-3.5 w-3.5 flex-none)"
:class="
selectedRecordPk
? 'bg-nc-bg-green-dark text-nc-content-green-dark'
@ -854,6 +872,7 @@ onBeforeUnmount(() => {
</div>
</NcModal>
</div>
<div v-else></div>
</template>
<style lang="scss">

85
packages/nc-gui/components/smartsheet/column/ButtonOptions.vue

@ -26,6 +26,8 @@ const { isUIAllowed } = useRoles()
const { getMeta } = useMetas()
const { isFeatureEnabled } = useBetaFeatureToggle()
const vModel = useVModel(props, 'value', emit)
const meta = inject(MetaInj, ref())
@ -49,7 +51,15 @@ const manualHooks = computed(() => {
return hooks.value.filter((hook) => hook.event === 'manual' && hook.active)
})
const buttonTypes = [
const isAiButtonEnabled = computed(() => {
if (isEdit.value) {
return true
}
return isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)
})
const buttonTypes = computed(() => [
{
label: t('labels.openUrl'),
value: ButtonActionsType.Url,
@ -58,12 +68,16 @@ const buttonTypes = [
label: t('labels.runWebHook'),
value: ButtonActionsType.Webhook,
},
{
label: t('labels.generateFieldDataUsingAi'),
value: ButtonActionsType.Ai,
tooltip: t('tooltip.generateFieldDataUsingAiButtonOption'),
},
]
...(isAiButtonEnabled.value
? [
{
label: t('labels.generateFieldDataUsingAi'),
value: ButtonActionsType.Ai,
tooltip: t('tooltip.generateFieldDataUsingAiButtonOption'),
},
]
: []),
])
const supportedColumns = computed(
() =>
@ -83,7 +97,7 @@ const supportedColumns = computed(
const validators = {
formula_raw: [
{
required: vModel.value.type === ButtonActionsType.Url,
required: [ButtonActionsType.Url, ButtonActionsType.Ai].includes(vModel.value.type),
validator: (_: any, formula: any) => {
return (async () => {
if (vModel.value.type === ButtonActionsType.Url) {
@ -104,6 +118,8 @@ const validators = {
throw new Error(e.message)
}
} else if (vModel.value.type === ButtonActionsType.Ai) {
if (!formula?.trim()) throw new Error('Prompt required for AI Button')
}
})()
},
@ -170,24 +186,30 @@ const validators = {
},
},
],
...((isEdit.value ? vModel.value.colOptions : vModel.value.type) === ButtonActionsType.Ai
? {
output_column_ids: [
{
required: true,
message: 'At least one output field is required for AI Button',
},
],
formula_raw: [
{
required: true,
message: 'Prompt required for AI Button',
},
],
fk_integration_id: [{ required: true, message: t('general.required') }],
}
: {}),
output_column_ids: [
{
validator: (_: any, value: any) => {
return new Promise<void>((resolve, reject) => {
if (vModel.value.type === ButtonActionsType.Ai && !value) {
reject(new Error('At least one output field is required for AI Button'))
}
resolve()
})
},
},
],
fk_integration_id: [
{
validator: (_: any, value: any) => {
return new Promise<void>((resolve, reject) => {
if (vModel.value.type === ButtonActionsType.Ai && !value) {
reject(new Error(t('title.aiIntegrationMissing')))
}
resolve()
})
},
},
],
}
if (isEdit.value) {
@ -206,10 +228,10 @@ if (isEdit.value) {
vModel.value.fk_integration_id = colOptions?.fk_integration_id
}
} else {
vModel.value.type = vModel.value?.type || buttonTypes[0].value
vModel.value.type = vModel.value?.type || buttonTypes.value[0]?.value
if (vModel.value.type === ButtonActionsType.Ai) {
vModel.value.theme = 'text'
vModel.value.theme = 'light'
vModel.value.label = 'Generate data'
vModel.value.color = 'purple'
vModel.value.icon = 'ncAutoAwesome'
@ -344,13 +366,6 @@ const selectIcon = (icon: string) => {
}
const handleUpdateActionType = (type: ButtonActionsType) => {
// We are using `formula_raw` in both type url & ai, so it's imp to reset it
if (type !== ButtonActionsType.Ai) {
setAdditionalValidations({
formula_raw: validators.formula_raw,
})
}
vModel.value.formula_raw = ''
}

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

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { type ColumnReqType, type ColumnType } from 'nocodb-sdk'
import { type ColumnReqType, type ColumnType, isAIPromptCol } from 'nocodb-sdk'
import {
ButtonActionsType,
UITypes,
@ -183,9 +183,10 @@ const uiFilters = (t: UiTypesType) => {
const specificDBType = t.name === UITypes.SpecificDBType && isXcdbBase(meta.value?.source_id)
const showDeprecatedField = !t.deprecated || showDeprecated.value
const showAiFields = [AIPrompt, AIButton].includes(t.name) ? isFeatureEnabled(FEATURE_FLAG.AI_FEATURES) && !isEdit.value : true
const isAllowToAddInFormView = isForm.value ? !formViewHiddenColTypes.includes(t.name) : true
return systemFiledNotEdited && geoDataToggle && !specificDBType && showDeprecatedField && isAllowToAddInFormView
return systemFiledNotEdited && geoDataToggle && !specificDBType && showDeprecatedField && isAllowToAddInFormView && showAiFields
}
const extraIcons = ref<Record<string, string>>({})
@ -238,7 +239,21 @@ const uiTypesOptions = computed<typeof uiTypes>(() => {
return types
})
const onSelectType = (uidt: UITypes | typeof AIButton, fromSearchList = false) => {
const editOrAddRef = ref<HTMLDivElement>()
const isScrollEnabled = ref(false)
const handleScrollDebounce = useDebounceFn(() => {
if (props.fromTableExplorer || !editOrAddRef.value || aiAutoSuggestMode.value) return
if (editOrAddRef.value.clientHeight < editOrAddRef.value.scrollHeight) {
isScrollEnabled.value = true
} else {
isScrollEnabled.value = false
}
}, 500)
const onSelectType = (uidt: UITypes | typeof AIButton | typeof AIPrompt, fromSearchList = false) => {
let preload
if (fromSearchList && !isEdit.value && aiAutoSuggestMode.value) {
@ -250,10 +265,22 @@ const onSelectType = (uidt: UITypes | typeof AIButton, fromSearchList = false) =
preload = {
type: ButtonActionsType.Ai,
}
} else if (uidt === AIPrompt) {
formState.value.uidt = UITypes.LongText
preload = {
meta: {
[LongTextAiMetaProp]: true,
},
}
} else {
formState.value.uidt = uidt
}
onUidtOrIdTypeChange(preload)
nextTick(() => {
handleScrollDebounce()
})
}
const reloadMetaAndData = async () => {
@ -393,6 +420,9 @@ onMounted(() => {
nextTick(() => {
mounted.value = true
emit('mounted')
handleScrollDebounce()
if (!isEdit.value) {
if (!formState.value?.temp_id) {
emit('add', formState.value)
@ -489,6 +519,9 @@ const triggerDescriptionEnable = () => {
descInputEl.value?.focus()
}, 100)
}
nextTick(() => {
handleScrollDebounce()
})
}
const isFullUpdateAllowed = computed(() => {
@ -582,6 +615,10 @@ const isAiButtonSelectOption = (uidt: string) => {
return uidt === UITypes.Button && formState.value.uidt === UITypes.Button && formState.value.type === ButtonActionsType.Ai
}
const isAiPromptSelectOption = (uidt: string) => {
return uidt === UITypes.LongText && isAIPromptCol(formState.value)
}
const aiPromptInputRef = ref<HTMLElement>()
watch(activeAiTab, (newValue) => {
@ -597,6 +634,7 @@ watch(activeAiTab, (newValue) => {
<template>
<div
v-if="!warningVisible"
ref="editOrAddRef"
class="overflow-auto nc-scrollbar-md"
:class="{
'bg-white max-h-[max(80vh,500px)]': !props.fromTableExplorer,
@ -604,13 +642,15 @@ watch(activeAiTab, (newValue) => {
'min-w-[500px]': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links,
'!w-[600px]': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links,
'min-w-[422px] !w-full': isLinksOrLTAR(formState.uidt),
'shadow-lg shadow-gray-300 border-1 border-gray-200 rounded-xl p-5': !embedMode,
'shadow-lg shadow-gray-300 border-1 border-gray-200 rounded-2xl p-5': !embedMode,
'nc-ai-mode': isAiMode,
'h-full': props.fromTableExplorer,
'!bg-nc-bg-gray-extralight': aiAutoSuggestMode && formState.uidt && !props.fromTableExplorer,
'!pb-0': !embedMode && !aiAutoSuggestMode && formState.uidt,
}"
@keydown="handleEscape"
@click.stop
@scroll="handleScrollDebounce"
>
<a-form
v-model="formState"
@ -927,7 +967,7 @@ watch(activeAiTab, (newValue) => {
type="primary"
theme="ai"
:loading="saving"
:disabled="disableSubmitBtn || !activeTabSelectedFields.length || saving"
:disabled="disableSubmitBtn || saving"
size="small"
:label="submitBtnLabel.label"
:loading-label="submitBtnLabel.loadingLabel"
@ -1041,14 +1081,20 @@ watch(activeAiTab, (newValue) => {
v-bind="validateInfos.uidt"
:class="{
'ant-select-item-option-active-selected': showHoverEffectOnSelectedType && formState.uidt === opt.name,
'!text-nc-content-purple-dark': [AIButton].includes(opt.name),
'!text-nc-content-purple-dark': [AIPrompt, AIButton].includes(opt.name),
}"
@mouseover="handleResetHoverEffect"
>
<div class="w-full flex gap-2 items-center justify-between" :data-testid="opt.name" :data-title="formState?.type">
<div class="flex-1 flex gap-2 items-center max-w-[calc(100%_-_24px)]">
<component
:is="isAiButtonSelectOption(opt.name) && !isColumnTypeOpen ? iconMap.cellAiButton : opt.icon"
:is="
isAiButtonSelectOption(opt.name) && !isColumnTypeOpen
? iconMap.cellAiButton
: isAiPromptSelectOption(opt.name) && !isColumnTypeOpen
? iconMap.cellAi
: opt.icon
"
class="nc-field-type-icon w-4 h-4"
:class="isMetaReadOnly && !readonlyMetaAllowedTypes.includes(opt.name) ? 'text-gray-300' : 'text-gray-700'"
/>
@ -1109,7 +1155,11 @@ watch(activeAiTab, (newValue) => {
<SmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<SmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" />
<SmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<SmartsheetColumnLongTextOptions v-if="formState.uidt === UITypes.LongText" v-model:value="formState" />
<SmartsheetColumnLongTextOptions
v-if="formState.uidt === UITypes.LongText"
v-model="formState"
@navigate-to-integrations="handleNavigateToIntegrations"
/>
<SmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<SmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<SmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
@ -1227,7 +1277,12 @@ watch(activeAiTab, (newValue) => {
</Transition>
</template>
<a-form-item v-if="enableDescription && !aiAutoSuggestMode">
<a-form-item
v-if="enableDescription && !aiAutoSuggestMode"
:class="{
'!pb-4': embedMode,
}"
>
<div class="flex gap-3 text-gray-800 h-7 mb-1 items-center justify-between">
<span class="text-[13px]">
{{ $t('labels.description') }}
@ -1254,8 +1309,13 @@ watch(activeAiTab, (newValue) => {
</a-form-item>
<template v-if="props.fromTableExplorer">
<a-form-item>
<NcButton v-if="!enableDescription" size="small" type="text" @click.stop="triggerDescriptionEnable">
<a-form-item
v-if="!enableDescription"
:class="{
'!pb-4': embedMode,
}"
>
<NcButton size="small" type="text" @click.stop="triggerDescriptionEnable">
<div class="flex !text-gray-700 items-center gap-2">
<GeneralIcon icon="plus" class="h-4 w-4" />
@ -1266,8 +1326,14 @@ watch(activeAiTab, (newValue) => {
</NcButton>
</a-form-item>
</template>
<template v-else>
<div class="flex items-center justify-between gap-2 empty:hidden">
<div
class="flex items-center justify-between gap-2 empty:hidden sticky bottom-0 z-10 bg-white px-5 pb-5 -mx-5"
:class="{
'border-t-1 border-nc-border-gray-medium pt-3': isScrollEnabled,
}"
>
<NcButton v-if="!enableDescription" size="small" type="text" @click.stop="triggerDescriptionEnable">
<div class="flex !text-gray-700 items-center gap-2">
<GeneralIcon icon="plus" class="h-4 w-4" />

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

@ -638,7 +638,7 @@ const enableAI = async () => {
<template>
<div
v-if="suggestionPreviewed && !suggestionPreviewed.unsupported && suggestionPreviewed.type === 'function'"
class="w-84 fixed bg-white z-10 pl-3 pt-3 border-1 shadow-md rounded-xl"
class="w-84 fixed bg-white z-11 pl-3 pt-3 border-1 shadow-md rounded-xl"
:style="{
left: suggestionPreviewPostion.left,
top: suggestionPreviewPostion.top,

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

@ -118,6 +118,10 @@ const debouncedValidate = useDebounceFn(async () => {
dataType: FormulaDataTypes.UNKNOWN,
}
}
} finally {
if (vModel.value?.colOptions?.parsed_tree?.dataType !== parsedTree.value?.dataType) {
vModel.value.meta.display_type = null
}
}
}, 300)
@ -175,15 +179,6 @@ watch(
immediate: true,
},
)
watch(parsedTree, (value, oldValue) => {
if (oldValue === undefined && value) {
return
}
if (value?.dataType !== oldValue?.dataType) {
vModel.value.meta.display_type = null
}
})
</script>
<template>
@ -209,9 +204,14 @@ watch(parsedTree, (value, oldValue) => {
<div>{{ $t('labels.formatting') }}</div>
</div>
</template>
<div class="flex flex-col px-0.5 gap-4">
<div class="flex flex-col px-0.5 gap-4 pb-0.5">
<a-form-item class="mt-4" :label="$t('general.format')">
<a-select v-model:value="vModel.meta.display_type" class="w-full" :placeholder="$t('labels.selectAFormatType')">
<NcSelect
v-model:value="vModel.meta.display_type"
class="w-full nc-select-shadow"
:placeholder="$t('labels.selectAFormatType')"
allow-clear
>
<a-select-option v-for="option in supportedFormulaAlias" :key="option.value" :value="option.value">
<div class="flex w-full items-center gap-2 justify-between">
<div class="w-full">
@ -226,7 +226,7 @@ watch(parsedTree, (value, oldValue) => {
/>
</div>
</a-select-option>
</a-select>
</NcSelect>
</a-form-item>
<template

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

@ -1,13 +1,147 @@
<!-- File not in use for now -->
<script setup lang="ts">
import { isAIPromptCol, UITypes } from 'nocodb-sdk'
const props = defineProps<{
value: any
modelValue: any
}>()
const emit = defineEmits(['update:value'])
const emit = defineEmits(['update:modelValue', 'navigateToIntegrations'])
const { t } = useI18n()
const meta = inject(MetaInj)!
const workspaceStore = useWorkspace()
const { activeWorkspaceId } = storeToRefs(workspaceStore)
const availableFields = computed(() => {
if (!meta.value?.columns) return []
return meta.value.columns.filter((c) => c.title && !c.system && c.uidt !== UITypes.ID)
})
const vModel = useVModel(props, 'modelValue', emit)
const { isEdit, setAdditionalValidations, column, formattedData, loadData, disableSubmitBtn } = useColumnCreateStoreOrThrow()
const { aiIntegrationAvailable, generateRows } = useNocoAi()
const { isFeatureEnabled } = useBetaFeatureToggle()
const previewRow = ref<Row>({
row: {},
oldRow: {},
rowMeta: { new: true },
})
const previewFieldTitle = ref(vModel.value.title || 'temp_title')
const generatingPreview = ref(false)
const isAlreadyGenerated = ref(false)
const isPreviewEnabled = computed(() => {
const isFieldAddedInPromt = availableFields.value.some((f) => {
return vModel.value.prompt_raw?.includes(`{${f.title}}`)
})
return isFieldAddedInPromt
})
const isEnabledGenerateText = computed({
get: () => {
return vModel.value.meta?.[LongTextAiMetaProp]
},
set: (value: boolean) => {
vModel.value.meta[LongTextAiMetaProp] = value
vModel.value.prompt_raw = ''
previewRow.value.row = {}
isAlreadyGenerated.value = false
},
})
const loadViewData = async () => {
if (!formattedData.value.length) {
await loadData(undefined, false)
}
}
const generate = async () => {
generatingPreview.value = true
await loadViewData()
const pk = formattedData.value.length ? extractPkFromRow(unref(formattedData.value[0].row), meta.value?.columns || []) : ''
if (!formattedData.value.length || !pk) {
message.error('Include at least 1 sample record in table to generate')
generatingPreview.value = false
return
}
previewFieldTitle.value = vModel.value?.title || 'temp_title'
const res = await generateRows(
meta.value.id!,
{
title: previewFieldTitle.value,
prompt_raw: vModel.value.prompt_raw,
fk_integration_id: vModel.value.fk_integration_id,
uidt: UITypes.LongText,
},
[pk],
)
const vModel = useVModel(props, 'value', emit)
if (res?.length && res[0]?.[previewFieldTitle.value]) {
previewRow.value.row = {
...res[0],
[previewFieldTitle.value]: {
value: res[0]?.[previewFieldTitle.value],
},
}
isAlreadyGenerated.value = true
}
generatingPreview.value = false
}
const isPromptEnabled = computed(() => {
if (isEdit.value) {
return isAIPromptCol(column.value) || isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)
}
return isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)
})
onMounted(() => {
// set default value
vModel.value.prompt_raw = (column?.value?.colOptions as Record<string, any>)?.prompt_raw || ''
})
const validators = {
fk_integration_id: [
{
validator: (_: any, value: any) => {
return new Promise<void>((resolve, reject) => {
if (isEnabledGenerateText.value && !value) {
reject(new Error(t('title.aiIntegrationMissing')))
}
resolve()
})
},
},
],
}
if (isEdit.value) {
vModel.value.fk_integration_id = vModel.value?.colOptions?.fk_integration_id
}
setAdditionalValidations({
...validators,
})
provide(EditColumnInj, ref(true))
const richMode = computed({
get: () => !!vModel.value.meta?.richMode,
@ -18,21 +152,214 @@ const richMode = computed({
},
})
const handleDisableSubmitBtn = () => {
if (!isEnabledGenerateText.value) {
if (disableSubmitBtn.value) {
disableSubmitBtn.value = false
}
return
}
if (isPreviewEnabled.value) {
disableSubmitBtn.value = false
} else {
disableSubmitBtn.value = true
}
}
watch(richMode, () => {
vModel.value.cdf = null
})
watch(isPreviewEnabled, handleDisableSubmitBtn, {
immediate: true,
})
</script>
<template>
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-4">
<a-form-item>
<div class="flex items-center gap-1">
<NcSwitch v-model:checked="richMode">
<div class="text-sm text-gray-800 select-none">
{{ $t('labels.enableRichText') }}
<NcTooltip :disabled="!isEnabledGenerateText">
<template #title> Rich text formatting is not supported when generate text using AI is enabled </template>
<div class="flex items-center gap-1">
<NcSwitch v-model:checked="richMode" :disabled="isEnabledGenerateText">
<div class="text-sm text-gray-800 select-none font-semibold">
{{ $t('labels.enableRichText') }}
</div>
</NcSwitch>
</div>
</NcTooltip>
</a-form-item>
<div v-if="isPromptEnabled" class="relative">
<a-form-item class="flex items-center">
<NcTooltip :disabled="!richMode" class="flex items-center">
<template #title> Generate text using AI is not supported when rich text formatting is enabled </template>
<NcSwitch
v-model:checked="isEnabledGenerateText"
:disabled="richMode"
class="nc-ai-field-generate-text nc-ai-input"
@change="handleDisableSubmitBtn"
>
<span
class="text-sm font-semibold pl-1"
:class="{
'text-nc-content-purple-dark': isEnabledGenerateText,
'text-nc-content-gray': !isEnabledGenerateText,
}"
>
Generate text using AI
</span>
</NcSwitch>
</NcTooltip>
<NcTooltip class="ml-2 mr-[40px] flex cursor-pointer">
<template #title> Use AI to generate content based on record data. </template>
<GeneralIcon icon="info" class="text-nc-content-gray-muted hover:text-nc-content-gray-subtle opacity-70 w-3.5 h-3.5" />
</NcTooltip>
<div class="flex-1"></div>
<div class="absolute right-0">
<AiSettings
v-model:fk-integration-id="vModel.fk_integration_id"
v-model:model="vModel.model"
v-model:randomness="vModel.randomness"
:workspace-id="activeWorkspaceId"
:show-tooltip="false"
:is-edit-column="isEdit"
placement="bottomRight"
>
<NcButton size="xs" theme="ai" class="!px-1" type="text">
<GeneralIcon icon="settings" />
</NcButton>
</AiSettings>
</div>
</a-form-item>
</div>
<template v-if="isPromptEnabled && (!isEdit ? aiIntegrationAvailable && isEnabledGenerateText : isEnabledGenerateText)">
<a-form-item class="flex">
<div class="nc-prompt-input-wrapper bg-nc-bg-gray-light rounded-lg w-full">
<AiPromptWithFields
v-model="vModel.prompt_raw"
:options="availableFields"
:read-only="!aiIntegrationAvailable"
placeholder="Write custom AI Prompt instruction here"
prompt-field-tag-class-name="!text-nc-content-purple-dark font-weight-500"
suggestion-icon-class-name="!text-nc-content-purple-medium"
/>
<div class="rounded-b-lg flex items-center gap-1.5 p-1">
<GeneralIcon icon="info" class="!text-nc-content-purple-medium w-3.5 h-3.5" />
<span class="text-xs text-nc-content-gray-subtle2"
>Mention fields using curly braces, e.g. <span class="text-nc-content-purple-dark">{Field name}</span>.</span
>
</div>
</div>
</a-form-item>
<div v-if="aiIntegrationAvailable && isEnabledGenerateText" class="nc-ai-options-preview overflow-hidden">
<div>
<div
class="flex items-center gap-2 transition-all duration-300"
:class="{
'pl-3 py-2 pr-2': !isAlreadyGenerated,
'pl-3 py-1 pr-1 border-b-1 border-nc-border-gray-medium': isAlreadyGenerated,
}"
>
<div class="flex flex-col flex-1 gap-1">
<div class="flex items-center gap-2">
<span class="text-sm font-bold text-nc-content-gray-subtle">Preview</span>
<NcTooltip class="flex cursor-pointer">
<template #title> Preview is generated using the first record in this table</template>
<GeneralIcon
icon="info"
class="text-nc-content-gray-muted hover:text-nc-content-gray-subtle opacity-70 w-3.5 h-3.5"
/>
</NcTooltip>
</div>
<span v-if="!isAlreadyGenerated" class="text-[11px] leading-[18px] text-nc-content-gray-muted">
Include at least 1 field in prompt.
</span>
</div>
<NcTooltip :disabled="isPreviewEnabled">
<template #title> Include at least 1 field in prompt to generate </template>
<NcButton
class="nc-aioptions-preview-generate-btn"
:class="{
'nc-is-already-generated': isAlreadyGenerated,
'nc-preview-enabled': isPreviewEnabled,
}"
size="xs"
:type="isAlreadyGenerated ? 'text' : 'secondary'"
:theme="isPreviewEnabled ? 'ai' : 'default'"
:disabled="!isPreviewEnabled"
:loading="generatingPreview"
@click.stop="generate"
>
<div
:class="{
'nc-animate-dots min-w-[91px] text-left': generatingPreview,
'min-w-[102px]': isAlreadyGenerated && generatingPreview,
'min-w-[80px]': !isAlreadyGenerated && generatingPreview,
}"
>
{{
isAlreadyGenerated
? generatingPreview
? 'Re-generating'
: 'Re-generate'
: generatingPreview
? 'Generating'
: 'Generate preview'
}}
</div>
</NcButton>
</NcTooltip>
</div>
<div v-if="previewRow.row?.[previewFieldTitle]?.value">
<div class="relative">
<LazySmartsheetRow :row="previewRow">
<LazySmartsheetCell
:edit-enabled="true"
:model-value="previewRow.row[previewFieldTitle]"
:column="vModel"
class="!border-none h-auto my-auto pl-1"
/>
</LazySmartsheetRow>
</div>
</div>
</NcSwitch>
</div>
</div>
</a-form-item>
</template>
<AiIntegrationNotFound v-if="!aiIntegrationAvailable && isEnabledGenerateText && isPromptEnabled" />
</div>
</template>
<style lang="scss" scoped>
:deep(.ant-form-item-control-input-content) {
@apply flex items-center;
}
.nc-prompt-input-wrapper {
@apply border-1 border-nc-border-gray-medium;
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08);
}
.nc-ai-options-preview {
@apply rounded-lg border-1 border-nc-border-gray-medium;
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08);
:deep(.nc-text-area-expand-btn) {
@apply right-1;
}
}
.nc-aioptions-preview-generate-btn {
&:not(.nc-is-already-generated) {
&.nc-preview-enabled {
@apply !border-transparent;
}
}
}
</style>

10
packages/nc-gui/components/smartsheet/column/UITypesOptionsWithSearch.vue

@ -17,13 +17,7 @@ const { isMetaReadOnly } = useRoles()
const { isFeatureEnabled } = useBetaFeatureToggle()
const filteredOptions = computed(
() =>
options.value?.filter(
(c) =>
!(c.name === 'AIButton' && !isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)) &&
(c.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
(UITypesName[c.name] && UITypesName[c.name].toLowerCase().includes(searchQuery.value.toLowerCase()))),
) ?? [],
() => options.value?.filter((c) => searchCompare([c.name, UITypesName[c.name]], searchQuery.value)) ?? [],
)
const inputRef = ref()
@ -126,7 +120,7 @@ watch(
'hover:bg-gray-100 cursor-pointer': !isDisabledUIType(option.name),
'bg-gray-100 nc-column-list-option-active': activeFieldIndex === index && !isDisabledUIType(option.name),
'!text-gray-400 cursor-not-allowed': isDisabledUIType(option.name),
'!text-nc-content-purple-dark': option.name === 'AIButton',
'!text-nc-content-purple-dark': option.name === 'AIButton' || option.name === 'AIPrompt',
},
]"
:data-testid="option.name"

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

@ -4,6 +4,7 @@ import { message } from 'ant-design-vue'
import {
ButtonActionsType,
UITypes,
isAIPromptCol,
isLinksOrLTAR,
isSystemColumn,
isVirtualCol,
@ -417,6 +418,7 @@ const onFieldUpdate = (state: TableExplorerColumn, skipLinkChecks = false) => {
const diffs = Object.fromEntries(
Object.entries(pdiffs).filter(([_, value]) => value !== undefined),
) as Partial<TableExplorerColumn>
if (
Object.keys(diffs).length === 0 ||
// skip custom prop since it's only used for custom LTAR links
@ -642,9 +644,20 @@ const isColumnValid = (column: TableExplorerColumn) => {
}
}
if (column.uidt === UITypes.Button && isNew) {
if (column.type === ButtonActionsType.Url && !column.formula_raw) return false
if (column.type === ButtonActionsType.Webhook && !column.fk_webhook_id) return false
if (column.uidt === UITypes.Button) {
if (isNew) {
if (column.type === ButtonActionsType.Url && !column.formula_raw) return false
if (column.type === ButtonActionsType.Webhook && !column.fk_webhook_id) return false
}
if (column.type === ButtonActionsType.Ai) {
return !(
!column.fk_integration_id ||
!column.formula_raw?.trim() ||
!column.output_column_ids?.length ||
!column.output_column_ids?.split(',')?.length
)
}
}
return true
@ -703,6 +716,11 @@ function updateDefaultColumnValues(column: TableExplorerColumn) {
column.fk_webhook_id = colOptions?.fk_webhook_id
column.icon = colOptions?.icon
column.formula_raw = column.colOptions?.formula_raw
if (column.type === ButtonActionsType.Ai) {
column.output_column_ids = colOptions?.output_column_ids
column.fk_integration_id = colOptions?.fk_integration_id
}
} else {
column.type = column?.type || ButtonActionsType.Url
@ -722,6 +740,16 @@ function updateDefaultColumnValues(column: TableExplorerColumn) {
}
}
if (column.uidt === UITypes.LongText && isAIPromptCol(column)) {
if (column?.id) {
const colOptions = column.colOptions as Record<string, any>
column.prompt_raw = colOptions?.prompt_raw
} else {
column.prompt_raw = column.prompt_raw || ''
}
}
return column
}
@ -1037,6 +1065,10 @@ const onClickCopyFieldUrl = async (field: ColumnType) => {
await copy(field.id!)
isFieldIdCopied.value = true
await ncDelay(5000)
isFieldIdCopied.value = false
}
const keys = useMagicKeys()

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

@ -327,6 +327,10 @@ const copyRecordUrl = async () => {
)
isRecordLinkCopied.value = true
await ncDelay(5000)
isRecordLinkCopied.value = false
}
const saveChanges = async () => {

1
packages/nc-gui/components/smartsheet/grid/GroupByTable.vue

@ -276,6 +276,7 @@ async function deleteSelectedRowsWrapper() {
</script>
<template>
<!-- eslint-disable vue/no-restricted-v-bind -->
<Table
v-if="vGroup.rows"
v-model:selected-all-records="selectedAllRecords"

75
packages/nc-gui/components/smartsheet/grid/InfiniteTable.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import {
ButtonActionsType,
type ButtonType,
type ColumnReqType,
type ColumnType,
@ -8,6 +7,7 @@ import {
UITypes,
type ViewType,
ViewTypes,
isAIPromptCol,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR,
@ -665,15 +665,13 @@ const onActiveCellChanged = () => {
}
}
const isOpen = ref(false)
const isDeleteAllModalIsOpen = ref(false)
async function deleteAllRecords() {
isDeleteAllModalIsOpen.value = true
function closeDlg() {
isOpen.value = false
close(200)
}
const { close } = useDialog(resolveComponent('DlgRecordDeleteAll'), {
'modelValue': isDeleteAllModalIsOpen,
'rows': totalRows.value,
@ -685,10 +683,14 @@ async function deleteAllRecords() {
},
})
function closeDlg() {
isOpen.value = false
close(200)
}
await until(isDeleteAllModalIsOpen).toBe(false)
}
const isOpen = ref(false)
async function expandRows({
newRows,
newColumns,
@ -1022,8 +1024,8 @@ const isSelectedOnlyAI = computed(() => {
if (selectedRange.start.col === selectedRange.end.col) {
const field = fields.value[selectedRange.start.col]
return {
enabled: field.uidt === UITypes.Button && (field?.colOptions as ButtonType)?.type === ButtonActionsType.Ai,
disabled: !ncIsArrayIncludes(aiIntegrations.value, (field?.colOptions as ButtonType)?.fk_integration_id, 'id'),
enabled: isAIPromptCol(field) || isAiButton(field),
disabled: !(field?.colOptions as ButtonType)?.fk_integration_id,
}
}
@ -1046,9 +1048,7 @@ const generateAIBulk = async () => {
let outputColumnIds = [field.id]
const isAiButton = field.uidt === UITypes.Button && (field?.colOptions as ButtonType)?.type === ButtonActionsType.Ai
if (isAiButton) {
if (isAiButton(field)) {
outputColumnIds =
ncIsString(field.colOptions?.output_column_ids) && field.colOptions.output_column_ids.split(',').length > 0
? field.colOptions.output_column_ids.split(',')
@ -2143,13 +2143,10 @@ watch(vSelectedAllRecords, (selectedAll) => {
top: `${(index + 1) * rowHeight - 6}px`,
zIndex: 100001,
}"
class="absolute z-30 left-0"
class="absolute z-30 left-0 w-full flex"
>
<div
class="flex items-center gap-2 transform bg-yellow-500 px-2 py-1 rounded-br-md font-semibold text-xs text-gray-800"
:style="{
transform: `translateX(${scrollLeft - leftOffset}px)`,
}"
class="sticky left-0 flex items-center gap-2 transform bg-yellow-500 px-2 py-1 rounded-br-md font-semibold text-xs text-gray-800"
>
Row filtered
@ -2168,13 +2165,10 @@ watch(vSelectedAllRecords, (selectedAll) => {
top: `${(index + 1) * rowHeight - 6}px`,
zIndex: 100000,
}"
class="absolute transform z-30 left-0"
class="absolute transform z-30 left-0 w-full flex"
>
<div
class="flex items-center gap-2 transform bg-yellow-500 px-2 py-1 rounded-br-md font-semibold text-xs text-gray-800"
:style="{
transform: `translateX(${scrollLeft - leftOffset}px)`,
}"
class="sticky left-0 flex items-center gap-2 transform bg-yellow-500 px-2 py-1 rounded-br-md font-semibold text-xs text-gray-800"
>
Row moved
@ -2252,13 +2246,13 @@ watch(vSelectedAllRecords, (selectedAll) => {
</span>
<div
v-else-if="!row.rowMeta?.saving && !row.rowMeta?.isLoading"
class="cursor-pointer flex items-center border-1 border-gray-100 active:ring rounded p-1 hover:(bg-gray-50)"
class="cursor-pointer flex items-center border-1 border-gray-100 active:ring rounded-md p-1 hover:(bg-white border-nc-border-gray-medium)"
>
<component
:is="iconMap.expand"
:is="iconMap.maximize"
v-if="expandForm"
v-e="['c:row-expand:open']"
class="select-none transform hover:(text-black scale-120) nc-row-expand"
class="select-none transform nc-row-expand opacity-90 w-4 h-4"
@click="expandAndLooseFocus(row, state)"
/>
</div>
@ -2874,7 +2868,7 @@ watch(vSelectedAllRecords, (selectedAll) => {
top: 0;
left: 0;
width: 100%;
box-shadow: 0 0 0 2px #3366ff !important;
box-shadow: 0 0 0 1.5px #3366ff !important;
border-radius: 2px;
}
@ -2928,6 +2922,35 @@ watch(vSelectedAllRecords, (selectedAll) => {
background: white;
@apply border-r-1 border-r-gray-100;
}
tbody {
tr:not(.nc-grid-add-new-cell):not(.placeholder) td:nth-child(3) {
&.active-cell {
@apply border-l-[1.5px] !border-l-transparent;
}
&.filling::after {
left: 0px;
}
}
tr:not(.nc-grid-add-new-cell):not(.placeholder):nth-child(1) td {
&.active-cell {
@apply border-t-[1.5px] !border-t-transparent;
}
&.filling::after {
top: 0px;
}
}
tr:not(.nc-grid-add-new-cell):not(.placeholder):nth-last-child(2) td {
&.active-cell {
@apply border-b-[1.5px] !border-b-transparent;
}
&.filling::after {
bottom: 0px;
}
}
}
}
.nc-grid-skeleton-loader {

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

@ -3,9 +3,9 @@ import axios from 'axios'
import { nextTick } from '@vue/runtime-core'
import type { ButtonType, ColumnReqType, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import {
ButtonActionsType,
UITypes,
ViewTypes,
isAIPromptCol,
isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR,
@ -852,8 +852,8 @@ const isSelectedOnlyAI = computed(() => {
if (selectedRange.start.col === selectedRange.end.col) {
const field = fields.value[selectedRange.start.col]
return {
enabled: field.uidt === UITypes.Button && (field?.colOptions as ButtonType)?.type === ButtonActionsType.Ai,
disabled: !ncIsArrayIncludes(aiIntegrations.value, (field?.colOptions as ButtonType)?.fk_integration_id, 'id'),
enabled: isAIPromptCol(field) || isAiButton(field),
disabled: !(field?.colOptions as ButtonType)?.fk_integration_id,
}
}
@ -876,9 +876,7 @@ const generateAIBulk = async () => {
let outputColumnIds = [field.id]
const isAiButton = field.uidt === UITypes.Button && (field?.colOptions as ButtonType)?.type === ButtonActionsType.Ai
if (isAiButton) {
if (isAiButton(field)) {
outputColumnIds =
ncIsString(field.colOptions?.output_column_ids) && field.colOptions.output_column_ids.split(',').length > 0
? field.colOptions.output_column_ids.split(',')
@ -2139,13 +2137,13 @@ onKeyStroke('ArrowDown', onDown)
</span>
<div
v-else-if="!row.rowMeta.saving"
class="cursor-pointer flex items-center border-1 border-gray-100 active:ring rounded p-1 hover:(bg-gray-50)"
class="cursor-pointer flex items-center border-1 border-gray-100 active:ring rounded-md p-1 hover:(bg-white border-nc-border-gray-medium)"
>
<component
:is="iconMap.expand"
:is="iconMap.maximize"
v-if="expandForm"
v-e="['c:row-expand:open']"
class="select-none transform hover:(text-black scale-120) nc-row-expand"
class="select-none nc-row-expand opacity-90 w-4 h-4"
@click="expandAndLooseFocus(row, state)"
/>
</div>

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

@ -43,9 +43,16 @@ const editColumnDropdown = ref(false)
const columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null)
const columnTypeName = computed(() => {
if (column.value.uidt === UITypes.LongText && parseProp(column?.value?.meta)?.richMode) {
return UITypesName.RichText
if (column.value.uidt === UITypes.LongText) {
if (parseProp(column.value?.meta)?.richMode) {
return UITypesName.RichText
}
if (parseProp(column.value?.meta)?.[LongTextAiMetaProp]) {
return UITypesName.AIPrompt
}
}
return column.value.uidt ? UITypesName[column.value.uidt] : ''
})

4
packages/nc-gui/components/smartsheet/header/CellIcon.ts

@ -20,6 +20,8 @@ const renderIcon = (column: ColumnType, abstractType: any) => {
return iconMap.cellSingleSelect
} else if (isBoolean(column, abstractType)) {
return iconMap.cellCheckbox
} else if (isAI(column)) {
return iconMap.cellAi
} else if (isTextArea(column)) {
return iconMap.cellLongText
} else if (isEmail(column)) {
@ -51,8 +53,6 @@ const renderIcon = (column: ColumnType, abstractType: any) => {
return iconMap.cellUser
}
return iconMap.cellUser
} else if (isAI(column)) {
return iconMap.cellAi
} else if (isInt(column, abstractType) || isFloat(column, abstractType)) {
return iconMap.cellNumber
} else if (isString(column, abstractType)) {

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

@ -45,8 +45,21 @@ const { fieldsToGroupBy, groupByLimit } = useViewGroupByOrThrow(view)
const { isUIAllowed, isMetaReadOnly, isDataReadOnly } = useRoles()
const { aiIntegrations } = useNocoAi()
const isLoading = ref<'' | 'hideOrShow' | 'setDisplay'>('')
const columnInvalid = computed<{ isInvalid: boolean; tooltip: string }>(() => {
if (!column?.value) {
return {
isInvalid: false,
tooltip: '',
}
}
return isColumnInvalid(column.value, aiIntegrations.value, isPublic.value || !isUIAllowed('dataEdit'))
})
const setAsDisplayValue = async () => {
isLoading.value = 'setDisplay'
try {
@ -415,6 +428,10 @@ const onClickCopyFieldUrl = async (field: ColumnType) => {
await copy(field.id!)
isFieldIdCopied.value = true
await ncDelay(5000)
isFieldIdCopied.value = false
}
const onDeleteColumn = () => {
@ -431,7 +448,7 @@ const onDeleteColumn = () => {
overlay-class-name="nc-dropdown-column-operations !border-1 rounded-lg !shadow-xl "
@click.stop="openDropdown"
>
<div class="flex gap-0.5 items-center" @dblclick.stop>
<div class="flex gap-1 items-center" @dblclick.stop>
<div v-if="isExpandedForm" class="h-[1px]">&nbsp;</div>
<NcTooltip v-if="column?.description?.length && !isExpandedForm" class="flex">
<template #title>
@ -441,14 +458,10 @@ const onDeleteColumn = () => {
</NcTooltip>
<NcTooltip class="flex items-center">
<GeneralIcon
v-if="isColumnInvalid(column) && !isExpandedForm"
class="text-orange-500 w-3.5 h-3.5 ml-2"
icon="alertTriangle"
/>
<GeneralIcon v-if="columnInvalid.isInvalid && !isExpandedForm" class="text-red-300 w-3.5 h-3.5" icon="alertTriangle" />
<template #title>
{{ $t('msg.invalidColumnConfiguration') }}
{{ $t(columnInvalid.tooltip) }}
</template>
</NcTooltip>
<GeneralIcon

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

@ -126,13 +126,14 @@ const showTooltipAlways = computed(() => {
const columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null)
const columnTypeName = computed(() => {
if (column.value.uidt === UITypes.LongText && parseProp(column?.value?.meta)?.richMode) {
return UITypesName.RichText
}
if (column.value.uidt === UITypes.LinkToAnotherRecord && column.value.colOptions?.type === RelationTypes.ONE_TO_ONE) {
return UITypesName[UITypes.Links]
}
if (isAiButton(column.value)) {
return UITypesName.AIButton
}
return column.value.uidt ? UITypesName[column.value.uidt] : ''
})

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

@ -152,6 +152,10 @@ const { copy } = useCopy()
const onViewIdCopy = async () => {
await copy(view.value!.id!)
isViewIdCopied.value = true
await ncDelay(5000)
isViewIdCopied.value = false
}
const onDelete = async () => {

22
packages/nc-gui/components/smartsheet/topbar/ViewListDropdown.vue

@ -21,7 +21,7 @@ const { loadViews, navigateToView } = viewsStore
const { refreshCommandPalette } = useCommandPalette()
const { aiIntegrationAvailable } = useNocoAi()
const { isFeatureEnabled } = useBetaFeatureToggle()
const isOpen = ref<boolean>(false)
@ -254,17 +254,15 @@ async function onOpenModal({
</div>
</a-menu-item>
<NcDivider />
<a-menu-item
v-if="aiIntegrationAvailable"
data-testid="sidebar-view-create-ai"
@click="onOpenModal({ type: 'AI' })"
>
<div class="nc-viewlist-submenu-popup-item">
<GeneralIcon icon="ncAutoAwesome" class="!w-4 !h-4 text-nc-fill-purple-dark" />
<div>{{ $t('labels.aiSuggested') }}</div>
</div>
</a-menu-item>
<template v-if="isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)">
<NcDivider />
<a-menu-item data-testid="sidebar-view-create-ai" @click="onOpenModal({ type: 'AI' })">
<div class="nc-viewlist-submenu-popup-item">
<GeneralIcon icon="ncAutoAwesome" class="!w-4 !h-4 text-nc-fill-purple-dark" />
<div>{{ $t('labels.aiSuggested') }}</div>
</div>
</a-menu-item>
</template>
</a-sub-menu>
</a-menu>
</div>

2
packages/nc-gui/components/virtual-cell/Button.vue

@ -210,7 +210,7 @@ const componentProps = computed(() => {
<style scoped lang="scss">
.nc-cell-button {
@apply rounded-lg px-2 flex items-center gap-2 transition-all justify-center;
@apply rounded-md px-2 flex items-center gap-2 transition-all justify-center;
&:not([class*='text']) {
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.06), 0px 5px 3px -2px rgba(0, 0, 0, 0.02);
}

4
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -146,8 +146,8 @@ watch(
<div v-if="!isUnderLookup && !isSystemColumn(column)" class="flex justify-end gap-1 min-h-4 items-center">
<GeneralIcon
icon="expand"
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
icon="maximize"
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand h-3 w-3"
@click.stop="openChildList"
/>

4
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -145,8 +145,8 @@ watch(
<div v-if="!isUnderLookup || isForm" class="flex justify-end gap-1 min-h-4 items-center">
<GeneralIcon
icon="expand"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
icon="maximize"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand h-3 w-3"
@click.stop="openChildList"
/>

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

@ -1,8 +1,6 @@
<script lang="ts" setup>
import { UITypes, isVirtualCol, parseStringDateTime } from 'nocodb-sdk'
import MaximizeIcon from '~icons/nc-icons/maximize'
const props = withDefaults(
defineProps<{
row: any
@ -161,7 +159,7 @@ const displayValue = computed(() => {
class="z-10 flex items-center justify-center nc-expand-item !group-hover:visible !invisible !h-7 !w-7 transition-all !hover:children:(w-4.5 h-4.5)"
@click.stop="$emit('expand', row)"
>
<MaximizeIcon class="flex-none w-4 h-4 scale-125" />
<GeneralIcon icon="maximize" class="flex-none w-4 h-4 scale-125" />
</button>
</NcTooltip>
</div>

6
packages/nc-gui/components/workspace/integrations/ConnectionsTab.vue

@ -20,8 +20,6 @@ const {
setDefaultIntegration,
} = useIntegrationStore()
const { loadAiIntegrations } = useNocoAi()
const { $api, $e } = useNuxtApp()
const { allCollaborators } = storeToRefs(useWorkspace())
@ -164,10 +162,6 @@ const onDeleteConfirm = async () => {
sources: [...base.sources.filter((s) => s.id !== source.id)],
})
}
if (toBeDeletedIntegration.value?.type && toBeDeletedIntegration.value.type === IntegrationCategoryType.AI) {
loadAiIntegrations()
}
}
}

106
packages/nc-gui/components/workspace/integrations/IntegrationsTab.vue

@ -1,8 +1,9 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { IntegrationCategoryType } from 'nocodb-sdk'
import NcModal from '~/components/nc/Modal.vue'
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { IntegrationCategoryType, type IntegrationItemType, SyncDataType } from '#imports'
import { type IntegrationItemType, SyncDataType } from '#imports'
const props = withDefaults(
defineProps<{
@ -27,6 +28,10 @@ const { t } = useI18n()
const { syncDataUpvotes, updateSyncDataUpvotes } = useGlobal()
const { isFeatureEnabled } = useBetaFeatureToggle()
const easterEggToggle = computed(() => isFeatureEnabled(FEATURE_FLAG.INTEGRATIONS))
const router = useRouter()
const route = router.currentRoute
@ -39,6 +44,7 @@ const {
integrationsRefreshKey,
integrationsCategoryFilter,
activeViewTab,
loadDynamicIntegrations,
} = useIntegrationStore()
const focusTextArea: VNodeRef = (el) => el && el?.focus?.()
@ -144,6 +150,7 @@ const integrationsMapByCategory = computed(() => {
list: getIntegrationsByCategory(curr.value, searchQuery.value),
isAvailable: curr.isAvailable,
teleEventName: curr.teleEventName,
value: curr.value,
}
return acc
@ -212,6 +219,8 @@ const toggleShowOrHideAllCategory = () => {
}
onMounted(() => {
loadDynamicIntegrations()
if (!integrationsCategoryFilter.value.length) {
integrationsCategoryFilter.value = integrationCategoriesRef.value.map((c) => c.value)
}
@ -275,6 +284,7 @@ watch(activeViewTab, (value) => {
</div>
<div class="flex items-center gap-2 !max-w-[400px]">
<a-input
v-if="easterEggToggle"
v-model:value="searchQuery"
type="text"
class="flex-1 nc-input-border-on-value nc-search-integration-input !min-w-[300px] nc-input-sm flex-none"
@ -285,7 +295,7 @@ watch(activeViewTab, (value) => {
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500" />
</template>
</a-input>
<NcDropdown v-if="showFilter" v-model:visible="isOpenFilter">
<NcDropdown v-if="easterEggToggle && showFilter" v-model:visible="isOpenFilter">
<NcButton size="small" type="secondary">
<div class="flex items-center gap-2">
<GeneralIcon icon="filter" />
@ -328,7 +338,13 @@ watch(activeViewTab, (value) => {
</NcDropdown>
</div>
</div>
<NcButton type="ghost" size="small" class="!text-primary" @click="requestIntegration.isOpen = true">
<NcButton
v-if="easterEggToggle"
type="ghost"
size="small"
class="!text-primary"
@click="requestIntegration.isOpen = true"
>
Request Integration
</NcButton>
</div>
@ -352,7 +368,11 @@ watch(activeViewTab, (value) => {
}"
>
<template v-for="(category, key) in integrationsMapByCategory">
<div v-if="category.list.length" :key="key" class="integration-type-wrapper">
<div
v-if="(easterEggToggle || category.value === IntegrationCategoryType.DATABASE) && category.list.length"
:key="key"
class="integration-type-wrapper"
>
<div class="category-type-title flex gap-2">
{{ $t(category.title) }}
<NcBadge
@ -363,46 +383,47 @@ watch(activeViewTab, (value) => {
>
</div>
<div v-if="category.list.length" class="integration-type-list">
<NcTooltip
v-for="integration of category.list"
:key="integration.subType"
:disabled="integration?.isAvailable"
placement="bottom"
>
<template #title>{{ $t('tooltip.comingSoonIntegration') }}</template>
<div
:tabindex="0"
class="source-card focus-visible:outline-none outline-none h-full"
:class="{
'is-available': integration?.isAvailable,
}"
@click="handleAddIntegration(key, integration)"
<template v-for="integration of category.list" :key="integration.subType">
<NcTooltip
v-if="easterEggToggle || integration.isAvailable"
:disabled="integration?.isAvailable"
placement="bottom"
>
<div class="integration-icon-wrapper">
<component :is="integration.icon" class="integration-icon" :style="integration.iconStyle" />
</div>
<div class="flex-1">
<div class="name">{{ $t(integration.title) }}</div>
<div v-if="integration.subtitle" class="subtitle flex-1">{{ $t(integration.subtitle) }}</div>
</div>
<div v-if="integration?.isAvailable" class="action-btn">+</div>
<div v-else class="">
<NcButton
type="secondary"
size="xs"
class="integration-upvote-btn !rounded-lg !px-1 !py-0"
:class="{
selected: upvotesData.has(integration.subType),
}"
>
<div class="flex items-center gap-2">
<GeneralIcon icon="ncArrowUp" />
</div>
</NcButton>
<template #title>{{ $t('tooltip.comingSoonIntegration') }}</template>
<div
:tabindex="0"
class="source-card focus-visible:outline-none outline-none h-full"
:class="{
'is-available': integration?.isAvailable,
}"
@click="handleAddIntegration(key, integration)"
>
<div class="integration-icon-wrapper">
<component :is="integration.icon" class="integration-icon" :style="integration.iconStyle" />
</div>
<div class="flex-1">
<div class="name">{{ $t(integration.title) }}</div>
<div v-if="integration.subtitle" class="subtitle flex-1">{{ $t(integration.subtitle) }}</div>
</div>
<div v-if="integration?.isAvailable" class="action-btn">+</div>
<div v-else class="">
<NcButton
type="secondary"
size="xs"
class="integration-upvote-btn !rounded-lg !px-1 !py-0"
:class="{
selected: upvotesData.has(integration.subType),
}"
>
<div class="flex items-center gap-2">
<GeneralIcon icon="ncArrowUp" />
</div>
</NcButton>
</div>
</div>
</div>
</NcTooltip>
</NcTooltip>
</template>
</div>
</div>
</template>
@ -569,7 +590,6 @@ watch(activeViewTab, (value) => {
@apply text-gray-800;
}
}
&:not(.is-available) {
&:not(:hover) {
.integration-icon-wrapper {

8
packages/nc-gui/components/workspace/integrations/forms/EditOrAddCommon.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { IntegrationCategoryType, type SyncDataType } from '#imports'
import type { IntegrationCategoryType, SyncDataType } from '#imports'
import { clientTypes as _clientTypes } from '#imports'
const props = defineProps<{
@ -22,8 +22,6 @@ const {
updateIntegration,
} = useIntegrationStore()
const { loadAiIntegrations } = useNocoAi()
const isEditMode = computed(() => pageMode.value === IntegrationsPageMode.EDIT)
const initState = ref({
@ -43,10 +41,6 @@ const { form, formState, isLoading, initialState, submit } = useProvideFormBuild
try {
if (pageMode.value === IntegrationsPageMode.ADD) {
await saveIntegration(formState.value)
if (props.integrationType === IntegrationCategoryType.AI) {
loadAiIntegrations()
}
} else {
await updateIntegration({
id: activeIntegration.value?.id,

12
packages/nc-gui/components/workspace/integrations/forms/EditOrAddDatabase.vue

@ -658,12 +658,12 @@ watch(
>
<NcButton
type="text"
size="xsmall"
class="nc-extdb-btn-import-url !rounded-md !h-6 !px-2 flex-none"
size="small"
class="nc-extdb-btn-import-url !rounded-md !px-2 flex-none -my-1.5"
@click.stop="importURLDlg = true"
>
<div class="flex items-center gap-2">
<GeneralIcon icon="magic" class="flex-none text-yellow-500" />
<GeneralIcon icon="ncLink" class="flex-none" />
{{ $t('activity.useConnectionUrl') }}
</div>
</NcButton>
@ -672,7 +672,13 @@ watch(
<div class="text-sm text-gray-700">
Auto populate connection configuration using database connection URL
</div>
<a-textarea
:ref="
(el) => {
el?.$el?.focus()
}
"
v-model:value="importURL"
class="!rounded-lg !min-h-[120px] !max-h-[250px] nc-scrollbar-thin"
></a-textarea>

31
packages/nc-gui/components/workspace/project/AiCreateProject.vue

@ -28,6 +28,8 @@ const { workspaceId, baseType } = toRefs(props)
const { navigateToProject } = useGlobal()
const { $e } = useNuxtApp()
const { clone } = useUndoRedo()
const { aiIntegrationAvailable, aiError, aiLoading, createSchema, predictSchema } = useNocoAi()
@ -145,6 +147,21 @@ const onPredictSchema = async () => {
let currentMessageIndex = 0
let currentCharIndex = 0
let prompt = `${aiFormState.value.prompt}`
// Append optional information if provided
if (aiFormState.value.organization?.trim()) {
prompt += ` | Organization: ${aiFormState.value.organization}`
}
if (aiFormState.value.industry?.trim()) {
prompt += ` | Industry: ${aiFormState.value.industry}`
}
if (aiFormState.value.audience?.trim()) {
prompt += ` | Audience: ${aiFormState.value.audience}`
}
$e('a:base:ai:generate', prompt)
try {
const displayCharByChar = () => {
const currentMessage = loadingMessages[currentMessageIndex]
@ -169,19 +186,6 @@ const onPredictSchema = async () => {
// Set interval to display characters one by one
timerId = setInterval(displayCharByChar, 40) // Adjust the speed as needed (100ms)
let prompt = `${aiFormState.value.prompt}`
// Append optional information if provided
if (aiFormState.value.organization?.trim()) {
prompt += ` | Organization: ${aiFormState.value.organization}`
}
if (aiFormState.value.industry?.trim()) {
prompt += ` | Industry: ${aiFormState.value.industry}`
}
if (aiFormState.value.audience?.trim()) {
prompt += ` | Audience: ${aiFormState.value.audience}`
}
const res = await predictSchema(prompt)
if (!res?.tables) {
@ -465,7 +469,6 @@ onMounted(() => {
</div>
<div v-if="aiIntegrationAvailable" class="flex items-center gap-3">
<NcButton
v-e="['a:base:ai:generate']"
size="small"
:type="aiStep !== AI_STEP.MODIFY || isOldPromptChanged ? 'primary' : 'secondary'"
theme="ai"

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

@ -15,6 +15,13 @@ const FEATURES = [
enabled: false,
isEngineering: true,
},
{
id: 'integrations',
title: 'Integrations',
description: 'Enable dynamic integrations.',
enabled: false,
isEngineering: true,
},
{
id: 'geodata_column',
title: 'Geodata column',

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

@ -1,6 +1,6 @@
import rfdc from 'rfdc'
import type { ColumnReqType, ColumnType, TableType } from 'nocodb-sdk'
import { ButtonActionsType, UITypes, isLinksOrLTAR } from 'nocodb-sdk'
import { ButtonActionsType, UITypes, isAIPromptCol, isLinksOrLTAR } from 'nocodb-sdk'
import type { Ref } from 'vue'
import type { RuleObject } from 'ant-design-vue/es/form'
import { generateUniqueColumnName } from '~/helpers/parsers/parserHelpers'
@ -110,6 +110,10 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
return true
}
if (isAIPromptCol(formState.value)) {
return true
}
return false
})

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

@ -1,5 +1,5 @@
import type { ColumnType, LinkToAnotherRecordType, PaginatedType, RelationTypes, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isCreatedOrLastModifiedByCol, isCreatedOrLastModifiedTimeCol } from 'nocodb-sdk'
import { UITypes, isAIPromptCol, isCreatedOrLastModifiedByCol, isCreatedOrLastModifiedTimeCol } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import type { CellRange } from '#imports'
@ -286,6 +286,7 @@ export function useData(args: {
col.uidt === UITypes.Lookup ||
col.uidt === UITypes.Button ||
col.uidt === UITypes.Attachment ||
isAIPromptCol(col) ||
col.au ||
(isValidValue(col?.cdf) && / on update /i.test(col.cdf)))
)

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

@ -1,5 +1,5 @@
import type { ComputedRef, Ref } from 'vue'
import { UITypes, extractFilterFromXwhere } from 'nocodb-sdk'
import { UITypes, extractFilterFromXwhere, isAIPromptCol } from 'nocodb-sdk'
import type { Api, ColumnType, LinkToAnotherRecordType, PaginatedType, RelationTypes, TableType, ViewType } from 'nocodb-sdk'
import type { Row } from '../lib/types'
import { validateRowFilters } from '../utils/dataUtils'
@ -916,6 +916,7 @@ export function useInfiniteData(args: {
col.title &&
col.title in updatedRowData &&
(columnsToUpdate.has(col.uidt as UITypes) ||
isAIPromptCol(col) ||
col.au ||
(isValidValue(col?.cdf) && / on update /i.test(col.cdf as string)))
) {

63
packages/nc-gui/composables/useIntegrationsStore.ts

@ -90,6 +90,8 @@ const [useProvideIntegrationViewStore, _useIntegrationStore] = useInjectionState
const { t } = useI18n()
const { aiIntegrations } = useNocoAi()
const isFromIntegrationPage = ref(false)
const integrationsRefreshKey = ref(0)
@ -186,12 +188,17 @@ const [useProvideIntegrationViewStore, _useIntegrationStore] = useInjectionState
const deleteIntegration = async (integration: IntegrationType, force = false) => {
if (!integration?.id) return
$e('a:integration:delete')
try {
await api.integration.delete(integration.id, {
query: force ? { force: 'true' } : {},
})
$e('a:integration:delete')
if (integration.type === IntegrationsType.Ai) {
aiIntegrations.value = aiIntegrations.value.filter((i) => i.id !== integration.id)
}
await loadIntegrations()
// await message.success(`Connection ${integration.title} deleted successfully`)
@ -214,10 +221,21 @@ const [useProvideIntegrationViewStore, _useIntegrationStore] = useInjectionState
const updateIntegration = async (integration: IntegrationType) => {
if (!integration.id) return
$e('a:integration:update')
try {
await api.integration.update(integration.id, integration)
$e('a:integration:update')
if (integration.type === IntegrationsType.Ai) {
aiIntegrations.value = aiIntegrations.value.map((i) => {
if (i.id === integration.id) {
i.title = integration.title
}
return i
})
}
await loadIntegrations()
pageMode.value = null
@ -232,10 +250,23 @@ const [useProvideIntegrationViewStore, _useIntegrationStore] = useInjectionState
const setDefaultIntegration = async (integration: IntegrationType) => {
if (!integration.id) return
$e('a:integration:set-default')
try {
await api.integration.setDefault(integration.id)
$e('a:integration:set-default')
if (integration.type === IntegrationsType.Ai) {
aiIntegrations.value = aiIntegrations.value.map((i) => {
if (i.id === integration.id) {
i.is_default = true
} else {
i.is_default = false
}
return i
})
}
await loadIntegrations()
pageMode.value = null
@ -253,18 +284,29 @@ const [useProvideIntegrationViewStore, _useIntegrationStore] = useInjectionState
loadDatasourceInfo = false,
baseId: string | undefined = undefined,
) => {
if (mode === 'create') {
$e('a:integration:create')
} else {
$e('a:integration:duplicate')
}
try {
const response = await api.integration.create(integration)
if (mode === 'create') {
$e('a:integration:create')
} else {
$e('a:integration:duplicate')
}
if (response && response?.id) {
if (!loadDatasourceInfo) {
integrations.value.push(response)
}
if (response.type === IntegrationsType.Ai) {
aiIntegrations.value.push({
id: response.id,
title: response.title,
is_default: response.is_default,
type: response.type,
sub_type: response.sub_type,
})
}
}
await loadIntegrations(loadDatasourceInfo, baseId)
@ -402,7 +444,7 @@ const [useProvideIntegrationViewStore, _useIntegrationStore] = useInjectionState
return list
}
onMounted(async () => {
const loadDynamicIntegrations = async () => {
if (integrationsInitialized.value) return
integrationsInitialized.value = true
@ -451,7 +493,7 @@ const [useProvideIntegrationViewStore, _useIntegrationStore] = useInjectionState
integrationsRefreshKey.value++
}
})
}
const integrationsIconMap = computed(() => {
// eslint-disable-next-line no-unused-expressions
@ -496,6 +538,7 @@ const [useProvideIntegrationViewStore, _useIntegrationStore] = useInjectionState
setDefaultIntegration,
integrationsIconMap,
listIntegrationByType,
loadDynamicIntegrations,
}
}, 'integrations-store')

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

@ -3,6 +3,7 @@ import { computed } from 'vue'
import dayjs from 'dayjs'
import type { MaybeRef } from '@vueuse/core'
import type {
AIRecordType,
AttachmentType,
ColumnType,
LinkToAnotherRecordType,
@ -288,7 +289,15 @@ export function useMultiSelect(
}
if (columnObj.uidt === UITypes.LongText) {
textToCopy = `"${textToCopy.replace(/"/g, '\\"')}"`
if (parseProp(columnObj.meta)?.[LongTextAiMetaProp] === true) {
const aiCell: AIRecordType = (columnObj.title && rowObj.row[columnObj.title]) || null
if (aiCell) {
textToCopy = aiCell.value
}
} else {
textToCopy = `"${textToCopy.replace(/"/g, '\\"')}"`
}
}
return textToCopy

38
packages/nc-gui/composables/useNocoAi.ts

@ -1,4 +1,4 @@
import { type IntegrationType, IntegrationsType, type SerializedAiViewType, type TableType } from 'nocodb-sdk'
import type { IntegrationType, SerializedAiViewType, TableType } from 'nocodb-sdk'
const aiIntegrationNotFound = 'AI integration not found'
@ -13,15 +13,19 @@ export const useNocoAi = createSharedComposable(() => {
const { activeProjectId } = storeToRefs(basesStore)
const aiIntegrationAvailable = ref(true)
const aiLoading = ref(false)
const aiError = ref<string>('')
const aiIntegrations = ref<IntegrationType[]>([])
const aiIntegrations = ref<Partial<IntegrationType>[]>([])
const aiIntegrationAvailable = computed(() => !!aiIntegrations.value.length)
const isAiIntegrationAvailableInList = (integrationId?: string) => {
if (!aiIntegrationAvailable.value) return false
const { listIntegrationByType } = useProvideIntegrationViewStore()
return ncIsArrayIncludes(aiIntegrations.value, integrationId, 'id')
}
const callAiUtilsApi = async (operation: string, input: any, customBaseId?: string, skipMsgToast = false) => {
try {
@ -338,30 +342,9 @@ export const useNocoAi = createSharedComposable(() => {
}
}
const loadAiIntegrations = async () => {
aiIntegrations.value = (await listIntegrationByType(IntegrationsType.Ai)) || []
if (aiIntegrations.value.length) {
aiIntegrationAvailable.value = true
} else {
aiIntegrationAvailable.value = false
}
}
const { signedIn } = useGlobal()
watch(
signedIn,
(val) => {
if (val) {
loadAiIntegrations()
}
},
{ immediate: true },
)
return {
aiIntegrationAvailable,
isAiIntegrationAvailableInList,
aiLoading,
aiError,
predictFieldType,
@ -381,6 +364,5 @@ export const useNocoAi = createSharedComposable(() => {
repairFormula,
predictViews,
aiIntegrations,
loadAiIntegrations,
}
})

33
packages/nc-gui/composables/useViewAggregate.ts

@ -2,6 +2,7 @@ import type { Ref } from 'vue'
import {
type ColumnType,
CommonAggregations,
type FormulaType,
type TableType,
UITypes,
type ViewType,
@ -123,6 +124,32 @@ const [useProvideViewAggregate, useViewAggregate] = useInjectionState(
await updateGridViewColumn(fieldId, { aggregation: agg })
}
const aggregateFormulaFields = computed(() => {
return fields.value
.filter((field) => {
if (!field?.id || !field?.title) return false
if (
!isFormula(field) ||
!gridViewCols.value[field.id] ||
!gridViewCols.value[field.id]?.aggregation ||
gridViewCols.value[field.id]?.aggregation === CommonAggregations.None ||
!(field.colOptions as FormulaType)?.formula_raw
) {
return false
}
return true
})
.map((field) => {
return {
id: field.id,
aggregation: gridViewCols.value[field.id!]?.aggregation ?? CommonAggregations.None,
formula_raw: (field.colOptions as FormulaType)?.formula_raw ?? '',
}
})
})
reloadAggregate?.on(async (_fields) => {
if (!_fields || !_fields?.fields.length) {
await loadViewAggregate()
@ -135,6 +162,12 @@ const [useProvideViewAggregate, useViewAggregate] = useInjectionState(
acc[f.id] = field.aggregation ?? gridViewCols.value[f.id].aggregation ?? CommonAggregations.None
for (const formulaField of aggregateFormulaFields.value) {
if (!acc[formulaField.id!] && formulaField.formula_raw.includes(field.title)) {
acc[formulaField.id!] = formulaField.aggregation
}
}
return acc
}, {} as Record<string, string>)

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

@ -1,4 +1,4 @@
import { ButtonActionsType, type ColumnType, FieldNameFromUITypes, UITypes } from 'nocodb-sdk'
import { ButtonActionsType, type ColumnType, FieldNameFromUITypes, UITypes, UITypesName } from 'nocodb-sdk'
import isURL from 'validator/lib/isURL'
import { pluralize } from 'inflection'
@ -478,8 +478,16 @@ export const generateUniqueColumnName = ({
case UITypes.Button: {
if (formState?.type === ButtonActionsType.Ai) {
defaultColumnName = `AI ${defaultColumnName}`
defaultColumnName = UITypesName.AIButton
}
break
}
case UITypes.LongText: {
if (formState?.meta?.[LongTextAiMetaProp] === true) {
defaultColumnName = UITypesName.AIPrompt
}
break
}
}

18
packages/nc-gui/helpers/tiptapExtensions/mention/FieldList.vue

@ -56,6 +56,12 @@ export default {
return true
}
if (event.key === '}') {
setTimeout(() => {
this.selectItem(this.selectedIndex)
}, 250)
}
return false
},
@ -80,6 +86,7 @@ export default {
selectItem(index, _e) {
const item = this.items[index]
if (item) {
this.command({
id: item.title,
@ -92,7 +99,7 @@ export default {
<template>
<div
class="w-64 bg-white scroll-smooth nc-mention-list nc-scrollbar-md border-1 border-gray-200 rounded-lg max-h-64 !py-2 shadow-lg"
class="w-64 bg-white scroll-smooth nc-mention-list nc-scrollbar-thin border-1 border-gray-200 rounded-lg max-h-64 !py-2 px-2 shadow-lg"
@mousedown.stop
>
<template v-if="items.length">
@ -100,11 +107,16 @@ export default {
v-for="(item, index) in items"
:key="index"
:class="{ 'is-selected': index === selectedIndex }"
class="py-2 flex hover:bg-gray-100 transition-all cursor-pointer items-center gap-2 text-gray-800 pl-4"
class="py-2 flex hover:bg-gray-100 rounded-md transition-all cursor-pointer items-center gap-2 text-gray-800 pl-4"
@click="selectItem(index, $event)"
>
<component :is="getUIDTIcon(item.uidt)" v-if="item?.uidt" class="flex-none w-3.5 h-3.5" />
{{ item?.title || '' }}
<NcTooltip class="truncate" show-on-truncate-only :tooltip-style="{ zIndex: '10000' }">
<template #title>
{{ item?.title || '' }}
</template>
{{ item?.title || '' }}
</NcTooltip>
</div>
</template>
<div v-else class="px-4">No field available</div>

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "تفاصيل Webhook",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "ওযবহর বিবরণ",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Podrobnosti o webhooku",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Webhook Detaljer",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "Sichtbarkeit anzeigen"
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Ansicht zuweisen",
"webhookDetails": "Webhook-Details",
"showSaturdaysAndSundays": "Samstage und Sonntage anzeigen",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Detalles de Webhook",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Webhook xehetasunak",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "جزئیات وبهوک",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Webhotokin tiedot",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -655,7 +655,7 @@
"snapshotCreationFailed": "La création de l'instantané a échoué",
"snapshotCreationFailedDescription": "Impossible de créer votre instantané de base. Réessayez plus tard.",
"snapshotCooldownDescription": "Les instantanés ne peuvent être pris qu'à trois heures d'intervalle.",
"snapshotCooldownWarning": "Snapshot cooldown remaining",
"snapshotCooldownWarning": "Délai de récupération restant pour l'instantané",
"snapshotLimitDescription": "Vous ne pouvez gérer que 2 instantanés de base à la fois. Passez à un plan supérieur pour obtenir des instantanés supplémentaires.",
"snapshotLimitReached": "Limite de nombre d'instantanés atteinte",
"confirmRestore": "Confirmer la restauration",
@ -1067,6 +1067,7 @@
"viewHide": "Afficher la visibilité "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Affecter la vue",
"webhookDetails": "Détails du Webhook",
"showSaturdaysAndSundays": "Afficher les samedis et dimanches",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "הקצאת תצוגה",
"webhookDetails": "פרטי Webhook",
"showSaturdaysAndSundays": "הצג שבתות וימי ראשון",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "वबहक विवरण",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Detalji o webhooku",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Webhook Részletek",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Detail Webhook",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Dettagli webhook",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Webhook Details",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "ពនលមតរបស Webhook",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "ವಿವರಣಗಳ",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "뷰 보임 여부 "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "뷰 할당",
"webhookDetails": "웹훅 상세",
"showSaturdaysAndSundays": "토요일 & 일요일 보기",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Webhook detaļas",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "വിശദശങങൾ",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "वबहक विवरणहर",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -297,7 +297,7 @@
"languages": "Talen",
"extension": "Extensie",
"extensions": "Extensies",
"enable": "Enable",
"enable": "Inschakelen",
"poweredBy": "Powered by",
"nocoAI": "Noco AI",
"you": "You"
@ -636,20 +636,20 @@
"noConditionsAdded": "Geen voorwaarden toegevoegd",
"noAiIntegrationAvailable": "No AI Integrations available.",
"nocoAiBaseBuilder": "Noco AI Base Builder",
"additionalDetails": "Additional Details",
"aiIntegrationMissing": "AI Integration missing",
"noAiIntegrationsHaveBeenAdded": "No AI Integrations have been added.",
"additionalDetails": "Aanvullende details",
"aiIntegrationMissing": "AI-integratie ontbreekt",
"noAiIntegrationsHaveBeenAdded": "Er zijn geen AI-integraties toegevoegd.",
"generatingBaseTailoredToYourRequirement": "Generating a Base tailored to your requirement...",
"hereYourCrmBase": "Here’s your CRM Base",
"lockThisView": "Lock this view",
"lockThisView": "Vergrendel deze weergave",
"lockThisViewSubtle": "Locking this view will prevent anyone from changing the view settings until it is unlocked by a collaborator with creator permissions.",
"unlockViewTitle": "Are you sure you want to unlock this view?",
"unlockViewTitleSubtitle": "This view was locked by",
"thisViewIsLockType": "This view is {type}",
"thisFormIsLocked": "This Form is currently locked",
"unlockThisVieToMakeChanges": "Unlock this view to make changes",
"unlockViewTitleSubtitle": "Deze weergave is vergrendeld door",
"thisViewIsLockType": "Deze weergave is {type}",
"thisFormIsLocked": "Dit formulier is momenteel vergrendeld",
"unlockThisVieToMakeChanges": "Ontgrendel deze weergave om wijzigingen aan te brengen",
"yourBaseRole": "Your base role",
"lockedByUser": "Locked by {user}"
"lockedByUser": "Geblokkeerd door {user}"
},
"labels": {
"snapshotCreationFailed": "Snapshot creation failed",
@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Weergave toewijzen",
"webhookDetails": "Webhookdetails",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",
@ -1085,7 +1086,7 @@
"goToToday": "Ga naar Vandaag",
"toggleSidebar": "Toggle Zijbalk",
"addEndDate": "Voeg einddatum toe",
"endDate": "End Date",
"endDate": "Einddatum",
"withEndDate": "with end date",
"calendar": "Kalender",
"viewSettings": "Bekijk instellingen",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Webhook Detaljer",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Szczegóły Webhooka",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Detalhes do Webhook",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Detalhes do Webhook",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Detalii Webhook",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Детали вебхука",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Detaily Webhooku",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Podrobnosti Webhook",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Webhook Detaljer",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "รายละเอยด Webhook",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Webhook Detayları",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Деталі вебхука",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

@ -1067,6 +1067,7 @@
"viewHide": "View visibility "
},
"activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Assign view",
"webhookDetails": "Chi tiết Webhook",
"showSaturdaysAndSundays": "Show Saturdays & Sundays",

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

Loading…
Cancel
Save