优化页面交互并增强 Provider 配置能力
- 表单下拉框不再用 label 包裹,改为 div + aria-label,避免点击后 下拉面板瞬间关闭 - Provider 卡片新增「从上游获取模型」和「修改」入口,支持编辑 base URL、API Key 与 api_mode,凭据写入对应的环境变量 - 服务端抽出 provider-model-discovery 统一不同 api_mode 的模型发现, 旧的 custom_providers 与新的 providers 字典结构都能正确更新 - Agent Bridge 改为后台启动,HTTP 接口在 Python 运行时就绪前即可用; worker 连接被拒时按类型重试,不重放可能已产生副作用的请求 - 断线重连后恢复 profiles、models 与会话列表,健康检查改为自调度 的 setTimeout,离线时 2 秒后重试 - 批量删除确认文案改用 Popconfirm content,避免变成侧栏触发器文本
This commit is contained in:
@@ -100,6 +100,7 @@ export interface AvailableModelGroup {
|
||||
builtin?: boolean
|
||||
/** Env var used by Hermes to override this provider's base URL. If present, the preset URL is editable. */
|
||||
base_url_env?: string
|
||||
credentials_editable?: boolean
|
||||
/** Config source for custom providers. Dict-backed providers can be deleted from providers:<key>. */
|
||||
provider_source?: 'custom_providers' | 'providers'
|
||||
provider_key?: string
|
||||
@@ -224,6 +225,7 @@ export async function fetchProviderModels(data: {
|
||||
provider?: string
|
||||
label?: string
|
||||
update_cache?: boolean
|
||||
api_mode?: ProviderApiMode
|
||||
}): Promise<{ models: string[] }> {
|
||||
return request<{ models: string[] }>('/api/hermes/provider-models', {
|
||||
method: 'POST',
|
||||
@@ -277,6 +279,8 @@ export async function removeCustomProvider(name: string, options: { source?: 'cu
|
||||
}
|
||||
|
||||
export async function updateProvider(poolKey: string, data: {
|
||||
source?: 'custom_providers' | 'providers'
|
||||
providerKey?: string
|
||||
name?: string
|
||||
base_url?: string
|
||||
api_key?: string
|
||||
|
||||
@@ -1125,13 +1125,16 @@ async function handleSessionModelCustomSubmit() {
|
||||
<a-popconfirm
|
||||
v-if="isBatchMode && selectedCount > 0"
|
||||
:visible="showBatchDeleteConfirm"
|
||||
:content="t('chat.confirmBatchDelete', { count: selectedCount })"
|
||||
:ok-text="t('common.confirm')"
|
||||
:cancel-text="t('common.cancel')"
|
||||
:ok-button-props="{ loading: isBatchDeleting, disabled: isBatchDeleting }"
|
||||
:cancel-button-props="{ disabled: isBatchDeleting }"
|
||||
@ok="handleBatchDeleteConfirm"
|
||||
@cancel="showBatchDeleteConfirm = false"
|
||||
@update:visible="showBatchDeleteConfirm = $event"
|
||||
>
|
||||
<a-button type="secondary" size="mini" status="danger" :loading="isBatchDeleting" :disabled="isBatchDeleting">
|
||||
<a-button :aria-label="t('common.delete')" type="secondary" size="mini" status="danger" :loading="isBatchDeleting" :disabled="isBatchDeleting">
|
||||
<template #icon>
|
||||
<svg
|
||||
width="14"
|
||||
@@ -1146,7 +1149,6 @@ async function handleSessionModelCustomSubmit() {
|
||||
</svg>
|
||||
</template>
|
||||
</a-button>
|
||||
{{ t('chat.confirmBatchDelete', { count: selectedCount }) }}
|
||||
</a-popconfirm>
|
||||
<a-button
|
||||
v-if="isBatchMode"
|
||||
@@ -1435,34 +1437,37 @@ async function handleSessionModelCustomSubmit() {
|
||||
>
|
||||
<template #title>{{ t('chat.newChat') }}</template>
|
||||
<div class="new-chat-form">
|
||||
<label class="new-chat-field">
|
||||
<div class="new-chat-field">
|
||||
<span class="new-chat-label">{{ t("sidebar.profiles") }}</span>
|
||||
<a-select
|
||||
:aria-label="t('sidebar.profiles')"
|
||||
:model-value="newChatProfile"
|
||||
:options="newChatProfileOptions"
|
||||
:loading="newChatLoading || profilesStore.loading"
|
||||
@change="handleNewChatProfileChange"
|
||||
/>
|
||||
</label>
|
||||
<label class="new-chat-field">
|
||||
</div>
|
||||
<div class="new-chat-field">
|
||||
<span class="new-chat-label">{{ t("models.provider") }}</span>
|
||||
<a-select
|
||||
:aria-label="t('models.provider')"
|
||||
:model-value="newChatProvider"
|
||||
:options="newChatProviderOptions"
|
||||
:disabled="newChatLoading"
|
||||
@change="handleNewChatProviderChange"
|
||||
/>
|
||||
</label>
|
||||
<label class="new-chat-field">
|
||||
</div>
|
||||
<div class="new-chat-field">
|
||||
<span class="new-chat-label">{{ t("models.models") }}</span>
|
||||
<a-select
|
||||
:aria-label="t('models.models')"
|
||||
v-model="newChatModel"
|
||||
:options="newChatModelOptions"
|
||||
:disabled="newChatLoading || !newChatProvider"
|
||||
allow-search
|
||||
allow-clear
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="new-chat-field">
|
||||
<span class="new-chat-label">
|
||||
{{ t("chat.workspace") }}
|
||||
|
||||
@@ -282,25 +282,27 @@ watch(() => form.value.provider, (provider) => {
|
||||
:mask-closable="!saving"
|
||||
>
|
||||
<div class="auxiliary-form">
|
||||
<label>
|
||||
<div class="auxiliary-select-field">
|
||||
<span>{{ t('models.provider') }}</span>
|
||||
<a-select
|
||||
:aria-label="t('models.provider')"
|
||||
v-model="form.provider"
|
||||
:options="providerOptions"
|
||||
:placeholder="t('models.chooseProvider')"
|
||||
allow-search
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
</div>
|
||||
<div class="auxiliary-select-field">
|
||||
<span>{{ t('models.defaultModel') }}</span>
|
||||
<a-select
|
||||
:aria-label="t('models.defaultModel')"
|
||||
v-model="form.model"
|
||||
:options="modelOptions"
|
||||
:placeholder="t('models.selectModel')"
|
||||
:disabled="!form.provider || form.provider === 'auto' || form.provider === 'main'"
|
||||
allow-search
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<span>{{ t('models.auxiliaryTimeout') }}</span>
|
||||
<a-input-number
|
||||
@@ -395,7 +397,8 @@ watch(() => form.value.provider, (provider) => {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
|
||||
label {
|
||||
label,
|
||||
.auxiliary-select-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { AvailableModelGroup } from '@/api/hermes/system'
|
||||
import { useModelsStore } from '@/stores/hermes/models'
|
||||
import { useAppStore } from '@/stores/hermes/app'
|
||||
import { useChatStore } from '@/stores/hermes/chat'
|
||||
import ProviderEditModal from './ProviderEditModal.vue'
|
||||
import { fetchProviderModels } from '@/api/hermes/system'
|
||||
import { checkCopilotToken, disableCopilot } from '@/api/hermes/copilot-auth'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -24,6 +26,32 @@ const isConfigBackedProvider = computed(() => isCustomProviderKey.value || (!pro
|
||||
const isCopilot = computed(() => props.provider.provider === 'copilot')
|
||||
const displayName = computed(() => props.provider.label)
|
||||
const deleting = ref(false)
|
||||
const showEditModal = ref(false)
|
||||
const fetchingUpstream = ref(false)
|
||||
const canEditProvider = computed(() => isConfigBackedProvider.value || props.provider.credentials_editable || !!props.provider.base_url_env)
|
||||
|
||||
async function handleFetchUpstream() {
|
||||
if (fetchingUpstream.value) return
|
||||
fetchingUpstream.value = true
|
||||
try {
|
||||
const result = await fetchProviderModels({
|
||||
provider: props.provider.provider,
|
||||
label: props.provider.label,
|
||||
base_url: props.provider.base_url,
|
||||
api_key: props.provider.api_key,
|
||||
update_cache: true,
|
||||
api_mode: props.provider.api_mode,
|
||||
freeOnly: props.provider.provider === 'openrouter',
|
||||
})
|
||||
await modelsStore.fetchProviders()
|
||||
await appStore.reloadModels()
|
||||
message.success(t('models.upstreamModelsFetched', { count: result.models.length }))
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || t('models.upstreamModelsFailed'))
|
||||
} finally {
|
||||
fetchingUpstream.value = false
|
||||
}
|
||||
}
|
||||
const destructiveActionLabel = computed(() => {
|
||||
if (isConfigBackedProvider.value) return t('common.delete')
|
||||
if (isCopilot.value) return t('models.disableProvider')
|
||||
@@ -302,11 +330,15 @@ async function handleDelete() {
|
||||
</div>
|
||||
|
||||
<div class="cy-panel-b card-actions">
|
||||
<a-button size="mini" type="text" :loading="fetchingUpstream" :disabled="!provider.base_url || showEditModal" @click="handleFetchUpstream">{{ t('models.fetchUpstreamModels') }}</a-button>
|
||||
<a-button v-if="canEditProvider" size="mini" type="text" :disabled="fetchingUpstream || deleting" @click="showEditModal = true">{{ t('common.edit') }}</a-button>
|
||||
<a-button size="mini" type="text" @click="showAliasListModal = true">{{ t('models.aliasManage') }}</a-button>
|
||||
<a-button size="mini" type="text" @click="openVisibilityModal">{{ t('models.manageVisibleModels') }}</a-button>
|
||||
<a-button size="mini" type="text" status="danger" :loading="deleting" @click="handleDelete">{{ destructiveActionLabel }}</a-button>
|
||||
</div>
|
||||
|
||||
<ProviderEditModal v-if="showEditModal" :provider="provider" @close="showEditModal = false" />
|
||||
|
||||
<a-modal
|
||||
v-model:visible="showAliasListModal"
|
||||
:footer="false"
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { updateProvider, type AvailableModelGroup } from '@/api/hermes/system'
|
||||
import { useModelsStore } from '@/stores/hermes/models'
|
||||
import { useAppStore } from '@/stores/hermes/app'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
|
||||
const props = defineProps<{ provider: AvailableModelGroup }>()
|
||||
const emit = defineEmits<{ close: [] }>()
|
||||
const { t } = useI18n()
|
||||
const modelsStore = useModelsStore()
|
||||
const appStore = useAppStore()
|
||||
const message = useMessage()
|
||||
const visible = ref(true)
|
||||
const saving = ref(false)
|
||||
const baseUrl = ref(props.provider.base_url)
|
||||
const apiKey = ref('')
|
||||
const canEditBaseUrl = computed(() => props.provider.provider.startsWith('custom:') || !!props.provider.provider_source || !!props.provider.base_url_env)
|
||||
|
||||
async function save() {
|
||||
if (saving.value) return
|
||||
const url = baseUrl.value.trim()
|
||||
if (canEditBaseUrl.value) {
|
||||
try {
|
||||
if (!['http:', 'https:'].includes(new URL(url).protocol)) throw new Error('Invalid protocol')
|
||||
} catch {
|
||||
message.error(t('models.invalidBaseUrl'))
|
||||
return
|
||||
}
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
await updateProvider(props.provider.provider, {
|
||||
...(canEditBaseUrl.value ? { base_url: url } : {}),
|
||||
...(apiKey.value.trim() ? { api_key: apiKey.value.trim() } : {}),
|
||||
source: props.provider.provider_source,
|
||||
providerKey: props.provider.provider_key,
|
||||
})
|
||||
await modelsStore.fetchProviders()
|
||||
await appStore.reloadModels()
|
||||
message.success(t('models.providerUpdated'))
|
||||
emit('close')
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || t('models.providerUpdateFailed'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
:title="t('models.editProviderFor', { name: provider.label })"
|
||||
width="min(560px, calc(100vw - 32px))"
|
||||
:mask-closable="!saving"
|
||||
:esc-to-close="!saving"
|
||||
:closable="!saving"
|
||||
@cancel="emit('close')"
|
||||
>
|
||||
<a-form :model="{ baseUrl, apiKey }" layout="vertical" class="provider-edit-form">
|
||||
<a-form-item :label="t('models.provider')">
|
||||
<a-input :model-value="provider.provider" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('models.baseUrl')">
|
||||
<a-input v-model="baseUrl" :aria-label="t('models.baseUrl')" :disabled="saving || !canEditBaseUrl" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('models.apiKey')" :extra="t('models.keepExistingKey')">
|
||||
<a-input-password v-model="apiKey" :aria-label="t('models.apiKey')" :placeholder="t('models.keepExistingKey')" :disabled="saving" autocomplete="new-password" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-button :disabled="saving" @click="emit('close')">{{ t('common.cancel') }}</a-button>
|
||||
<a-button type="primary" :loading="saving" @click="save">{{ t('common.save') }}</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
@@ -221,6 +221,7 @@ async function fetchModels() {
|
||||
: formData.value.name.trim() || provider
|
||||
const data = await fetchProviderModels({
|
||||
base_url: base_url.trim(),
|
||||
api_mode: formData.value.api_mode,
|
||||
api_key: formData.value.api_key.trim(),
|
||||
provider,
|
||||
label,
|
||||
|
||||
@@ -1211,6 +1211,14 @@ export default {
|
||||
|
||||
// Models
|
||||
models: {
|
||||
fetchUpstreamModels: 'Fetch models from upstream',
|
||||
upstreamModelsFetched: 'Fetched {count} models from upstream',
|
||||
upstreamModelsFailed: 'Failed to fetch models from upstream',
|
||||
editProviderFor: 'Edit {name}',
|
||||
keepExistingKey: 'Leave blank to keep the existing key',
|
||||
invalidBaseUrl: 'Enter a valid HTTP or HTTPS Base URL',
|
||||
providerUpdated: 'Provider configuration updated',
|
||||
providerUpdateFailed: 'Failed to update provider configuration',
|
||||
title: 'Models',
|
||||
searchPlaceholder: 'Search models...',
|
||||
noResults: 'No results',
|
||||
|
||||
@@ -1211,6 +1211,14 @@ export default {
|
||||
|
||||
// 模型
|
||||
models: {
|
||||
fetchUpstreamModels: '从上游获取模型',
|
||||
upstreamModelsFetched: '已从上游获取 {count} 个模型',
|
||||
upstreamModelsFailed: '从上游获取模型失败',
|
||||
editProviderFor: '修改 {name}',
|
||||
keepExistingKey: '留空保留现有 Key',
|
||||
invalidBaseUrl: '请输入有效的 HTTP 或 HTTPS Base URL',
|
||||
providerUpdated: 'Provider 配置已更新',
|
||||
providerUpdateFailed: '修改 Provider 配置失败',
|
||||
title: '模型',
|
||||
searchPlaceholder: '搜索模型...',
|
||||
noResults: '无结果',
|
||||
|
||||
@@ -41,7 +41,8 @@ export const useAppStore = defineStore('app', () => {
|
||||
const customModels = ref<Record<string, string[]>>({})
|
||||
const modelAliases = ref<Record<string, Record<string, string>>>({})
|
||||
const modelVisibility = ref<ModelVisibility>({})
|
||||
const healthPollTimer = ref<ReturnType<typeof setInterval>>()
|
||||
const healthPollTimer = ref<ReturnType<typeof setTimeout>>()
|
||||
let healthPollGeneration = 0
|
||||
const nodeVersion = ref('')
|
||||
const isDocker = ref(false)
|
||||
|
||||
@@ -301,13 +302,19 @@ export const useAppStore = defineStore('app', () => {
|
||||
|
||||
function startHealthPolling(interval = 30000) {
|
||||
stopHealthPolling()
|
||||
checkConnection()
|
||||
healthPollTimer.value = setInterval(checkConnection, interval)
|
||||
const generation = healthPollGeneration
|
||||
const poll = async () => {
|
||||
await checkConnection()
|
||||
if (generation !== healthPollGeneration) return
|
||||
healthPollTimer.value = setTimeout(poll, connected.value ? interval : Math.min(interval, 2000))
|
||||
}
|
||||
void poll()
|
||||
}
|
||||
|
||||
function stopHealthPolling() {
|
||||
healthPollGeneration++
|
||||
if (healthPollTimer.value) {
|
||||
clearInterval(healthPollTimer.value)
|
||||
clearTimeout(healthPollTimer.value)
|
||||
healthPollTimer.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -995,11 +995,13 @@ export const useChatStore = defineStore('chat', () => {
|
||||
} else {
|
||||
clearActiveSession()
|
||||
}
|
||||
sessionsLoaded.value = true
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Failed to load sessions:', err)
|
||||
return false
|
||||
} finally {
|
||||
isLoadingSessions.value = false
|
||||
sessionsLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ChatPanel from '@/components/hermes/chat/ChatPanel.vue'
|
||||
import { useAppStore } from '@/stores/hermes/app'
|
||||
@@ -24,6 +24,8 @@ const routeProfile = computed(() => {
|
||||
return typeof value === 'string' && value.trim() ? value : null
|
||||
})
|
||||
|
||||
const initialLoadFinished = ref(false)
|
||||
|
||||
const productTitle = 'hermes-ui'
|
||||
const tabTitle = computed(() => {
|
||||
if (route.name !== 'hermes.session') return productTitle
|
||||
@@ -39,8 +41,8 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
async function loadRouteSession() {
|
||||
await chatStore.loadSessions(chatStore.sessionProfileFilter, routeSessionId.value)
|
||||
if (routeSessionId.value && chatStore.activeSessionId !== routeSessionId.value) {
|
||||
const loaded = await chatStore.loadSessions(chatStore.sessionProfileFilter, routeSessionId.value)
|
||||
if (loaded && routeSessionId.value && chatStore.activeSessionId !== routeSessionId.value) {
|
||||
await router.replace({ name: 'hermes.chat' })
|
||||
}
|
||||
}
|
||||
@@ -55,6 +57,16 @@ onMounted(async () => {
|
||||
settingsStore.fetchSettings(),
|
||||
])
|
||||
await loadRouteSession()
|
||||
initialLoadFinished.value = true
|
||||
})
|
||||
|
||||
watch([() => appStore.connected, initialLoadFinished], async ([connected, initialized], [, wasInitialized]) => {
|
||||
if (!connected || !initialized) return
|
||||
if (!wasInitialized && chatStore.sessionsLoaded) return
|
||||
await Promise.all([profilesStore.fetchProfiles(), settingsStore.fetchSettings(), appStore.loadModels()])
|
||||
if (chatStore.isLoadingSessions) return
|
||||
if (!chatStore.sessionsLoaded) await loadRouteSession()
|
||||
else await chatStore.refreshSessionListOnly(chatStore.sessionProfileFilter)
|
||||
})
|
||||
|
||||
watch([routeSessionId, routeProfile], async ([sessionId]) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ChatPanel from '@/components/hermes/chat/ChatPanel.vue'
|
||||
import { useAppStore } from '@/stores/hermes/app'
|
||||
@@ -24,9 +24,11 @@ const routeProfile = computed(() => {
|
||||
return typeof value === 'string' && value.trim() ? value : null
|
||||
})
|
||||
|
||||
const initialLoadFinished = ref(false)
|
||||
|
||||
async function loadRouteSession() {
|
||||
await chatStore.loadSessions(chatStore.sessionProfileFilter, routeSessionId.value)
|
||||
if (routeSessionId.value && chatStore.activeSessionId !== routeSessionId.value) {
|
||||
const loaded = await chatStore.loadSessions(chatStore.sessionProfileFilter, routeSessionId.value)
|
||||
if (loaded && routeSessionId.value && chatStore.activeSessionId !== routeSessionId.value) {
|
||||
await router.replace({ name: 'hermes.globalAgent' })
|
||||
}
|
||||
}
|
||||
@@ -39,6 +41,16 @@ onMounted(async () => {
|
||||
settingsStore.fetchSettings(),
|
||||
])
|
||||
await loadRouteSession()
|
||||
initialLoadFinished.value = true
|
||||
})
|
||||
|
||||
watch([() => appStore.connected, initialLoadFinished], async ([connected, initialized], [, wasInitialized]) => {
|
||||
if (!connected || !initialized || chatStore.runtimeMode !== 'global_agent') return
|
||||
if (!wasInitialized && chatStore.sessionsLoaded) return
|
||||
await Promise.all([profilesStore.fetchProfiles(), settingsStore.fetchSettings(), appStore.loadModels()])
|
||||
if (chatStore.isLoadingSessions) return
|
||||
if (!chatStore.sessionsLoaded) await loadRouteSession()
|
||||
else await chatStore.refreshSessionListOnly(chatStore.sessionProfileFilter)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -332,6 +332,7 @@ async function openDefaultHistorySession(replace = false) {
|
||||
}
|
||||
|
||||
async function syncRouteSession() {
|
||||
if (!hermesSessionsLoaded.value) return
|
||||
const sessionId = routeSessionId.value
|
||||
if (!sessionId) return
|
||||
|
||||
@@ -379,6 +380,13 @@ onUnmounted(() => {
|
||||
window.removeEventListener('hermes:open-page-sidebar', openPageSidebar)
|
||||
})
|
||||
|
||||
watch(() => appStore.connected, async (connected) => {
|
||||
if (!connected || hermesSessionsLoading.value) return
|
||||
await profilesStore.fetchProfiles()
|
||||
await loadHermesSessions()
|
||||
await syncRouteSession()
|
||||
})
|
||||
|
||||
watch([routeSessionId, routeProfile], async ([sessionId]) => {
|
||||
if (!sessionId) {
|
||||
historySessionId.value = null
|
||||
|
||||
@@ -2147,14 +2147,15 @@ function nodeColor(node: { data: WorkflowAgentNodeData }) {
|
||||
@press-enter="submitCreateWorkflow"
|
||||
/>
|
||||
</label>
|
||||
<label class="workflow-field">
|
||||
<div class="workflow-field">
|
||||
<span class="workflow-field-label">{{ t('workflow.profile') }}</span>
|
||||
<a-select
|
||||
:aria-label="t('workflow.profile')"
|
||||
v-model="createWorkflowProfile"
|
||||
:options="workflowProfileOptions"
|
||||
:loading="profilesStore.loading"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="workflow-field">
|
||||
<span class="workflow-field-label">{{ t('workflow.workspace.select') }}</span>
|
||||
<FolderPicker v-model="createWorkflowWorkspace" />
|
||||
|
||||
Reference in New Issue
Block a user