Repository navigation
refactor: split trigger #7714
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: split trigger #7714
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| <script setup lang="ts"> | ||
| import { computed, nextTick, ref, useTemplateRef } from 'vue' | ||
| import type ResourceTriggerApi from '@/api/admin/workspace/trigger/resource-trigger' | ||
| import type ApplicationApi from '@/api/admin/workspace/application/application' | ||
| import type SystemResourceApplicationApi from '@/api/admin/system/resource-management/application/application' | ||
| import type { ApplicationDetail, ResourceTrigger, ResourceTriggerResource, TriggerSetting } from '@/api/types' | ||
| import { RESOURCE_TYPE, TRIGGER_TYPE, SCHEDULE_TYPE } from '@/api/enums' | ||
| import { MsgSuccess } from '@/utils/message' | ||
| import TriggerFormDrawer from '@/views/trigger/trigger-form/TriggerFormDrawer.vue' | ||
|
|
||
| const props = defineProps<{ | ||
| api: typeof ResourceTriggerApi | ||
| applicationApi?: typeof ApplicationApi | typeof SystemResourceApplicationApi | ||
| application: ApplicationDetail | ||
| }>() | ||
| const emit = defineEmits<{ closed: [] }>() | ||
| const resource = computed<ResourceTriggerResource>(() => ({ | ||
| source_type: RESOURCE_TYPE.APPLICATION, | ||
| source_id: props.application.id, | ||
| // Action 在打开弹窗前已校验所属工作空间。 | ||
| workspace_id: props.application.workspace_id!, | ||
| })) | ||
|
|
||
| /* 当前资源的触发器列表 */ | ||
| const visible = ref(false) | ||
| const loading = ref(false) | ||
| const triggersData = ref<ResourceTrigger[]>([]) | ||
| function loadTriggers() { | ||
| loading.value = true | ||
| return props.api | ||
| .getResourceTriggerList(resource.value) | ||
| .then((result) => { | ||
| triggersData.value = result | ||
| }) | ||
| .catch(() => {}) | ||
| .finally(() => { | ||
| loading.value = false | ||
| }) | ||
| } | ||
| function open() { | ||
| visible.value = true | ||
| return loadTriggers() | ||
| } | ||
|
|
||
| function getScheduleLabel(setting: TriggerSetting) { | ||
| const times = setting.time?.join('、') || '-' | ||
| switch (setting.schedule_type) { | ||
| case SCHEDULE_TYPE.DAILY: | ||
| return `每日/${times}` | ||
| case SCHEDULE_TYPE.WEEKLY: { | ||
| const weekdays = ['日', '一', '二', '三', '四', '五', '六'] | ||
| return `每周${setting.days?.map((day) => weekdays[Number(day) % 7]).join('、') || '-'}/${times}` | ||
| } | ||
| case SCHEDULE_TYPE.MONTHLY: | ||
| return `每月${setting.days?.join('、') || '-'}日/${times}` | ||
| case SCHEDULE_TYPE.INTERVAL: | ||
| return `每隔${setting.interval_value ?? '-'}${setting.interval_unit === 'hours' ? '小时' : '分钟'}` | ||
| case SCHEDULE_TYPE.CRON: | ||
| return setting.cron_expression || '-' | ||
| default: | ||
| return '-' | ||
| } | ||
| } | ||
|
|
||
| /* 创建、编辑复用同一表单抽屉,关闭后释放 */ | ||
| const formMounted = ref(false) | ||
| const triggerFormRef = useTemplateRef<InstanceType<typeof TriggerFormDrawer>>('triggerFormRef') | ||
| function handleOpenForm(triggerId?: string) { | ||
| if (loading.value) return | ||
| formMounted.value = true | ||
| return nextTick(() => triggerFormRef.value?.open(triggerId)) | ||
| } | ||
| function handleFormClosed() { | ||
| formMounted.value = false | ||
| } | ||
| function handleRemoveTrigger(trigger: ResourceTrigger) { | ||
| if (loading.value) return | ||
| loading.value = true | ||
| return props.api | ||
| .deleteResourceTrigger(resource.value, trigger.id) | ||
| .then(() => { | ||
| MsgSuccess('移除成功') | ||
| return loadTriggers() | ||
| }) | ||
| .finally(() => { | ||
| loading.value = false | ||
| }) | ||
| } | ||
| defineExpose({ open }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <MkDialog class="resource-trigger-dialog" v-model="visible" @closed="emit('closed')"> | ||
| <template #header> | ||
| <div class="flex-between"> | ||
| <h4>触发器</h4> | ||
| <div class="flex-align-center"> | ||
| <!-- 添加触发器 --> | ||
| <el-button text :disabled="loading" @click="handleOpenForm()" class="text-N900!"> | ||
| <MkIcon name="icon_add_outlined" /> | ||
| <span>添加</span> | ||
| </el-button> | ||
| <el-divider direction="vertical" class="ml-3! mr-4!" /> | ||
| </div> | ||
| </div> | ||
| </template> | ||
| <div v-loading="loading" class="min-h-30"> | ||
| <div class="space-y-2"> | ||
| <template v-for="trigger in triggersData" :key="trigger.id"> | ||
| <el-card shadow="never" class="small"> | ||
| <div class="flex-between gap-3"> | ||
| <div class="flex-align-center min-w-0 flex-1 gap-2"> | ||
| <TriggerIcon :type="trigger.trigger_type" :size="20" class="shrink-0" /> | ||
| <span class="truncate" :title="trigger.name">{{ trigger.name }}</span> | ||
| </div> | ||
| <span v-if="trigger.trigger_type === TRIGGER_TYPE.SCHEDULED" class="text-N600" :title="getScheduleLabel(trigger.trigger_setting)"> | ||
| {{ getScheduleLabel(trigger.trigger_setting) }} | ||
| </span> | ||
| <div class="flex-align-center shrink-0"> | ||
| <!-- 编辑触发器 --> | ||
|
|
||
| <el-button text :disabled="loading" @click="handleOpenForm(trigger.id)"><MkIcon name="icon_edit_outlined" /></el-button> | ||
|
|
||
| <!-- 移除当前资源的触发器 --> | ||
|
|
||
| <el-button text :disabled="loading" @click="handleRemoveTrigger(trigger)"><MkIcon name="icon_close_outlined" /></el-button> | ||
| </div> | ||
| </div> | ||
| </el-card> | ||
| </template> | ||
| </div> | ||
| <MkEmpty v-if="!triggersData.length" /> | ||
| </div> | ||
| </MkDialog> | ||
| <TriggerFormDrawer | ||
| v-if="formMounted" | ||
| ref="triggerFormRef" | ||
| :resource="resource" | ||
| :resource-api="api" | ||
| :application-api="applicationApi" | ||
| @refresh="loadTriggers" | ||
| @closed="handleFormClosed" | ||
| /> | ||
| </template> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| <script setup lang="ts"> | ||
| import { nextTick, ref, useTemplateRef } from 'vue' | ||
| import type ResourceTriggerApi from '@/api/admin/workspace/trigger/resource-trigger' | ||
| import type ApplicationApi from '@/api/admin/workspace/application/application' | ||
| import type SystemResourceApplicationApi from '@/api/admin/system/resource-management/application/application' | ||
| import type { ApplicationDetail } from '@/api/types' | ||
| import { MsgError } from '@/utils/message' | ||
| import ApplicationTriggerDialog from './ApplicationTriggerDialog.vue' | ||
|
|
||
| const props = defineProps<{ | ||
| api: typeof ResourceTriggerApi | ||
| applicationApi?: typeof ApplicationApi | typeof SystemResourceApplicationApi | ||
| application: ApplicationDetail | ||
| label: string | ||
| }>() | ||
|
|
||
| /* 点击后挂载资源触发器列表。 */ | ||
| const dialogMounted = ref(false) | ||
| const resourceTriggerRef = useTemplateRef<InstanceType<typeof ApplicationTriggerDialog>>('resourceTriggerRef') | ||
| function handleOpenTriggers() { | ||
| if (!props.application.workspace_id) { | ||
| MsgError('智能体缺少所属工作空间信息') | ||
| return | ||
| } | ||
| dialogMounted.value = true | ||
| return nextTick(() => resourceTriggerRef.value?.open()) | ||
| } | ||
| function handleDialogClosed() { | ||
| dialogMounted.value = false | ||
| } | ||
| </script> | ||
|
|
||
| <template> | ||
| <!-- 查看智能体触发器 --> | ||
| <MkDropdownItem @click="handleOpenTriggers" :disabled="!application.is_publish"> | ||
| <template #icon><MkIcon name="icon-laser" /></template> | ||
| <span>{{ label }}</span> | ||
| </MkDropdownItem> | ||
| <ApplicationTriggerDialog | ||
| v-if="dialogMounted" | ||
| ref="resourceTriggerRef" | ||
| :api="api" | ||
| :application-api="applicationApi" | ||
| :application="application" | ||
| @closed="handleDialogClosed" | ||
| /> | ||
| </template> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -186,7 +186,13 @@ onMounted(() => { | |
| <!-- 资源授权 --> | ||
| <AuthorizeApplicationAction v-if="perm.application.system.auth()" label="资源授权" :application="row" /> | ||
| <!-- 智能体触发器 --> | ||
| <TriggerApplicationAction v-if="perm.application.system.triggerRead()" label="触发器" :api="SystemResourceTriggerApi" :application="row" /> | ||
| <TriggerApplicationAction | ||
| v-if="perm.application.system.triggerRead()" | ||
| label="触发器" | ||
| :api="SystemResourceTriggerApi" | ||
| :application-api="SystemResourceApplicationApi" | ||
| :application="row" | ||
| /> | ||
| <!-- MCP 服务配置 --> | ||
| <McpConfigApplicationAction v-if="perm.application.system.mcpConfig()" label="MCP 服务配置" :application="row" /> | ||
| <!-- 查看关联资源 --> | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on the shown diff only, the template syntax itself looks valid. The main points to verify are around the new Potential issues
Optimization suggestions
Overall: the diff is syntactically fine, but the key thing to check is that |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review
Correctness / potential bugs
Falsy check on
workspace_idIf
workspace_idcan legitimately be0, this wrongly blocks. Prefer an explicit null check:if (props.application.workspace_id == null).is_publishtruthiness fordisabledFine if it's a real boolean. If the API can return
0/1or"0"/"1", this is buggy ("0"is truthy). Consider normalizing (Boolean(application.is_publish)) or typing it strictly. Also the property name is grammatically odd —is_publishedreads better.Inconsistent return from
handleOpenTriggersOne path returns
undefined, the other returns the promise fromnextTick. This tripsconsistent-returnand is a bit misleading:Silently swallowing a failed open
resourceTriggerRef.value?.open()does nothing if the ref isn't resolved yet. IfnextTickever isn't enough (async child setup, async component, teleport delay), the click appears to do nothing. Safer alternatives:open-on-mount/v-model:visibleprop instead of imperativeopen()+nextTick, orawait nextTick(); if (!resourceTriggerRef.value) { MsgError(...) }.State not reset on unexpected unmount
dialogMountedis only reset via@closed. If the child is destroyed by an ancestor (e.g., the dropdown popper is torn down while the dialog is open and the dialog is not teleported),closednever fires and the flag staystrue. ConfirmApplicationTriggerDialogrenders into a teleport/body. If not, this component's placement as a sibling of a dropdown item is fragile — the whole dialog could unmount with the menu.Double-click race
Two rapid clicks before
nextTickresolves will callopen()twice. Usually harmless, but guard ifopen()isn't idempotent.Type-safety issues
typeof SomeApias a prop type is likely wrongIf
ResourceTriggerApiis a class,typeof Xis the constructor type, not an instance. You probably wantInstanceType<typeof ResourceTriggerApi>(the same applies toapplicationApi). If it's a plain object/namespace export, ignore this.InstanceType<typeof ApplicationTriggerDialog>only exposesdefineExposed membersWith
<script setup>,InstanceTypedoes not include internal methods. Make sure the child actually doesdefineExpose({ open }), otherwiseresourceTriggerRef.value?.open()is a type error or a runtime no-op. A cleaner approach is to declare an explicitexport interface ApplicationTriggerDialogExpose { open(): void }and useuseTemplateRef<ApplicationTriggerDialogExpose>('resourceTriggerRef').useTemplateRefrequires Vue ≥ 3.5. Not a problem if you're already on it, but worth confirming for the project's declared peer version.UX / a11y
Performance / optimization
useTemplateRef.) This is the biggest win here.v-model-driven visibility instead of thev-if+nextTick+ imperativeopen()dance. Keeping the component mounted but its content lazy (<ApplicationTriggerDialog v-model:open="open" />where the inner dialog usesv-ifon its body) removes the timing dependency entirely and is easier to reason about.Style nits
dialogMounted→isDialogMounted/showTriggerDialogmatches common Vue naming./* 点击后挂载资源触发器列表。 */documents what, not why; the lazy-mount rationale would be more useful.propsis only used insidesetup, not the template — you could destructure withconst { application } = toRefs(props)if you prefer, but the current form is fine.