diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml
index 973574ce5d..59702bc50d 100644
--- a/.github/workflows/release-pr.yml
+++ b/.github/workflows/release-pr.yml
@@ -107,9 +107,9 @@ jobs:
envsubst < .github/uffizzi/docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: preview-spec
+ name: preview-spec-docker-compose
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
@@ -118,9 +118,9 @@ jobs:
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: preview-spec
+ name: preview-spec-event
path: event.json
retention-days: 2
@@ -174,8 +174,9 @@ jobs:
EOF
- name: Upload PR Event as Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: preview-spec
+ name: preview-spec-event
path: event.json
retention-days: 2
+ overwrite: true
diff --git a/.github/workflows/uffizzi-preview.yml b/.github/workflows/uffizzi-preview.yml
index 437d42cc73..ce5c73a4e1 100644
--- a/.github/workflows/uffizzi-preview.yml
+++ b/.github/workflows/uffizzi-preview.yml
@@ -20,31 +20,14 @@ jobs:
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
- uses: actions/github-script@v6
+ uses: actions/download-artifact@v4
with:
- script: |
- let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
- owner: context.repo.owner,
- repo: context.repo.repo,
- run_id: context.payload.workflow_run.id,
- });
- let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
- return artifact.name == "preview-spec"
- })[0];
- if (matchArtifact === undefined) {
- throw TypeError('Build Artifact not found!');
- }
- let download = await github.rest.actions.downloadArtifact({
- owner: context.repo.owner,
- repo: context.repo.repo,
- artifact_id: matchArtifact.id,
- archive_format: 'zip',
- });
- let fs = require('fs');
- fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
+ path: preview-spec
+ pattern: preview-spec-*
+ merge-multiple: true
- name: 'Accept event from first stage'
- run: unzip preview-spec.zip event.json
+ run: cp preview-spec/event.json .
- name: Read Event into ENV
id: event
@@ -58,7 +41,7 @@ jobs:
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ steps.event.outputs.ACTION != 'closed' }}
run: |
- unzip preview-spec.zip docker-compose.rendered.yml
+ cp preview-spec/docker-compose.rendered.yml .
echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_OUTPUT
- name: Cache Rendered Compose File
diff --git a/build-local-docker-image.sh b/build-local-docker-image.sh
index c6fd2878b8..72d598fd64 100755
--- a/build-local-docker-image.sh
+++ b/build-local-docker-image.sh
@@ -45,7 +45,7 @@ function copy_gui_artifacts() {
function package_nocodb() {
# build nocodb ( pack nocodb-sdk and nc-gui )
cd ${SCRIPT_DIR}/packages/nocodb
- EE=true ${SCRIPT_DIR}/node_modules/@rspack/cli/bin --config ${SCRIPT_DIR}/packages/nocodb/rspack.config.js || ERROR="package_nocodb failed"
+ EE=true ${SCRIPT_DIR}/node_modules/@rspack/cli/bin/rspack.js --config ${SCRIPT_DIR}/packages/nocodb/rspack.config.js || ERROR="package_nocodb failed"
}
function build_image() {
diff --git a/packages/nc-gui/assets/style.scss b/packages/nc-gui/assets/style.scss
index 335b6199ba..f5b5913d06 100644
--- a/packages/nc-gui/assets/style.scss
+++ b/packages/nc-gui/assets/style.scss
@@ -20,6 +20,14 @@
font-synthesis: none;
}
+i {
+ font-synthesis: initial !important;
+
+ & * {
+ font-synthesis: initial !important;
+ }
+}
+
html {
overflow: hidden;
}
diff --git a/packages/nc-gui/components/ai/PromptWithFields.vue b/packages/nc-gui/components/ai/PromptWithFields.vue
index 3b56693836..219f6cd597 100644
--- a/packages/nc-gui/components/ai/PromptWithFields.vue
+++ b/packages/nc-gui/components/ai/PromptWithFields.vue
@@ -3,6 +3,7 @@ import Placeholder from '@tiptap/extension-placeholder'
import StarterKit from '@tiptap/starter-kit'
import Mention from '@tiptap/extension-mention'
import { EditorContent, useEditor } from '@tiptap/vue-3'
+import tippy from 'tippy.js'
import { type ColumnType, UITypes } from 'nocodb-sdk'
import FieldList from '~/helpers/tiptapExtensions/mention/FieldList'
import suggestion from '~/helpers/tiptapExtensions/mention/suggestion.ts'
@@ -15,6 +16,7 @@ const props = withDefaults(
promptFieldTagClassName?: string
suggestionIconClassName?: string
placeholder?: string
+ readOnly?: boolean
}>(),
{
options: () => [],
@@ -26,6 +28,7 @@ const props = withDefaults(
* @example: :placeholder="`Enter prompt here...\n\neg : Categorise this {Notes}`"
*/
placeholder: 'Write your prompt here...',
+ readOnly: false,
},
)
@@ -38,7 +41,9 @@ const vModel = computed({
},
})
-const { autoFocus } = toRefs(props)
+const { autoFocus, readOnly } = toRefs(props)
+
+const debouncedLoadMentionFieldTagTooltip = useDebounceFn(loadMentionFieldTagTooltip, 1000)
const editor = useEditor({
content: vModel.value,
@@ -55,18 +60,25 @@ const editor = useEditor({
...suggestion(FieldList),
items: ({ query }) => {
if (query.length === 0) return props.options ?? []
- return props.options?.filter((o) => o.title?.toLowerCase()?.includes(query.toLowerCase())) ?? []
+ return (
+ props.options?.filter(
+ (o) =>
+ o.title?.toLowerCase()?.includes(query.toLowerCase()) || `${o.title?.toLowerCase()}}` === query.toLowerCase(),
+ ) ?? []
+ )
},
char: '{',
allowSpaces: true,
},
renderHTML: ({ node }) => {
+ const matchedOption = props.options?.find((option) => option.title === node.attrs.id)
+ const isAttachment = matchedOption?.uidt === UITypes.Attachment
return [
'span',
{
- class: `prompt-field-tag ${
- props.options?.find((option) => option.title === node.attrs.id)?.uidt === UITypes.Attachment ? '!bg-green-200' : ''
- } ${props.promptFieldTagClassName}`,
+ 'class': `prompt-field-tag ${isAttachment ? '!bg-green-200' : ''} ${props.promptFieldTagClassName}`,
+ 'style': 'max-width: 100px; white-space: nowrap; overflow: hidden; display: inline-block; text-overflow: ellipsis;', // Enforces truncation
+ 'data-tooltip': node.attrs.id, // Tooltip content
},
`${node.attrs.id}`,
]
@@ -93,8 +105,10 @@ const editor = useEditor({
text = text.trim()
vModel.value = text
+
+ debouncedLoadMentionFieldTagTooltip()
},
- editable: true,
+ editable: !readOnly.value,
autofocus: autoFocus.value,
editorProps: { scrollThreshold: 100 },
})
@@ -141,15 +155,60 @@ onMounted(async () => {
}, 100)
}
})
+
+const tooltipInstances: any[] = []
+
+function loadMentionFieldTagTooltip() {
+ document.querySelectorAll('.nc-ai-prompt-with-fields .prompt-field-tag').forEach((el) => {
+ const tooltip = Object.values(el.attributes).find((attr) => attr.name === 'data-tooltip')
+ if (!tooltip || el.scrollWidth <= el.clientWidth) return
+ // Show tooltip only on truncate
+ const instance = tippy(el, {
+ content: `
${tooltip.value}
`,
+ 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
+})
-
+
-
+
@@ -160,11 +219,11 @@ onMounted(async () => {
@apply relative;
.nc-prompt-with-field-suggestion-btn {
- @apply absolute top-[1px] right-[1px];
+ @apply absolute top-[2px] right-[1px];
}
.prompt-field-tag {
- @apply bg-gray-100 rounded-md px-1;
+ @apply bg-gray-100 rounded-md px-1 align-middle;
}
.ProseMirror {
@@ -172,6 +231,10 @@ onMounted(async () => {
resize: vertical;
min-width: 100%;
max-height: min(800px, calc(100vh - 200px)) !important;
+
+ & > p {
+ @apply mr-3;
+ }
}
.ProseMirror-focused {
diff --git a/packages/nc-gui/components/ai/Settings.vue b/packages/nc-gui/components/ai/Settings.vue
index 2b9d66db1c..a1c25b380c 100644
--- a/packages/nc-gui/components/ai/Settings.vue
+++ b/packages/nc-gui/components/ai/Settings.vue
@@ -7,9 +7,11 @@ const props = withDefaults(
workspaceId: string
scope?: string
showTooltip?: boolean
+ isEditColumn?: boolean
}>(),
{
showTooltip: true,
+ isEditColumn: false,
},
)
@@ -19,6 +21,8 @@ const vFkIntegrationId = useVModel(props, 'fkIntegrationId', emits)
const vModel = useVModel(props, 'model', emits)
+const { isEditColumn } = toRefs(props)
+
// const vRandomness = useVModel(props, 'randomness', emits)
const { $api } = useNuxtApp()
@@ -29,7 +33,7 @@ const lastIntegrationId = ref(null)
const isDropdownOpen = ref(false)
-const availableModels = ref([])
+const availableModels = ref<{ value: string; label: string }[]>([])
const isLoadingAvailableModels = ref(false)
@@ -50,10 +54,10 @@ const onIntegrationChange = async (newFkINtegrationId?: string) => {
try {
const response = await $api.integrations.endpoint(newFkINtegrationId, 'availableModels', {})
- availableModels.value = response as string[]
+ availableModels.value = (response || []) as { value: string; label: string }[]
if (!vModel.value && availableModels.value.length > 0) {
- vModel.value = availableModels.value[0]
+ vModel.value = availableModels.value[0].value
}
} catch (error) {
console.error(error)
@@ -63,15 +67,19 @@ const onIntegrationChange = async (newFkINtegrationId?: string) => {
}
onMounted(async () => {
- if (!vFkIntegrationId.value) {
+ if (!vFkIntegrationId.value && !isEditColumn.value) {
if (aiIntegrations.value.length > 0 && aiIntegrations.value[0].id) {
vFkIntegrationId.value = aiIntegrations.value[0].id
nextTick(() => {
onIntegrationChange()
})
}
- } else {
+ } else if (vFkIntegrationId.value) {
lastIntegrationId.value = vFkIntegrationId.value
+
+ if (!vModel.value || !availableModels.value.length) {
+ onIntegrationChange()
+ }
}
})
@@ -111,6 +119,7 @@ onMounted(async () => {
v-model:value="vFkIntegrationId"
class="w-full nc-select-shadow nc-ai-input"
size="middle"
+ placeholder="- select integration -"
@change="onIntegrationChange"
>
@@ -150,20 +159,21 @@ onMounted(async () => {
v-model:value="vModel"
class="w-full nc-select-shadow nc-ai-input"
size="middle"
+ placeholder="- select model -"
:disabled="!vFkIntegrationId || availableModels.length === 0"
:loading="isLoadingAvailableModels"
>
-
+
- {{ md }}
+ {{ md.label }}
- {{ md }}
+ {{ md.label }}
@@ -198,4 +208,10 @@ onMounted(async () => {
-
+
diff --git a/packages/nc-gui/components/cell/AI.vue b/packages/nc-gui/components/cell/AI.vue
index aa5ea1e2c6..ff2e8bbfe5 100644
--- a/packages/nc-gui/components/cell/AI.vue
+++ b/packages/nc-gui/components/cell/AI.vue
@@ -13,6 +13,10 @@ const { generateRows, generatingRows, generatingColumnRows, aiIntegrations } = u
const { row } = useSmartsheetRowStoreOrThrow()
+const { isUIAllowed } = useRoles()
+
+const isPublic = inject(IsPublicInj, ref(false))
+
const meta = inject(MetaInj, ref())
const column = inject(ColumnInj) as Ref<
@@ -40,9 +44,7 @@ const isAiEdited = ref(false)
const isFieldAiIntegrationAvailable = computed(() => {
const fkIntegrationId = column.value?.colOptions?.fk_integration_id
- if (!fkIntegrationId) return false
-
- return ncIsArrayIncludes(aiIntegrations.value, fkIntegrationId, 'id')
+ return !!fkIntegrationId
})
const pk = computed(() => {
@@ -58,7 +60,7 @@ const generate = async () => {
ncIsString(column.value.colOptions?.output_column_ids) && column.value.colOptions.output_column_ids.split(',').length > 1
? column.value.colOptions.output_column_ids.split(',')
: []
- const outputColumns = outputColumnIds.map((id) => meta.value?.columnsById[id])
+ const outputColumns = outputColumnIds.map((id) => meta.value?.columnsById?.[id]).filter(Boolean)
generatingRows.value.push(pk.value)
generatingColumnRows.value.push(column.value.id)
@@ -76,11 +78,18 @@ const generate = async () => {
}
} else {
const obj: AIRecordType = resRow[column.value.title!]
+
if (obj && typeof obj === 'object') {
vModel.value = obj
setTimeout(() => {
isAiEdited.value = false
}, 100)
+ } else {
+ vModel.value = {
+ ...(ncIsObject(vModel.value) ? vModel.value : {}),
+ isStale: false,
+ value: resRow[column.value.title!],
+ }
}
}
}
@@ -99,10 +108,16 @@ const isLoading = computed(() => {
})
const handleSave = () => {
+ vModel.value = { ...vModel.value }
+
emits('save')
}
const debouncedSave = useDebounceFn(handleSave, 1000)
+
+const isDisabledAiButton = computed(() => {
+ return !isFieldAiIntegrationAvailable.value || isLoading.value || isPublic.value || !isUIAllowed('dataEdit')
+})
@@ -113,20 +128,15 @@ const debouncedSave = useDebounceFn(handleSave, 1000)
'justify-center': isGrid && !isExpandedForm,
}"
>
-
+
{{ aiIntegrations.length ? $t('tooltip.aiIntegrationReConfigure') : $t('tooltip.aiIntegrationAddAndReConfigure') }}
-
@@ -147,10 +157,19 @@ const debouncedSave = useDebounceFn(handleSave, 1000)
diff --git a/packages/nc-gui/components/cell/RichText.vue b/packages/nc-gui/components/cell/RichText.vue
index a01b7e41d6..440bfafd9f 100644
--- a/packages/nc-gui/components/cell/RichText.vue
+++ b/packages/nc-gui/components/cell/RichText.vue
@@ -11,6 +11,7 @@ import { TaskItem } from '~/helpers/dbTiptapExtensions/task-item'
import { Link } from '~/helpers/dbTiptapExtensions/links'
import { Mention } from '~/helpers/tiptapExtensions/mention'
import suggestion from '~/helpers/tiptapExtensions/mention/suggestion'
+import UserMentionList from '~/helpers/tiptapExtensions/mention/UserMentionList.vue'
const props = withDefaults(
defineProps<{
@@ -145,7 +146,7 @@ if (appInfo.value.ee) {
isSameUser: bUser?.id === user.value?.id,
}),
)
- span.setAttribute('class', `${colorStyles} mention font-semibold m-0.5 rounded-md px-1`)
+ span.setAttribute('class', `${colorStyles} mention font-semibold m-0.5 rounded-md px-1 inline-block`)
span.textContent = `@${processedContent}`
return span.outerHTML
}
@@ -224,7 +225,7 @@ const tiptapExtensions = [
? [
Mention.configure({
suggestion: {
- ...suggestion,
+ ...suggestion(UserMentionList),
items: ({ query }) =>
baseUsers.value
.filter((user) => user.deleted !== true)
@@ -424,7 +425,7 @@ onClickOutside(editorDom, (e) => {
'justify-end xs:hidden': !isForm,
}"
>
-