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 +}) - + 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') +}) diff --git a/packages/nc-gui/components/cell/SingleSelect.vue b/packages/nc-gui/components/cell/SingleSelect.vue index 874bc99e41..07b56e4e5d 100644 --- a/packages/nc-gui/components/cell/SingleSelect.vue +++ b/packages/nc-gui/components/cell/SingleSelect.vue @@ -482,8 +482,12 @@ const onFocus = () => { text-overflow: clip; } -:deep(.ant-select-selection-search-input) { - @apply !text-xs; +:deep(.ant-select-selection-search) { + @apply flex items-center; + + .ant-select-selection-search-input { + @apply !text-xs; + } } :deep(.ant-select-clear > span) { diff --git a/packages/nc-gui/components/cell/TextArea.vue b/packages/nc-gui/components/cell/TextArea.vue index 0214061431..c51602a5c5 100644 --- a/packages/nc-gui/components/cell/TextArea.vue +++ b/packages/nc-gui/components/cell/TextArea.vue @@ -14,6 +14,8 @@ const props = defineProps<{ const emits = defineEmits(['update:modelValue', 'update:isAiEdited', 'generate', 'close']) +const meta = inject(MetaInj, ref()) + const column = inject(ColumnInj) const editEnabled = inject(EditModeInj, ref(false)) @@ -34,7 +36,9 @@ const readOnly = inject(ReadonlyInj, ref(false)) const { showNull, user } = useGlobal() -const { aiLoading, aiIntegrations } = useNocoAi() +const { currentRow } = useSmartsheetRowStoreOrThrow() + +const { aiLoading, aiIntegrations, generatingRows, generatingColumnRows } = useNocoAi() const baseStore = useBase() @@ -93,6 +97,19 @@ const aiWarningRef = ref() const { height: aiWarningRefHeight } = useElementSize(aiWarningRef) +const rowId = computed(() => { + return extractPkFromRow(currentRow.value?.row, meta.value!.columns!) +}) + +const isAiGenerating = computed(() => { + return !!( + rowId.value && + column?.value.id && + generatingRows.value.includes(rowId.value) && + generatingColumnRows.value.includes(column.value.id) + ) +}) + watch(isVisible, () => { if (isVisible.value) { setTimeout(() => { @@ -272,33 +289,43 @@ const updateSize = () => { } } -watch([isVisible, inputRef], (value) => { - const observer = new ResizeObserver((entries) => { - for (const entry of entries) { - const { width, height } = entry.contentRect +watch( + [isVisible, inputRef], + (value) => { + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const { width, height } = entry.contentRect + + if (!isVisible.value) { + return + } - if (!isVisible.value) { - return + localStorage.setItem(STORAGE_KEY, JSON.stringify({ width, height })) } + }) - localStorage.setItem(STORAGE_KEY, JSON.stringify({ width, height })) - } - }) + if (value) { + if (isRichMode.value && isVisible.value) { + setTimeout(() => { + const el = document.querySelector('.nc-long-text-expanded-modal .nc-textarea-rich-editor .tiptap') as HTMLElement - if (value) { - if (isRichMode.value) { - setTimeout(() => { - observer.observe(document.querySelector('.nc-long-text-expanded-modal .nc-textarea-rich-editor .tiptap') as HTMLElement) + if (!el) return + + observer.observe(el) + updateSize() + }, 50) + } else { updateSize() - }, 50) + } } else { - updateSize() + observer.disconnect() } - } else { - observer.disconnect() - } -}) + }, + { + immediate: true, + }, +)