mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-01 19:17:04 +00:00
434 lines
12 KiB
Vue
434 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import { ButtonActionsType, type ButtonType, type ColumnType, type FilterType } from 'nocodb-sdk'
|
|
import type { Ref } from 'vue'
|
|
import { validateRowFilters } from '~/utils/dataUtils'
|
|
|
|
const column = inject(ColumnInj) as Ref<
|
|
ColumnType & {
|
|
colOptions: ButtonType
|
|
}
|
|
>
|
|
|
|
const cellValue = inject(CellValueInj, ref())
|
|
|
|
const { currentRow, displayValue, changedColumns } = useSmartsheetRowStoreOrThrow()
|
|
|
|
const { generateRows, generatingRows, generatingColumnRows, generatingColumns, aiIntegrations } = useNocoAi()
|
|
|
|
const { appInfo } = useGlobal()
|
|
|
|
const { getColor } = useTheme()
|
|
|
|
const meta = inject(MetaInj, ref())
|
|
|
|
const isGrid = inject(IsGridInj, ref(false))
|
|
|
|
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
|
|
|
|
const { isUIAllowed } = useRoles()
|
|
|
|
const isPublic = inject(IsPublicInj, ref(false))
|
|
|
|
const { $api } = useNuxtApp()
|
|
|
|
const { t } = useI18n()
|
|
|
|
const rowId = computed(() => {
|
|
return extractPkFromRow(currentRow.value?.row, meta.value!.columns!)
|
|
})
|
|
|
|
const { runScript, activeExecutions, fieldIDRowMapping } = useScriptExecutor()
|
|
|
|
const { addScriptExecution } = useActionPane()
|
|
|
|
const scriptStore = useScriptStore()
|
|
|
|
const { loadScript } = scriptStore
|
|
|
|
const isLoading = ref(false)
|
|
|
|
const pk = computed(() => {
|
|
if (!meta.value?.columns) return
|
|
return extractPkFromRow(currentRow.value?.row, meta.value.columns)
|
|
})
|
|
|
|
const isAiButtonType = computed(() => column.value?.colOptions?.type === ButtonActionsType.Ai)
|
|
|
|
const isFieldAiIntegrationAvailable = computed(() => {
|
|
if (!isAiButtonType.value) return true
|
|
|
|
const fkIntegrationId = column.value?.colOptions?.fk_integration_id
|
|
|
|
return !!fkIntegrationId
|
|
})
|
|
|
|
const generate = async () => {
|
|
if (!meta?.value?.id || !meta.value.columns || !column?.value?.id) return
|
|
|
|
if (!pk.value) return
|
|
|
|
const outputColumnIds =
|
|
ncIsString(column.value.colOptions?.output_column_ids) && column.value.colOptions.output_column_ids.split(',').length > 0
|
|
? column.value.colOptions.output_column_ids.split(',')
|
|
: []
|
|
const outputColumns = outputColumnIds.map((id) => meta.value?.columnsById[id])
|
|
|
|
generatingRows.value.push(pk.value)
|
|
generatingColumnRows.value.push(column.value.id)
|
|
|
|
generatingColumns.value.push(...(outputColumnIds ?? []))
|
|
|
|
// In expanded form get preview data and update local state so that expanded form save btn works properly
|
|
const res = await generateRows(meta.value.id, column.value.id, [pk.value], false, isExpandedForm.value)
|
|
|
|
if (res?.length) {
|
|
const resRow = res[0]
|
|
|
|
if (outputColumnIds) {
|
|
for (const col of outputColumns) {
|
|
if (col && currentRow.value.row) {
|
|
changedColumns.value.add(col.title!)
|
|
|
|
currentRow.value.row[col.title!] = resRow[col.title!]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
generatingRows.value = generatingRows.value.filter((v) => v !== pk.value)
|
|
generatingColumnRows.value = generatingColumnRows.value.filter((v) => v !== column.value.id)
|
|
generatingColumns.value = generatingColumns.value.filter((v) => !outputColumnIds?.includes(v))
|
|
}
|
|
|
|
const isExecutingId = ref('')
|
|
|
|
const isExecuting = computed(
|
|
() =>
|
|
activeExecutions.value.get(isExecutingId.value)?.status === 'running' ||
|
|
fieldIDRowMapping.value.get(`${pk.value}:${column.value.id}`) === 'running',
|
|
)
|
|
|
|
const invalidUrlTooltip = ref('')
|
|
|
|
const baseStore = useBase()
|
|
const { getBaseType } = baseStore
|
|
const { metas } = useMetas()
|
|
const { user } = useGlobal()
|
|
|
|
const isFilterConditionMet = computed(() => {
|
|
const filters = column.value.colOptions?.filters as FilterType[] | undefined
|
|
if (!filters || !filters.length) return true
|
|
|
|
const rowData = currentRow.value?.row
|
|
if (!rowData) return true
|
|
|
|
const columns = meta.value?.columns as ColumnType[]
|
|
if (!columns) return true
|
|
|
|
return validateRowFilters(filters, rowData, columns, getBaseType(meta.value?.source_id), metas.value, meta.value?.base_id, {
|
|
currentUser: user.value?.id ? { id: user.value.id, email: user.value.email } : undefined,
|
|
})
|
|
})
|
|
|
|
const filterDisabledTooltip = computed(() => {
|
|
if (isFilterConditionMet.value) return ''
|
|
return t('msg.buttonConditionNotMet')
|
|
})
|
|
|
|
const componentProps = computed(() => {
|
|
const filterDisabled = !isFilterConditionMet.value
|
|
|
|
if (column.value.colOptions.type === ButtonActionsType.Url) {
|
|
let url = addMissingUrlSchma(cellValue.value?.url)
|
|
|
|
// if url params not encoded, encode them using encodeURI
|
|
try {
|
|
url = decodeURI(url) === url ? encodeURI(url) : url
|
|
} catch {
|
|
url = encodeURI(url)
|
|
}
|
|
|
|
const isValidUrl = isValidURL(url, { require_tld: !appInfo.value?.allowLocalUrl })
|
|
|
|
invalidUrlTooltip.value = !isValidUrl ? t('msg.error.invalidURL') : ''
|
|
|
|
return {
|
|
href: url,
|
|
target: '_blank',
|
|
...(column.value?.colOptions.error || !isValidUrl || filterDisabled ? { disabled: true } : {}),
|
|
}
|
|
} else if (column.value.colOptions.type === ButtonActionsType.Webhook) {
|
|
return {
|
|
disabled:
|
|
filterDisabled ||
|
|
isPublic.value ||
|
|
!isUIAllowed('hookTrigger') ||
|
|
isLoading.value ||
|
|
!column.value.colOptions.fk_webhook_id ||
|
|
!cellValue.value?.fk_webhook_id,
|
|
}
|
|
} else if (column.value.colOptions.type === ButtonActionsType.Script) {
|
|
return {
|
|
disabled: filterDisabled || isPublic.value || isExecuting.value || !isUIAllowed('dataEdit') || isLoading.value,
|
|
}
|
|
} else if (column.value.colOptions.type === ButtonActionsType.Ai) {
|
|
return {
|
|
disabled:
|
|
filterDisabled ||
|
|
isPublic.value ||
|
|
!isUIAllowed('dataEdit') ||
|
|
!isFieldAiIntegrationAvailable.value ||
|
|
isLoading.value ||
|
|
(pk.value &&
|
|
generatingRows.value.includes(pk.value) &&
|
|
column.value?.id &&
|
|
generatingColumnRows.value.includes(column.value.id)),
|
|
}
|
|
}
|
|
|
|
// If button type is missing then keep it as disabled
|
|
if (!column.value.colOptions.type) {
|
|
return {
|
|
disabled: true,
|
|
}
|
|
}
|
|
|
|
return filterDisabled ? { disabled: true } : {}
|
|
})
|
|
|
|
const buttonColors = computed(() => {
|
|
return getButtonColorsCssVariables(column.value.colOptions.theme ?? 'solid', column.value.colOptions.color ?? 'brand', getColor)
|
|
})
|
|
|
|
const afterActionStatus = ref<{
|
|
status: 'success' | 'error'
|
|
tooltip?: string
|
|
} | null>(null)
|
|
|
|
const triggerAction = async () => {
|
|
const colOptions = column.value.colOptions
|
|
afterActionStatus.value = null
|
|
|
|
if (!colOptions.type) return
|
|
|
|
if (colOptions.type === ButtonActionsType.Url) {
|
|
confirmPageLeavingRedirect(componentProps.value?.href, componentProps.value?.target, appInfo.value?.allowLocalUrl)
|
|
} else if (colOptions.type === ButtonActionsType.Webhook) {
|
|
try {
|
|
isLoading.value = true
|
|
|
|
await $api.internal.postOperation(
|
|
meta.value!.fk_workspace_id!,
|
|
meta.value!.base_id!,
|
|
{
|
|
operation: 'hookTrigger',
|
|
hookId: cellValue.value?.fk_webhook_id,
|
|
rowId: rowId!.value,
|
|
},
|
|
{},
|
|
)
|
|
|
|
afterActionStatus.value = { status: 'success' }
|
|
ncDelay(2000).then(() => {
|
|
afterActionStatus.value = null
|
|
})
|
|
} catch (e: any) {
|
|
console.log(e)
|
|
|
|
const errorMsg = await extractSdkResponseErrorMsg(e)
|
|
message.error(errorMsg)
|
|
|
|
afterActionStatus.value = { status: 'error', tooltip: errorMsg }
|
|
ncDelay(3000).then(() => {
|
|
afterActionStatus.value = null
|
|
})
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
} else if (colOptions.type === ButtonActionsType.Ai) {
|
|
await generate()
|
|
} else if (colOptions.type === ButtonActionsType.Script) {
|
|
try {
|
|
isLoading.value = true
|
|
|
|
const script = await loadScript(colOptions.fk_script_id)
|
|
|
|
if (!script) {
|
|
throw new Error('Script not found')
|
|
}
|
|
|
|
const rowId = pk.value
|
|
|
|
const scriptExecutionId = await runScript(script, currentRow.value.row, {
|
|
pk: rowId!,
|
|
fieldId: column.value.id!,
|
|
executionId: `${rowId}-${column.value.id!}-${Date.now()}`,
|
|
})
|
|
|
|
addScriptExecution(scriptExecutionId, {
|
|
recordId: rowId as string,
|
|
displayValue: displayValue.value,
|
|
scriptId: script?.id as string,
|
|
scriptName: script?.title || 'Untitled Script',
|
|
buttonFieldName: column.value.title || 'Button',
|
|
})
|
|
|
|
isExecutingId.value = scriptExecutionId
|
|
|
|
afterActionStatus.value = { status: 'success' }
|
|
ncDelay(2000).then(() => {
|
|
afterActionStatus.value = null
|
|
})
|
|
} catch (e: any) {
|
|
console.log(e)
|
|
|
|
const errorMsg = await extractSdkResponseErrorMsg(e)
|
|
message.error(errorMsg)
|
|
|
|
afterActionStatus.value = { status: 'error', tooltip: errorMsg }
|
|
ncDelay(3000).then(() => {
|
|
afterActionStatus.value = null
|
|
})
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
:class="{
|
|
'justify-center': isGrid && !isExpandedForm,
|
|
}"
|
|
class="w-full flex items-center"
|
|
>
|
|
<NcTooltip
|
|
:disabled="
|
|
isAiButtonType
|
|
? (isFieldAiIntegrationAvailable || isPublic || !isUIAllowed('dataEdit')) && !filterDisabledTooltip
|
|
: !invalidUrlTooltip && !afterActionStatus?.tooltip && !filterDisabledTooltip
|
|
"
|
|
class="flex"
|
|
>
|
|
<template #title>
|
|
{{
|
|
filterDisabledTooltip
|
|
? filterDisabledTooltip
|
|
: isAiButtonType
|
|
? aiIntegrations.length
|
|
? $t('tooltip.aiIntegrationReConfigure')
|
|
: $t('tooltip.aiIntegrationAddAndReConfigure')
|
|
: afterActionStatus?.tooltip || invalidUrlTooltip
|
|
}}
|
|
</template>
|
|
<component
|
|
:is="column.colOptions.type === ButtonActionsType.Url ? 'a' : 'button'"
|
|
v-bind="componentProps"
|
|
data-testid="nc-button-cell"
|
|
:class="[
|
|
`${column.colOptions.color ?? 'brand'} ${column.colOptions.theme ?? 'solid'}`,
|
|
{ '!w-6': !column.colOptions.label, 'disabled': componentProps.disabled, 'is-expanded-form': isExpandedForm },
|
|
]"
|
|
class="nc-cell-button nc-button-cell-link btn-cell-colors truncate flex items-center"
|
|
:style="buttonColors"
|
|
@click.prevent="triggerAction"
|
|
>
|
|
<GeneralIcon
|
|
v-if="afterActionStatus"
|
|
:icon="afterActionStatus.status === 'success' ? 'ncCheck' : 'ncInfo'"
|
|
class="w-4 h-4 flex-none text-current"
|
|
/>
|
|
<GeneralLoader
|
|
v-else-if="
|
|
isLoading ||
|
|
isExecuting ||
|
|
(pk && generatingRows.includes(pk) && column?.id && generatingColumnRows.includes(column.id))
|
|
"
|
|
class="flex w-4 h-4 !text-current"
|
|
size="medium"
|
|
/>
|
|
<GeneralIcon v-else-if="column.colOptions.icon" :icon="column.colOptions.icon" class="!w-4 min-w-4 min-h-4 !h-4" />
|
|
<NcTooltip v-if="column.colOptions.label" class="!truncate" show-on-truncate-only>
|
|
<span class="truncate font-medium" :class="{ 'text-sm': isExpandedForm, 'text-[13px]': !isExpandedForm }">
|
|
{{ column.colOptions.label }}
|
|
</span>
|
|
<template #title>
|
|
{{ column.colOptions.label }}
|
|
</template>
|
|
</NcTooltip>
|
|
</component>
|
|
</NcTooltip>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
.nc-data-cell {
|
|
&:has(.nc-virtual-cell-button) {
|
|
@apply !border-none;
|
|
box-shadow: none !important;
|
|
|
|
&:focus-within:not(.nc-readonly-div-data-cell):not(.nc-system-field) {
|
|
box-shadow: none !important;
|
|
}
|
|
}
|
|
|
|
&:has(.nc-cell-button.is-expanded-form) {
|
|
@apply -mt-1 -ml-1;
|
|
}
|
|
|
|
.nc-cell-attachment {
|
|
@apply !border-none;
|
|
}
|
|
}
|
|
|
|
.nc-button-cell-link {
|
|
@apply !no-underline;
|
|
}
|
|
</style>
|
|
|
|
<style scoped lang="scss">
|
|
.nc-cell-button {
|
|
@apply px-2 flex items-center gap-2 transition-all justify-center;
|
|
&:not([class*='text']) {
|
|
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.06), 0px 5px 3px -2px rgba(0, 0, 0, 0.02);
|
|
}
|
|
&:focus-within {
|
|
@apply outline-none ring-0 shadow-focus;
|
|
}
|
|
&[disabled] {
|
|
@apply opacity-50 cursor-not-allowed;
|
|
}
|
|
|
|
&.is-expanded-form {
|
|
@apply h-8 rounded-lg my-1;
|
|
}
|
|
|
|
&:not(.is-expanded-form) {
|
|
@apply h-6 rounded-md;
|
|
}
|
|
}
|
|
|
|
.btn-cell-colors {
|
|
color: var(--btn-cell-text);
|
|
background: var(--btn-cell-bg);
|
|
|
|
&:hover {
|
|
background: var(--btn-cell-bg-hover);
|
|
color: var(--btn-cell-text-hover);
|
|
}
|
|
|
|
&.disabled,
|
|
&[disabled] {
|
|
@apply cursor-not-allowed opacity-60;
|
|
|
|
background: var(--btn-cell-disabled-bg);
|
|
color: var(--btn-cell-disabled-text);
|
|
}
|
|
|
|
&.light {
|
|
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.06), 0px 5px 3px -2px rgba(0, 0, 0, 0.02);
|
|
}
|
|
}
|
|
</style>
|