Browse Source

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

pull/9992/head
Raju Udava 3 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. 21
      packages/nc-gui/components/cell/RichText.vue
  9. 4
      packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue
  10. 6
      packages/nc-gui/components/cell/SingleSelect.vue
  11. 122
      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. 14
      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. 61
      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. 343
      packages/nc-gui/components/smartsheet/column/LongTextOptions.vue
  33. 10
      packages/nc-gui/components/smartsheet/column/UITypesOptionsWithSearch.vue
  34. 34
      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. 9
      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. 10
      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. 34
      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. 57
      packages/nc-gui/composables/useIntegrationsStore.ts
  59. 9
      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. 16
      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 envsubst < .github/uffizzi/docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact - name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: preview-spec name: preview-spec-docker-compose
path: docker-compose.rendered.yml path: docker-compose.rendered.yml
retention-days: 2 retention-days: 2
- name: Serialize PR Event to File - name: Serialize PR Event to File
@ -118,9 +118,9 @@ jobs:
${{ toJSON(github.event) }} ${{ toJSON(github.event) }}
EOF EOF
- name: Upload PR Event as Artifact - name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: preview-spec name: preview-spec-event
path: event.json path: event.json
retention-days: 2 retention-days: 2
@ -174,8 +174,9 @@ jobs:
EOF EOF
- name: Upload PR Event as Artifact - name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: preview-spec name: preview-spec-event
path: event.json path: event.json
retention-days: 2 retention-days: 2
overwrite: true

29
.github/workflows/uffizzi-preview.yml

@ -20,31 +20,14 @@ jobs:
steps: steps:
- name: 'Download artifacts' - name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow. # Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6 uses: actions/download-artifact@v4
with: with:
script: | path: preview-spec
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ pattern: preview-spec-*
owner: context.repo.owner, merge-multiple: true
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));
- name: 'Accept event from first stage' - name: 'Accept event from first stage'
run: unzip preview-spec.zip event.json run: cp preview-spec/event.json .
- name: Read Event into ENV - name: Read Event into ENV
id: event 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 the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ steps.event.outputs.ACTION != 'closed' }} if: ${{ steps.event.outputs.ACTION != 'closed' }}
run: | 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 echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_OUTPUT
- name: Cache Rendered Compose File - name: Cache Rendered Compose File

2
build-local-docker-image.sh

@ -45,7 +45,7 @@ function copy_gui_artifacts() {
function package_nocodb() { function package_nocodb() {
# build nocodb ( pack nocodb-sdk and nc-gui ) # build nocodb ( pack nocodb-sdk and nc-gui )
cd ${SCRIPT_DIR}/packages/nocodb 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() { function build_image() {

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

@ -20,6 +20,14 @@
font-synthesis: none; font-synthesis: none;
} }
i {
font-synthesis: initial !important;
& * {
font-synthesis: initial !important;
}
}
html { html {
overflow: hidden; 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 StarterKit from '@tiptap/starter-kit'
import Mention from '@tiptap/extension-mention' import Mention from '@tiptap/extension-mention'
import { EditorContent, useEditor } from '@tiptap/vue-3' import { EditorContent, useEditor } from '@tiptap/vue-3'
import tippy from 'tippy.js'
import { type ColumnType, UITypes } from 'nocodb-sdk' import { type ColumnType, UITypes } from 'nocodb-sdk'
import FieldList from '~/helpers/tiptapExtensions/mention/FieldList' import FieldList from '~/helpers/tiptapExtensions/mention/FieldList'
import suggestion from '~/helpers/tiptapExtensions/mention/suggestion.ts' import suggestion from '~/helpers/tiptapExtensions/mention/suggestion.ts'
@ -15,6 +16,7 @@ const props = withDefaults(
promptFieldTagClassName?: string promptFieldTagClassName?: string
suggestionIconClassName?: string suggestionIconClassName?: string
placeholder?: string placeholder?: string
readOnly?: boolean
}>(), }>(),
{ {
options: () => [], options: () => [],
@ -26,6 +28,7 @@ const props = withDefaults(
* @example: :placeholder="`Enter prompt here...\n\neg : Categorise this {Notes}`" * @example: :placeholder="`Enter prompt here...\n\neg : Categorise this {Notes}`"
*/ */
placeholder: 'Write your prompt here...', 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({ const editor = useEditor({
content: vModel.value, content: vModel.value,
@ -55,18 +60,25 @@ const editor = useEditor({
...suggestion(FieldList), ...suggestion(FieldList),
items: ({ query }) => { items: ({ query }) => {
if (query.length === 0) return props.options ?? [] 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: '{', char: '{',
allowSpaces: true, allowSpaces: true,
}, },
renderHTML: ({ node }) => { renderHTML: ({ node }) => {
const matchedOption = props.options?.find((option) => option.title === node.attrs.id)
const isAttachment = matchedOption?.uidt === UITypes.Attachment
return [ return [
'span', 'span',
{ {
class: `prompt-field-tag ${ 'class': `prompt-field-tag ${isAttachment ? '!bg-green-200' : ''} ${props.promptFieldTagClassName}`,
props.options?.find((option) => option.title === node.attrs.id)?.uidt === UITypes.Attachment ? '!bg-green-200' : '' 'style': 'max-width: 100px; white-space: nowrap; overflow: hidden; display: inline-block; text-overflow: ellipsis;', // Enforces truncation
} ${props.promptFieldTagClassName}`, 'data-tooltip': node.attrs.id, // Tooltip content
}, },
`${node.attrs.id}`, `${node.attrs.id}`,
] ]
@ -93,8 +105,10 @@ const editor = useEditor({
text = text.trim() text = text.trim()
vModel.value = text vModel.value = text
debouncedLoadMentionFieldTagTooltip()
}, },
editable: true, editable: !readOnly.value,
autofocus: autoFocus.value, autofocus: autoFocus.value,
editorProps: { scrollThreshold: 100 }, editorProps: { scrollThreshold: 100 },
}) })
@ -141,15 +155,60 @@ onMounted(async () => {
}, 100) }, 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> </script>
<template> <template>
<div class="nc-ai-prompt-with-fields w-full"> <div class="nc-ai-prompt-with-fields w-full">
<EditorContent ref="editorDom" :editor="editor" @keydown.alt.enter.stop @keydown.shift.enter.stop /> <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"> <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> </slot>
</NcButton> </NcButton>
</div> </div>
@ -160,11 +219,11 @@ onMounted(async () => {
@apply relative; @apply relative;
.nc-prompt-with-field-suggestion-btn { .nc-prompt-with-field-suggestion-btn {
@apply absolute top-[1px] right-[1px]; @apply absolute top-[2px] right-[1px];
} }
.prompt-field-tag { .prompt-field-tag {
@apply bg-gray-100 rounded-md px-1; @apply bg-gray-100 rounded-md px-1 align-middle;
} }
.ProseMirror { .ProseMirror {
@ -172,6 +231,10 @@ onMounted(async () => {
resize: vertical; resize: vertical;
min-width: 100%; min-width: 100%;
max-height: min(800px, calc(100vh - 200px)) !important; max-height: min(800px, calc(100vh - 200px)) !important;
& > p {
@apply mr-3;
}
} }
.ProseMirror-focused { .ProseMirror-focused {

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

@ -7,9 +7,11 @@ const props = withDefaults(
workspaceId: string workspaceId: string
scope?: string scope?: string
showTooltip?: boolean showTooltip?: boolean
isEditColumn?: boolean
}>(), }>(),
{ {
showTooltip: true, showTooltip: true,
isEditColumn: false,
}, },
) )
@ -19,6 +21,8 @@ const vFkIntegrationId = useVModel(props, 'fkIntegrationId', emits)
const vModel = useVModel(props, 'model', emits) const vModel = useVModel(props, 'model', emits)
const { isEditColumn } = toRefs(props)
// const vRandomness = useVModel(props, 'randomness', emits) // const vRandomness = useVModel(props, 'randomness', emits)
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -29,7 +33,7 @@ const lastIntegrationId = ref<string | null>(null)
const isDropdownOpen = ref(false) const isDropdownOpen = ref(false)
const availableModels = ref<string[]>([]) const availableModels = ref<{ value: string; label: string }[]>([])
const isLoadingAvailableModels = ref<boolean>(false) const isLoadingAvailableModels = ref<boolean>(false)
@ -50,10 +54,10 @@ const onIntegrationChange = async (newFkINtegrationId?: string) => {
try { try {
const response = await $api.integrations.endpoint(newFkINtegrationId, 'availableModels', {}) 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) { if (!vModel.value && availableModels.value.length > 0) {
vModel.value = availableModels.value[0] vModel.value = availableModels.value[0].value
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -63,15 +67,19 @@ const onIntegrationChange = async (newFkINtegrationId?: string) => {
} }
onMounted(async () => { onMounted(async () => {
if (!vFkIntegrationId.value) { if (!vFkIntegrationId.value && !isEditColumn.value) {
if (aiIntegrations.value.length > 0 && aiIntegrations.value[0].id) { if (aiIntegrations.value.length > 0 && aiIntegrations.value[0].id) {
vFkIntegrationId.value = aiIntegrations.value[0].id vFkIntegrationId.value = aiIntegrations.value[0].id
nextTick(() => { nextTick(() => {
onIntegrationChange() onIntegrationChange()
}) })
} }
} else { } else if (vFkIntegrationId.value) {
lastIntegrationId.value = vFkIntegrationId.value lastIntegrationId.value = vFkIntegrationId.value
if (!vModel.value || !availableModels.value.length) {
onIntegrationChange()
}
} }
}) })
</script> </script>
@ -111,6 +119,7 @@ onMounted(async () => {
v-model:value="vFkIntegrationId" v-model:value="vFkIntegrationId"
class="w-full nc-select-shadow nc-ai-input" class="w-full nc-select-shadow nc-ai-input"
size="middle" size="middle"
placeholder="- select integration -"
@change="onIntegrationChange" @change="onIntegrationChange"
> >
<a-select-option v-for="integration in aiIntegrations" :key="integration.id" :value="integration.id"> <a-select-option v-for="integration in aiIntegrations" :key="integration.id" :value="integration.id">
@ -150,20 +159,21 @@ onMounted(async () => {
v-model:value="vModel" v-model:value="vModel"
class="w-full nc-select-shadow nc-ai-input" class="w-full nc-select-shadow nc-ai-input"
size="middle" size="middle"
placeholder="- select model -"
:disabled="!vFkIntegrationId || availableModels.length === 0" :disabled="!vFkIntegrationId || availableModels.length === 0"
:loading="isLoadingAvailableModels" :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"> <div class="w-full flex gap-2 items-center">
<NcTooltip class="flex-1 truncate" show-on-truncate-only> <NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title> <template #title>
{{ md }} {{ md.label }}
</template> </template>
{{ md }} {{ md.label }}
</NcTooltip> </NcTooltip>
<component <component
:is="iconMap.check" :is="iconMap.check"
v-if="vModel === md" v-if="vModel === md.value"
id="nc-selected-item-icon" id="nc-selected-item-icon"
class="text-nc-content-purple-medium w-4 h-4" class="text-nc-content-purple-medium w-4 h-4"
/> />
@ -198,4 +208,10 @@ onMounted(async () => {
</NcDropdown> </NcDropdown>
</template> </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 { row } = useSmartsheetRowStoreOrThrow()
const { isUIAllowed } = useRoles()
const isPublic = inject(IsPublicInj, ref(false))
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const column = inject(ColumnInj) as Ref< const column = inject(ColumnInj) as Ref<
@ -40,9 +44,7 @@ const isAiEdited = ref(false)
const isFieldAiIntegrationAvailable = computed(() => { const isFieldAiIntegrationAvailable = computed(() => {
const fkIntegrationId = column.value?.colOptions?.fk_integration_id const fkIntegrationId = column.value?.colOptions?.fk_integration_id
if (!fkIntegrationId) return false return !!fkIntegrationId
return ncIsArrayIncludes(aiIntegrations.value, fkIntegrationId, 'id')
}) })
const pk = computed(() => { 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 ncIsString(column.value.colOptions?.output_column_ids) && column.value.colOptions.output_column_ids.split(',').length > 1
? column.value.colOptions.output_column_ids.split(',') ? 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) generatingRows.value.push(pk.value)
generatingColumnRows.value.push(column.value.id) generatingColumnRows.value.push(column.value.id)
@ -76,11 +78,18 @@ const generate = async () => {
} }
} else { } else {
const obj: AIRecordType = resRow[column.value.title!] const obj: AIRecordType = resRow[column.value.title!]
if (obj && typeof obj === 'object') { if (obj && typeof obj === 'object') {
vModel.value = obj vModel.value = obj
setTimeout(() => { setTimeout(() => {
isAiEdited.value = false isAiEdited.value = false
}, 100) }, 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 = () => { const handleSave = () => {
vModel.value = { ...vModel.value }
emits('save') emits('save')
} }
const debouncedSave = useDebounceFn(handleSave, 1000) const debouncedSave = useDebounceFn(handleSave, 1000)
const isDisabledAiButton = computed(() => {
return !isFieldAiIntegrationAvailable.value || isLoading.value || isPublic.value || !isUIAllowed('dataEdit')
})
</script> </script>
<template> <template>
@ -113,20 +128,15 @@ const debouncedSave = useDebounceFn(handleSave, 1000)
'justify-center': isGrid && !isExpandedForm, 'justify-center': isGrid && !isExpandedForm,
}" }"
> >
<NcTooltip :disabled="isFieldAiIntegrationAvailable" class="flex"> <NcTooltip :disabled="isFieldAiIntegrationAvailable || isPublic || isUIAllowed('dataEdit')" class="flex">
<template #title> <template #title>
{{ aiIntegrations.length ? $t('tooltip.aiIntegrationReConfigure') : $t('tooltip.aiIntegrationAddAndReConfigure') }} {{ aiIntegrations.length ? $t('tooltip.aiIntegrationReConfigure') : $t('tooltip.aiIntegrationAddAndReConfigure') }}
</template> </template>
<button <button class="nc-cell-ai-button nc-cell-button h-6" size="small" :disabled="isDisabledAiButton" @click.stop="generate">
class="nc-cell-ai-button nc-cell-button h-7"
size="small"
:disabled="!isFieldAiIntegrationAvailable || isLoading"
@click.stop="generate"
>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<GeneralLoader v-if="isLoading" size="regular" class="!text-nc-content-purple-dark" /> <GeneralLoader v-if="isLoading" size="regular" />
<GeneralIcon v-else icon="ncAutoAwesome" class="text-nc-content-purple-dark h-4 w-4" /> <GeneralIcon v-else icon="ncAutoAwesome" class="h-4 w-4" />
<span class="text-sm font-semibold">Generate</span> <span class="text-small leading-[18px] truncate font-medium">Generate</span>
</div> </div>
</button> </button>
</NcTooltip> </NcTooltip>
@ -147,10 +157,19 @@ const debouncedSave = useDebounceFn(handleSave, 1000)
<style scoped lang="scss"> <style scoped lang="scss">
.nc-cell-button { .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); 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] { &[disabled] {
@apply !bg-gray-100 opacity-50; @apply !bg-gray-100 opacity-50;
} }
@ -162,6 +181,10 @@ const debouncedSave = useDebounceFn(handleSave, 1000)
&:has(.nc-cell-ai-button) { &:has(.nc-cell-ai-button) {
@apply !border-none; @apply !border-none;
box-shadow: none !important; box-shadow: none !important;
&:focus-within:not(.nc-readonly-div-data-cell):not(.nc-system-field) {
box-shadow: none !important;
}
} }
} }
</style> </style>

21
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 { Link } from '~/helpers/dbTiptapExtensions/links'
import { Mention } from '~/helpers/tiptapExtensions/mention' import { Mention } from '~/helpers/tiptapExtensions/mention'
import suggestion from '~/helpers/tiptapExtensions/mention/suggestion' import suggestion from '~/helpers/tiptapExtensions/mention/suggestion'
import UserMentionList from '~/helpers/tiptapExtensions/mention/UserMentionList.vue'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -145,7 +146,7 @@ if (appInfo.value.ee) {
isSameUser: bUser?.id === user.value?.id, 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}` span.textContent = `@${processedContent}`
return span.outerHTML return span.outerHTML
} }
@ -224,7 +225,7 @@ const tiptapExtensions = [
? [ ? [
Mention.configure({ Mention.configure({
suggestion: { suggestion: {
...suggestion, ...suggestion(UserMentionList),
items: ({ query }) => items: ({ query }) =>
baseUsers.value baseUsers.value
.filter((user) => user.deleted !== true) .filter((user) => user.deleted !== true)
@ -424,7 +425,7 @@ onClickOutside(editorDom, (e) => {
'justify-end xs:hidden': !isForm, '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 <CellRichTextSelectedBubbleMenu
v-if="editor" v-if="editor"
:editor="editor" :editor="editor"
@ -706,9 +707,21 @@ onClickOutside(editorDom, (e) => {
height: fit-content; height: fit-content;
} }
.mention span { .mention {
@apply inline-block my-2px;
span {
display: none; display: none;
} }
}
em {
font-synthesis: initial !important;
& * {
font-synthesis: initial !important;
}
}
} }
.nc-form-field-bubble-menu-wrapper { .nc-form-field-bubble-menu-wrapper {
@apply absolute -bottom-9 left-1/2 z-50 rounded-lg; @apply absolute -bottom-9 left-1/2 z-50 rounded-lg;

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

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

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

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

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

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

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

@ -16,7 +16,6 @@ const { refreshCommandPalette } = useCommandPalette()
const viewsStore = useViewsStore() const viewsStore = useViewsStore()
const { loadViews, navigateToView } = viewsStore const { loadViews, navigateToView } = viewsStore
const { aiIntegrationAvailable } = useNocoAi()
const { isFeatureEnabled } = useBetaFeatureToggle() const { isFeatureEnabled } = useBetaFeatureToggle()
const table = inject(SidebarTableInj)! const table = inject(SidebarTableInj)!
@ -197,7 +196,7 @@ async function onOpenModal({
<GeneralIcon v-else class="plus" icon="plus" /> <GeneralIcon v-else class="plus" icon="plus" />
</div> </div>
</NcMenuItem> </NcMenuItem>
<template v-if="aiIntegrationAvailable && isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)"> <template v-if="isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)">
<NcDivider /> <NcDivider />
<NcMenuItem data-testid="sidebar-view-create-ai" @click="onOpenModal({ type: 'AI' })"> <NcMenuItem data-testid="sidebar-view-create-ai" @click="onOpenModal({ type: 'AI' })">
<div class="item"> <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 { base } = storeToRefs(useBase())
const { getBaseUrl, appInfo } = useGlobal() const { appInfo } = useGlobal()
const workspaceStore = useWorkspace()
const url = computed(() => { const url = computed(() => {
if (!sharedBase.value || !sharedBase.value.uuid) return '' if (!sharedBase.value || !sharedBase.value.uuid) return ''
// get base url for workspace return encodeURI(`${dashboardUrl.value}#/base/${sharedBase.value.uuid}`)
const baseUrl = getBaseUrl(workspaceStore.activeWorkspaceId)
let dashboardUrl1 = dashboardUrl.value
if (baseUrl) {
dashboardUrl1 = `${baseUrl}${appInfo.value?.dashboardPath}`
}
return encodeURI(`${dashboardUrl1}#/base/${sharedBase.value.uuid}`)
}) })
const loadBase = async () => { 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 { view: _view, $api } = useSmartsheetStoreOrThrow()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { getBaseUrl, appInfo } = useGlobal() const { appInfo } = useGlobal()
const { dashboardUrl } = useDashboard() const { dashboardUrl } = useDashboard()
@ -203,15 +203,7 @@ function sharedViewUrl() {
viewType = 'view' viewType = 'view'
} }
// get base url for workspace return `${encodeURI(`${dashboardUrl.value}#/nc/${viewType}/${activeView.value.uuid}${surveyMode.value ? '/survey' : ''}`)}${
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' : ''}`)}${
viewStore.preFillFormSearchParams && formPreFill.value.preFillEnabled ? `?${viewStore.preFillFormSearchParams}` : '' viewStore.preFillFormSearchParams && formPreFill.value.preFillEnabled ? `?${viewStore.preFillFormSearchParams}` : ''
}` }`
} }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -32,10 +32,13 @@ const {
formattedData, formattedData,
disableSubmitBtn, disableSubmitBtn,
tableExplorerColumns, tableExplorerColumns,
fromTableExplorer,
} = useColumnCreateStoreOrThrow() } = useColumnCreateStoreOrThrow()
const { aiIntegrationAvailable, aiLoading, aiError, generateRows } = useNocoAi() const { aiIntegrationAvailable, aiLoading, aiError, generateRows } = useNocoAi()
const { isFeatureEnabled } = useBetaFeatureToggle()
const isOpenConfigModal = ref<boolean>(false) const isOpenConfigModal = ref<boolean>(false)
const isOpenSelectOutputFieldDropdown = 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 previewPanelDom = ref<HTMLElement>()
const isPreviewPanelOnScrollTop = ref(false) const isPreviewPanelOnScrollTop = ref(false)
@ -294,7 +305,7 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<div class="relative flex flex-col gap-4"> <div v-if="isAiButtonEnabled" class="relative flex flex-col gap-4">
<AiIntegrationNotFound v-if="!aiIntegrationAvailable" /> <AiIntegrationNotFound v-if="!aiIntegrationAvailable" />
<template v-else-if="!!aiError"> </template> <template v-else-if="!!aiError"> </template>
<template v-else> <template v-else>
@ -332,6 +343,7 @@ onBeforeUnmount(() => {
</NcButton> </NcButton>
<NcButton <NcButton
v-if="!fromTableExplorer"
size="small" size="small"
type="primary" type="primary"
:disabled="disableSubmitBtn || saving" :disabled="disableSubmitBtn || saving"
@ -366,6 +378,7 @@ onBeforeUnmount(() => {
v-model:randomness="vModel.randomness" v-model:randomness="vModel.randomness"
:workspace-id="activeWorkspaceId" :workspace-id="activeWorkspaceId"
:show-tooltip="false" :show-tooltip="false"
:is-edit-column="isEdit"
placement="bottomRight" placement="bottomRight"
> >
<NcButton size="xs" theme="ai" class="!px-1" type="text"> <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"> <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"> <div class="flex flex-row items-center gap-1 py-[2px] text-sm">
<component :is="cellIcon(op)" class="!mx-0 !mr-1 opacity-80" /> <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"> <div class="flex items-center p-0.5 mt-0.5">
<GeneralIcon <GeneralIcon
icon="close" icon="close"
@ -599,7 +616,7 @@ onBeforeUnmount(() => {
show-search-always show-search-always
search-input-placeholder="Search records" search-input-placeholder="Search records"
:item-height="60" :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" container-class-name="!px-0 !pb-0"
item-class-name="!rounded-none !p-0 !bg-none !hover:bg-none" item-class-name="!rounded-none !p-0 !bg-none !hover:bg-none"
@update:value="handleResetOutput" @update:value="handleResetOutput"
@ -679,18 +696,19 @@ onBeforeUnmount(() => {
<div <div
class="flex justify-center nc-ai-button-test-generate-wrapper" class="flex justify-center nc-ai-button-test-generate-wrapper"
:class="{ :class="{
'text-nc-border-gray-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 && vModel.formula_raw), '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> <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)"> <NcTooltip :disabled="!!(selectedRecordPk && outputColumnIds.length && inputColumns.length)">
<template #title> <template #title>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2 py-1 px-0.5">
<div>Preview checklist</div> <div class="text-[10px] leading-[14px] text-gray-300 uppercase mb-1">Preview checklist</div>
<div class="flex gap-2"> <div class="flex gap-2">
<div <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=" :class="
inputColumns.length inputColumns.length
? 'bg-nc-bg-green-dark text-nc-content-green-dark' ? 'bg-nc-bg-green-dark text-nc-content-green-dark'
@ -703,7 +721,7 @@ onBeforeUnmount(() => {
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<div <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=" :class="
outputColumnIds.length outputColumnIds.length
? 'bg-nc-bg-green-dark text-nc-content-green-dark' ? 'bg-nc-bg-green-dark text-nc-content-green-dark'
@ -716,7 +734,7 @@ onBeforeUnmount(() => {
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<div <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=" :class="
selectedRecordPk selectedRecordPk
? 'bg-nc-bg-green-dark text-nc-content-green-dark' ? 'bg-nc-bg-green-dark text-nc-content-green-dark'
@ -854,6 +872,7 @@ onBeforeUnmount(() => {
</div> </div>
</NcModal> </NcModal>
</div> </div>
<div v-else></div>
</template> </template>
<style lang="scss"> <style lang="scss">

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

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

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

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { type ColumnReqType, type ColumnType } from 'nocodb-sdk' import { type ColumnReqType, type ColumnType, isAIPromptCol } from 'nocodb-sdk'
import { import {
ButtonActionsType, ButtonActionsType,
UITypes, UITypes,
@ -183,9 +183,10 @@ const uiFilters = (t: UiTypesType) => {
const specificDBType = t.name === UITypes.SpecificDBType && isXcdbBase(meta.value?.source_id) const specificDBType = t.name === UITypes.SpecificDBType && isXcdbBase(meta.value?.source_id)
const showDeprecatedField = !t.deprecated || showDeprecated.value 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 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>>({}) const extraIcons = ref<Record<string, string>>({})
@ -238,7 +239,21 @@ const uiTypesOptions = computed<typeof uiTypes>(() => {
return types 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 let preload
if (fromSearchList && !isEdit.value && aiAutoSuggestMode.value) { if (fromSearchList && !isEdit.value && aiAutoSuggestMode.value) {
@ -250,10 +265,22 @@ const onSelectType = (uidt: UITypes | typeof AIButton, fromSearchList = false) =
preload = { preload = {
type: ButtonActionsType.Ai, type: ButtonActionsType.Ai,
} }
} else if (uidt === AIPrompt) {
formState.value.uidt = UITypes.LongText
preload = {
meta: {
[LongTextAiMetaProp]: true,
},
}
} else { } else {
formState.value.uidt = uidt formState.value.uidt = uidt
} }
onUidtOrIdTypeChange(preload) onUidtOrIdTypeChange(preload)
nextTick(() => {
handleScrollDebounce()
})
} }
const reloadMetaAndData = async () => { const reloadMetaAndData = async () => {
@ -393,6 +420,9 @@ onMounted(() => {
nextTick(() => { nextTick(() => {
mounted.value = true mounted.value = true
emit('mounted') emit('mounted')
handleScrollDebounce()
if (!isEdit.value) { if (!isEdit.value) {
if (!formState.value?.temp_id) { if (!formState.value?.temp_id) {
emit('add', formState.value) emit('add', formState.value)
@ -489,6 +519,9 @@ const triggerDescriptionEnable = () => {
descInputEl.value?.focus() descInputEl.value?.focus()
}, 100) }, 100)
} }
nextTick(() => {
handleScrollDebounce()
})
} }
const isFullUpdateAllowed = computed(() => { 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 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>() const aiPromptInputRef = ref<HTMLElement>()
watch(activeAiTab, (newValue) => { watch(activeAiTab, (newValue) => {
@ -597,6 +634,7 @@ watch(activeAiTab, (newValue) => {
<template> <template>
<div <div
v-if="!warningVisible" v-if="!warningVisible"
ref="editOrAddRef"
class="overflow-auto nc-scrollbar-md" class="overflow-auto nc-scrollbar-md"
:class="{ :class="{
'bg-white max-h-[max(80vh,500px)]': !props.fromTableExplorer, '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, 'min-w-[500px]': formState.uidt === UITypes.LinkToAnotherRecord || formState.uidt === UITypes.Links,
'!w-[600px]': 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), '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, 'nc-ai-mode': isAiMode,
'h-full': props.fromTableExplorer, 'h-full': props.fromTableExplorer,
'!bg-nc-bg-gray-extralight': aiAutoSuggestMode && formState.uidt && !props.fromTableExplorer, '!bg-nc-bg-gray-extralight': aiAutoSuggestMode && formState.uidt && !props.fromTableExplorer,
'!pb-0': !embedMode && !aiAutoSuggestMode && formState.uidt,
}" }"
@keydown="handleEscape" @keydown="handleEscape"
@click.stop @click.stop
@scroll="handleScrollDebounce"
> >
<a-form <a-form
v-model="formState" v-model="formState"
@ -927,7 +967,7 @@ watch(activeAiTab, (newValue) => {
type="primary" type="primary"
theme="ai" theme="ai"
:loading="saving" :loading="saving"
:disabled="disableSubmitBtn || !activeTabSelectedFields.length || saving" :disabled="disableSubmitBtn || saving"
size="small" size="small"
:label="submitBtnLabel.label" :label="submitBtnLabel.label"
:loading-label="submitBtnLabel.loadingLabel" :loading-label="submitBtnLabel.loadingLabel"
@ -1041,14 +1081,20 @@ watch(activeAiTab, (newValue) => {
v-bind="validateInfos.uidt" v-bind="validateInfos.uidt"
:class="{ :class="{
'ant-select-item-option-active-selected': showHoverEffectOnSelectedType && formState.uidt === opt.name, '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" @mouseover="handleResetHoverEffect"
> >
<div class="w-full flex gap-2 items-center justify-between" :data-testid="opt.name" :data-title="formState?.type"> <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)]"> <div class="flex-1 flex gap-2 items-center max-w-[calc(100%_-_24px)]">
<component <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="nc-field-type-icon w-4 h-4"
:class="isMetaReadOnly && !readonlyMetaAllowedTypes.includes(opt.name) ? 'text-gray-300' : 'text-gray-700'" :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" /> <SmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<SmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" /> <SmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" />
<SmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="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" /> <SmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<SmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" 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" /> <SmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
@ -1227,7 +1277,12 @@ watch(activeAiTab, (newValue) => {
</Transition> </Transition>
</template> </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"> <div class="flex gap-3 text-gray-800 h-7 mb-1 items-center justify-between">
<span class="text-[13px]"> <span class="text-[13px]">
{{ $t('labels.description') }} {{ $t('labels.description') }}
@ -1254,8 +1309,13 @@ watch(activeAiTab, (newValue) => {
</a-form-item> </a-form-item>
<template v-if="props.fromTableExplorer"> <template v-if="props.fromTableExplorer">
<a-form-item> <a-form-item
<NcButton v-if="!enableDescription" size="small" type="text" @click.stop="triggerDescriptionEnable"> 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"> <div class="flex !text-gray-700 items-center gap-2">
<GeneralIcon icon="plus" class="h-4 w-4" /> <GeneralIcon icon="plus" class="h-4 w-4" />
@ -1266,8 +1326,14 @@ watch(activeAiTab, (newValue) => {
</NcButton> </NcButton>
</a-form-item> </a-form-item>
</template> </template>
<template v-else> <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"> <NcButton v-if="!enableDescription" size="small" type="text" @click.stop="triggerDescriptionEnable">
<div class="flex !text-gray-700 items-center gap-2"> <div class="flex !text-gray-700 items-center gap-2">
<GeneralIcon icon="plus" class="h-4 w-4" /> <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> <template>
<div <div
v-if="suggestionPreviewed && !suggestionPreviewed.unsupported && suggestionPreviewed.type === 'function'" 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="{ :style="{
left: suggestionPreviewPostion.left, left: suggestionPreviewPostion.left,
top: suggestionPreviewPostion.top, top: suggestionPreviewPostion.top,

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

@ -118,6 +118,10 @@ const debouncedValidate = useDebounceFn(async () => {
dataType: FormulaDataTypes.UNKNOWN, dataType: FormulaDataTypes.UNKNOWN,
} }
} }
} finally {
if (vModel.value?.colOptions?.parsed_tree?.dataType !== parsedTree.value?.dataType) {
vModel.value.meta.display_type = null
}
} }
}, 300) }, 300)
@ -175,15 +179,6 @@ watch(
immediate: true, immediate: true,
}, },
) )
watch(parsedTree, (value, oldValue) => {
if (oldValue === undefined && value) {
return
}
if (value?.dataType !== oldValue?.dataType) {
vModel.value.meta.display_type = null
}
})
</script> </script>
<template> <template>
@ -209,9 +204,14 @@ watch(parsedTree, (value, oldValue) => {
<div>{{ $t('labels.formatting') }}</div> <div>{{ $t('labels.formatting') }}</div>
</div> </div>
</template> </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-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"> <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="flex w-full items-center gap-2 justify-between">
<div class="w-full"> <div class="w-full">
@ -226,7 +226,7 @@ watch(parsedTree, (value, oldValue) => {
/> />
</div> </div>
</a-select-option> </a-select-option>
</a-select> </NcSelect>
</a-form-item> </a-form-item>
<template <template

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

@ -1,13 +1,147 @@
<!-- File not in use for now -->
<script setup lang="ts"> <script setup lang="ts">
import { isAIPromptCol, UITypes } from 'nocodb-sdk'
const props = defineProps<{ 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],
)
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,
})
const vModel = useVModel(props, 'value', emit) provide(EditColumnInj, ref(true))
const richMode = computed({ const richMode = computed({
get: () => !!vModel.value.meta?.richMode, 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, () => { watch(richMode, () => {
vModel.value.cdf = null vModel.value.cdf = null
}) })
watch(isPreviewEnabled, handleDisableSubmitBtn, {
immediate: true,
})
</script> </script>
<template> <template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-4">
<a-form-item> <a-form-item>
<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"> <div class="flex items-center gap-1">
<NcSwitch v-model:checked="richMode"> <NcSwitch v-model:checked="richMode" :disabled="isEnabledGenerateText">
<div class="text-sm text-gray-800 select-none"> <div class="text-sm text-gray-800 select-none font-semibold">
{{ $t('labels.enableRichText') }} {{ $t('labels.enableRichText') }}
</div> </div>
</NcSwitch> </NcSwitch>
</div> </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> </a-form-item>
</div> </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>
</div>
</div>
</template>
<AiIntegrationNotFound v-if="!aiIntegrationAvailable && isEnabledGenerateText && isPromptEnabled" />
</div>
</template> </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 { isFeatureEnabled } = useBetaFeatureToggle()
const filteredOptions = computed( const filteredOptions = computed(
() => () => options.value?.filter((c) => searchCompare([c.name, UITypesName[c.name]], searchQuery.value)) ?? [],
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()))),
) ?? [],
) )
const inputRef = ref() const inputRef = ref()
@ -126,7 +120,7 @@ watch(
'hover:bg-gray-100 cursor-pointer': !isDisabledUIType(option.name), 'hover:bg-gray-100 cursor-pointer': !isDisabledUIType(option.name),
'bg-gray-100 nc-column-list-option-active': activeFieldIndex === index && !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-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" :data-testid="option.name"

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

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

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

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

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

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

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

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
ButtonActionsType,
type ButtonType, type ButtonType,
type ColumnReqType, type ColumnReqType,
type ColumnType, type ColumnType,
@ -8,6 +7,7 @@ import {
UITypes, UITypes,
type ViewType, type ViewType,
ViewTypes, ViewTypes,
isAIPromptCol,
isCreatedOrLastModifiedByCol, isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol, isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR, isLinksOrLTAR,
@ -665,15 +665,13 @@ const onActiveCellChanged = () => {
} }
} }
const isOpen = ref(false)
const isDeleteAllModalIsOpen = ref(false) const isDeleteAllModalIsOpen = ref(false)
async function deleteAllRecords() { async function deleteAllRecords() {
isDeleteAllModalIsOpen.value = true isDeleteAllModalIsOpen.value = true
function closeDlg() {
isOpen.value = false
close(200)
}
const { close } = useDialog(resolveComponent('DlgRecordDeleteAll'), { const { close } = useDialog(resolveComponent('DlgRecordDeleteAll'), {
'modelValue': isDeleteAllModalIsOpen, 'modelValue': isDeleteAllModalIsOpen,
'rows': totalRows.value, 'rows': totalRows.value,
@ -685,10 +683,14 @@ async function deleteAllRecords() {
}, },
}) })
function closeDlg() {
isOpen.value = false
close(200)
}
await until(isDeleteAllModalIsOpen).toBe(false) await until(isDeleteAllModalIsOpen).toBe(false)
} }
const isOpen = ref(false)
async function expandRows({ async function expandRows({
newRows, newRows,
newColumns, newColumns,
@ -1022,8 +1024,8 @@ const isSelectedOnlyAI = computed(() => {
if (selectedRange.start.col === selectedRange.end.col) { if (selectedRange.start.col === selectedRange.end.col) {
const field = fields.value[selectedRange.start.col] const field = fields.value[selectedRange.start.col]
return { return {
enabled: field.uidt === UITypes.Button && (field?.colOptions as ButtonType)?.type === ButtonActionsType.Ai, enabled: isAIPromptCol(field) || isAiButton(field),
disabled: !ncIsArrayIncludes(aiIntegrations.value, (field?.colOptions as ButtonType)?.fk_integration_id, 'id'), disabled: !(field?.colOptions as ButtonType)?.fk_integration_id,
} }
} }
@ -1046,9 +1048,7 @@ const generateAIBulk = async () => {
let outputColumnIds = [field.id] let outputColumnIds = [field.id]
const isAiButton = field.uidt === UITypes.Button && (field?.colOptions as ButtonType)?.type === ButtonActionsType.Ai if (isAiButton(field)) {
if (isAiButton) {
outputColumnIds = outputColumnIds =
ncIsString(field.colOptions?.output_column_ids) && field.colOptions.output_column_ids.split(',').length > 0 ncIsString(field.colOptions?.output_column_ids) && field.colOptions.output_column_ids.split(',').length > 0
? field.colOptions.output_column_ids.split(',') ? field.colOptions.output_column_ids.split(',')
@ -2143,13 +2143,10 @@ watch(vSelectedAllRecords, (selectedAll) => {
top: `${(index + 1) * rowHeight - 6}px`, top: `${(index + 1) * rowHeight - 6}px`,
zIndex: 100001, zIndex: 100001,
}" }"
class="absolute z-30 left-0" class="absolute z-30 left-0 w-full flex"
> >
<div <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" 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"
:style="{
transform: `translateX(${scrollLeft - leftOffset}px)`,
}"
> >
Row filtered Row filtered
@ -2168,13 +2165,10 @@ watch(vSelectedAllRecords, (selectedAll) => {
top: `${(index + 1) * rowHeight - 6}px`, top: `${(index + 1) * rowHeight - 6}px`,
zIndex: 100000, zIndex: 100000,
}" }"
class="absolute transform z-30 left-0" class="absolute transform z-30 left-0 w-full flex"
> >
<div <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" 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"
:style="{
transform: `translateX(${scrollLeft - leftOffset}px)`,
}"
> >
Row moved Row moved
@ -2252,13 +2246,13 @@ watch(vSelectedAllRecords, (selectedAll) => {
</span> </span>
<div <div
v-else-if="!row.rowMeta?.saving && !row.rowMeta?.isLoading" 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 <component
:is="iconMap.expand" :is="iconMap.maximize"
v-if="expandForm" v-if="expandForm"
v-e="['c:row-expand:open']" 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)" @click="expandAndLooseFocus(row, state)"
/> />
</div> </div>
@ -2874,7 +2868,7 @@ watch(vSelectedAllRecords, (selectedAll) => {
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
box-shadow: 0 0 0 2px #3366ff !important; box-shadow: 0 0 0 1.5px #3366ff !important;
border-radius: 2px; border-radius: 2px;
} }
@ -2928,6 +2922,35 @@ watch(vSelectedAllRecords, (selectedAll) => {
background: white; background: white;
@apply border-r-1 border-r-gray-100; @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 { .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 { nextTick } from '@vue/runtime-core'
import type { ButtonType, ColumnReqType, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import type { ButtonType, ColumnReqType, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { import {
ButtonActionsType,
UITypes, UITypes,
ViewTypes, ViewTypes,
isAIPromptCol,
isCreatedOrLastModifiedByCol, isCreatedOrLastModifiedByCol,
isCreatedOrLastModifiedTimeCol, isCreatedOrLastModifiedTimeCol,
isLinksOrLTAR, isLinksOrLTAR,
@ -852,8 +852,8 @@ const isSelectedOnlyAI = computed(() => {
if (selectedRange.start.col === selectedRange.end.col) { if (selectedRange.start.col === selectedRange.end.col) {
const field = fields.value[selectedRange.start.col] const field = fields.value[selectedRange.start.col]
return { return {
enabled: field.uidt === UITypes.Button && (field?.colOptions as ButtonType)?.type === ButtonActionsType.Ai, enabled: isAIPromptCol(field) || isAiButton(field),
disabled: !ncIsArrayIncludes(aiIntegrations.value, (field?.colOptions as ButtonType)?.fk_integration_id, 'id'), disabled: !(field?.colOptions as ButtonType)?.fk_integration_id,
} }
} }
@ -876,9 +876,7 @@ const generateAIBulk = async () => {
let outputColumnIds = [field.id] let outputColumnIds = [field.id]
const isAiButton = field.uidt === UITypes.Button && (field?.colOptions as ButtonType)?.type === ButtonActionsType.Ai if (isAiButton(field)) {
if (isAiButton) {
outputColumnIds = outputColumnIds =
ncIsString(field.colOptions?.output_column_ids) && field.colOptions.output_column_ids.split(',').length > 0 ncIsString(field.colOptions?.output_column_ids) && field.colOptions.output_column_ids.split(',').length > 0
? field.colOptions.output_column_ids.split(',') ? field.colOptions.output_column_ids.split(',')
@ -2139,13 +2137,13 @@ onKeyStroke('ArrowDown', onDown)
</span> </span>
<div <div
v-else-if="!row.rowMeta.saving" 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 <component
:is="iconMap.expand" :is="iconMap.maximize"
v-if="expandForm" v-if="expandForm"
v-e="['c:row-expand:open']" 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)" @click="expandAndLooseFocus(row, state)"
/> />
</div> </div>

9
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 columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null)
const columnTypeName = computed(() => { const columnTypeName = computed(() => {
if (column.value.uidt === UITypes.LongText && parseProp(column?.value?.meta)?.richMode) { if (column.value.uidt === UITypes.LongText) {
if (parseProp(column.value?.meta)?.richMode) {
return UITypesName.RichText return UITypesName.RichText
} }
if (parseProp(column.value?.meta)?.[LongTextAiMetaProp]) {
return UITypesName.AIPrompt
}
}
return column.value.uidt ? UITypesName[column.value.uidt] : '' 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 return iconMap.cellSingleSelect
} else if (isBoolean(column, abstractType)) { } else if (isBoolean(column, abstractType)) {
return iconMap.cellCheckbox return iconMap.cellCheckbox
} else if (isAI(column)) {
return iconMap.cellAi
} else if (isTextArea(column)) { } else if (isTextArea(column)) {
return iconMap.cellLongText return iconMap.cellLongText
} else if (isEmail(column)) { } else if (isEmail(column)) {
@ -51,8 +53,6 @@ const renderIcon = (column: ColumnType, abstractType: any) => {
return iconMap.cellUser return iconMap.cellUser
} }
return iconMap.cellUser return iconMap.cellUser
} else if (isAI(column)) {
return iconMap.cellAi
} else if (isInt(column, abstractType) || isFloat(column, abstractType)) { } else if (isInt(column, abstractType) || isFloat(column, abstractType)) {
return iconMap.cellNumber return iconMap.cellNumber
} else if (isString(column, abstractType)) { } 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 { isUIAllowed, isMetaReadOnly, isDataReadOnly } = useRoles()
const { aiIntegrations } = useNocoAi()
const isLoading = ref<'' | 'hideOrShow' | 'setDisplay'>('') 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 () => { const setAsDisplayValue = async () => {
isLoading.value = 'setDisplay' isLoading.value = 'setDisplay'
try { try {
@ -415,6 +428,10 @@ const onClickCopyFieldUrl = async (field: ColumnType) => {
await copy(field.id!) await copy(field.id!)
isFieldIdCopied.value = true isFieldIdCopied.value = true
await ncDelay(5000)
isFieldIdCopied.value = false
} }
const onDeleteColumn = () => { const onDeleteColumn = () => {
@ -431,7 +448,7 @@ const onDeleteColumn = () => {
overlay-class-name="nc-dropdown-column-operations !border-1 rounded-lg !shadow-xl " overlay-class-name="nc-dropdown-column-operations !border-1 rounded-lg !shadow-xl "
@click.stop="openDropdown" @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> <div v-if="isExpandedForm" class="h-[1px]">&nbsp;</div>
<NcTooltip v-if="column?.description?.length && !isExpandedForm" class="flex"> <NcTooltip v-if="column?.description?.length && !isExpandedForm" class="flex">
<template #title> <template #title>
@ -441,14 +458,10 @@ const onDeleteColumn = () => {
</NcTooltip> </NcTooltip>
<NcTooltip class="flex items-center"> <NcTooltip class="flex items-center">
<GeneralIcon <GeneralIcon v-if="columnInvalid.isInvalid && !isExpandedForm" class="text-red-300 w-3.5 h-3.5" icon="alertTriangle" />
v-if="isColumnInvalid(column) && !isExpandedForm"
class="text-orange-500 w-3.5 h-3.5 ml-2"
icon="alertTriangle"
/>
<template #title> <template #title>
{{ $t('msg.invalidColumnConfiguration') }} {{ $t(columnInvalid.tooltip) }}
</template> </template>
</NcTooltip> </NcTooltip>
<GeneralIcon <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 columnOrder = ref<Pick<ColumnReqType, 'column_order'> | null>(null)
const columnTypeName = computed(() => { 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) { if (column.value.uidt === UITypes.LinkToAnotherRecord && column.value.colOptions?.type === RelationTypes.ONE_TO_ONE) {
return UITypesName[UITypes.Links] return UITypesName[UITypes.Links]
} }
if (isAiButton(column.value)) {
return UITypesName.AIButton
}
return column.value.uidt ? UITypesName[column.value.uidt] : '' 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 () => { const onViewIdCopy = async () => {
await copy(view.value!.id!) await copy(view.value!.id!)
isViewIdCopied.value = true isViewIdCopied.value = true
await ncDelay(5000)
isViewIdCopied.value = false
} }
const onDelete = async () => { const onDelete = async () => {

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

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

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

@ -210,7 +210,7 @@ const componentProps = computed(() => {
<style scoped lang="scss"> <style scoped lang="scss">
.nc-cell-button { .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']) { &: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); 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"> <div v-if="!isUnderLookup && !isSystemColumn(column)" class="flex justify-end gap-1 min-h-4 items-center">
<GeneralIcon <GeneralIcon
icon="expand" icon="maximize"
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand" 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" @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"> <div v-if="!isUnderLookup || isForm" class="flex justify-end gap-1 min-h-4 items-center">
<GeneralIcon <GeneralIcon
icon="expand" icon="maximize"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand" class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand h-3 w-3"
@click.stop="openChildList" @click.stop="openChildList"
/> />

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

@ -1,8 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { UITypes, isVirtualCol, parseStringDateTime } from 'nocodb-sdk' import { UITypes, isVirtualCol, parseStringDateTime } from 'nocodb-sdk'
import MaximizeIcon from '~icons/nc-icons/maximize'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
row: any 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)" 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)" @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> </button>
</NcTooltip> </NcTooltip>
</div> </div>

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,6 +1,6 @@
import rfdc from 'rfdc' import rfdc from 'rfdc'
import type { ColumnReqType, ColumnType, TableType } from 'nocodb-sdk' 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 { Ref } from 'vue'
import type { RuleObject } from 'ant-design-vue/es/form' import type { RuleObject } from 'ant-design-vue/es/form'
import { generateUniqueColumnName } from '~/helpers/parsers/parserHelpers' import { generateUniqueColumnName } from '~/helpers/parsers/parserHelpers'
@ -110,6 +110,10 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
return true return true
} }
if (isAIPromptCol(formState.value)) {
return true
}
return false 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 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 { ComputedRef, Ref } from 'vue'
import type { CellRange } from '#imports' import type { CellRange } from '#imports'
@ -286,6 +286,7 @@ export function useData(args: {
col.uidt === UITypes.Lookup || col.uidt === UITypes.Lookup ||
col.uidt === UITypes.Button || col.uidt === UITypes.Button ||
col.uidt === UITypes.Attachment || col.uidt === UITypes.Attachment ||
isAIPromptCol(col) ||
col.au || col.au ||
(isValidValue(col?.cdf) && / on update /i.test(col.cdf))) (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 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 { Api, ColumnType, LinkToAnotherRecordType, PaginatedType, RelationTypes, TableType, ViewType } from 'nocodb-sdk'
import type { Row } from '../lib/types' import type { Row } from '../lib/types'
import { validateRowFilters } from '../utils/dataUtils' import { validateRowFilters } from '../utils/dataUtils'
@ -916,6 +916,7 @@ export function useInfiniteData(args: {
col.title && col.title &&
col.title in updatedRowData && col.title in updatedRowData &&
(columnsToUpdate.has(col.uidt as UITypes) || (columnsToUpdate.has(col.uidt as UITypes) ||
isAIPromptCol(col) ||
col.au || col.au ||
(isValidValue(col?.cdf) && / on update /i.test(col.cdf as string))) (isValidValue(col?.cdf) && / on update /i.test(col.cdf as string)))
) { ) {

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

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

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

@ -3,6 +3,7 @@ import { computed } from 'vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef } from '@vueuse/core'
import type { import type {
AIRecordType,
AttachmentType, AttachmentType,
ColumnType, ColumnType,
LinkToAnotherRecordType, LinkToAnotherRecordType,
@ -288,8 +289,16 @@ export function useMultiSelect(
} }
if (columnObj.uidt === UITypes.LongText) { if (columnObj.uidt === UITypes.LongText) {
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, '\\"')}"` textToCopy = `"${textToCopy.replace(/"/g, '\\"')}"`
} }
}
return textToCopy 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' const aiIntegrationNotFound = 'AI integration not found'
@ -13,15 +13,19 @@ export const useNocoAi = createSharedComposable(() => {
const { activeProjectId } = storeToRefs(basesStore) const { activeProjectId } = storeToRefs(basesStore)
const aiIntegrationAvailable = ref(true)
const aiLoading = ref(false) const aiLoading = ref(false)
const aiError = ref<string>('') const aiError = ref<string>('')
const aiIntegrations = ref<IntegrationType[]>([]) const aiIntegrations = ref<Partial<IntegrationType>[]>([])
const aiIntegrationAvailable = computed(() => !!aiIntegrations.value.length)
const { listIntegrationByType } = useProvideIntegrationViewStore() const isAiIntegrationAvailableInList = (integrationId?: string) => {
if (!aiIntegrationAvailable.value) return false
return ncIsArrayIncludes(aiIntegrations.value, integrationId, 'id')
}
const callAiUtilsApi = async (operation: string, input: any, customBaseId?: string, skipMsgToast = false) => { const callAiUtilsApi = async (operation: string, input: any, customBaseId?: string, skipMsgToast = false) => {
try { 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 { return {
aiIntegrationAvailable, aiIntegrationAvailable,
isAiIntegrationAvailableInList,
aiLoading, aiLoading,
aiError, aiError,
predictFieldType, predictFieldType,
@ -381,6 +364,5 @@ export const useNocoAi = createSharedComposable(() => {
repairFormula, repairFormula,
predictViews, predictViews,
aiIntegrations, aiIntegrations,
loadAiIntegrations,
} }
}) })

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

@ -2,6 +2,7 @@ import type { Ref } from 'vue'
import { import {
type ColumnType, type ColumnType,
CommonAggregations, CommonAggregations,
type FormulaType,
type TableType, type TableType,
UITypes, UITypes,
type ViewType, type ViewType,
@ -123,6 +124,32 @@ const [useProvideViewAggregate, useViewAggregate] = useInjectionState(
await updateGridViewColumn(fieldId, { aggregation: agg }) 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) => { reloadAggregate?.on(async (_fields) => {
if (!_fields || !_fields?.fields.length) { if (!_fields || !_fields?.fields.length) {
await loadViewAggregate() await loadViewAggregate()
@ -135,6 +162,12 @@ const [useProvideViewAggregate, useViewAggregate] = useInjectionState(
acc[f.id] = field.aggregation ?? gridViewCols.value[f.id].aggregation ?? CommonAggregations.None 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 return acc
}, {} as Record<string, string>) }, {} 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 isURL from 'validator/lib/isURL'
import { pluralize } from 'inflection' import { pluralize } from 'inflection'
@ -478,8 +478,16 @@ export const generateUniqueColumnName = ({
case UITypes.Button: { case UITypes.Button: {
if (formState?.type === ButtonActionsType.Ai) { if (formState?.type === ButtonActionsType.Ai) {
defaultColumnName = `AI ${defaultColumnName}` defaultColumnName = UITypesName.AIButton
} }
break
}
case UITypes.LongText: {
if (formState?.meta?.[LongTextAiMetaProp] === true) {
defaultColumnName = UITypesName.AIPrompt
}
break
} }
} }

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

@ -56,6 +56,12 @@ export default {
return true return true
} }
if (event.key === '}') {
setTimeout(() => {
this.selectItem(this.selectedIndex)
}, 250)
}
return false return false
}, },
@ -80,6 +86,7 @@ export default {
selectItem(index, _e) { selectItem(index, _e) {
const item = this.items[index] const item = this.items[index]
if (item) { if (item) {
this.command({ this.command({
id: item.title, id: item.title,
@ -92,7 +99,7 @@ export default {
<template> <template>
<div <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 @mousedown.stop
> >
<template v-if="items.length"> <template v-if="items.length">
@ -100,11 +107,16 @@ export default {
v-for="(item, index) in items" v-for="(item, index) in items"
:key="index" :key="index"
:class="{ 'is-selected': index === selectedIndex }" :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)" @click="selectItem(index, $event)"
> >
<component :is="getUIDTIcon(item.uidt)" v-if="item?.uidt" class="flex-none w-3.5 h-3.5" /> <component :is="getUIDTIcon(item.uidt)" v-if="item?.uidt" class="flex-none w-3.5 h-3.5" />
<NcTooltip class="truncate" show-on-truncate-only :tooltip-style="{ zIndex: '10000' }">
<template #title>
{{ item?.title || '' }}
</template>
{{ item?.title || '' }} {{ item?.title || '' }}
</NcTooltip>
</div> </div>
</template> </template>
<div v-else class="px-4">No field available</div> <div v-else class="px-4">No field available</div>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -655,7 +655,7 @@
"snapshotCreationFailed": "La création de l'instantané a échoué", "snapshotCreationFailed": "La création de l'instantané a échoué",
"snapshotCreationFailedDescription": "Impossible de créer votre instantané de base. Réessayez plus tard.", "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.", "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.", "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", "snapshotLimitReached": "Limite de nombre d'instantanés atteinte",
"confirmRestore": "Confirmer la restauration", "confirmRestore": "Confirmer la restauration",
@ -1067,6 +1067,7 @@
"viewHide": "Afficher la visibilité " "viewHide": "Afficher la visibilité "
}, },
"activity": { "activity": {
"deleteAllRecords": "Delete all records",
"assignView": "Affecter la vue", "assignView": "Affecter la vue",
"webhookDetails": "Détails du Webhook", "webhookDetails": "Détails du Webhook",
"showSaturdaysAndSundays": "Afficher les samedis et dimanches", "showSaturdaysAndSundays": "Afficher les samedis et dimanches",

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save