Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ defineExpose({ open })
<div class="flex-align-center shrink-0">
<!-- 编辑触发器 -->

<el-button v-if="applicationTriggerPermission('triggerEdit')" text :disabled="loading" @click="handleOpenForm(trigger.id)"><MkIcon name="icon_edit_outlined" /></el-button>
<el-button text :disabled="loading" @click="handleOpenForm(trigger.id)"><MkIcon name="icon_edit_outlined" /></el-button>

<!-- 移除当前资源的触发器 -->

Expand All @@ -148,5 +148,6 @@ defineExpose({ open })
:application-api="applicationApi"
@refresh="loadTriggers"
@closed="handleFormClosed"
:can-edit="applicationTriggerPermission('triggerEdit')"
/>
</template>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is an analysis of the provided code snippet, focusing on correctness, best practices, and potential issues.

1. Vue Directive Order / : vs v-bind

  • Issue: In the added line :can-edit="applicationTriggerPermission('triggerEdit')", you are using shorthand binding (:) but placed it at the end of a list that already includes both static props and event handlers. While syntactically valid in Vue 3, mixing @event and :prop without clear grouping can reduce readability.
  • Suggestion: Consistency in prop ordering is preferred but not critical. However, ensure :can-edit is a valid prop for the component (not covered here).

2. Permission Logic Shift

  • Change: The v-if="applicationTriggerPermission('triggerEdit')" check was removed from the button and moved into a :can-edit prop passed to a child component.
  • Risk:
    • If the child component does not respect the can-edit prop (e.g., ignores it when rendering internal buttons), users without permission might still see interactive elements or trigger unintended actions.
    • If the child component does use can-edit correctly, this is a good refactoring as it centralizes permission logic inside the child.
  • Recommendation:
    • Confirm that the child component (likely TriggerFormDialog or similar) properly evaluates props.canEdit before enabling editing controls.
    • Consider whether hiding the entire edit-triggering flow at the parent level (v-if) is preferable for performance/UX consistency if the child always shows some UI regardless.

3. Removal of v-if vs. Prop-based Control

  • Original: Parent controlled visibility entirely via v-if. If unauthorized, no element rendered.
  • New: Parent always renders the button (or passes control down). This may cause flicker or unnecessary DOM nodes if permissions change dynamically.
  • Optimization: If applicationTriggerPermission() is expensive or asynchronous, consider caching the result or using a computed property to avoid recalculating on every render.

4. Naming Convention

  • :can-edit is kebab-case, which is correct for Vue templates. Ensure the corresponding prop in the child component is defined as either canEdit (camelCase in script/setup) or explicitly declared with hyphenated name if needed (Vue handles conversion automatically).

5. General Best Practice

  • Computed Property: If applicationTriggerPermission() is called multiple times in the template (as it likely is now in two places: previously button, now prop), wrap it in a computed property for efficiency:
const hasEditPermission = computed(() => applicationTriggerPermission('triggerEdit'));

Then use :can-edit="hasEditPermission".

✅ Suggested Improved Code Snippet:

<!-- Template -->
<el-button text :disabled="loading" @click="handleOpenForm(trigger.id)">
  <MkIcon name="icon_edit_outlined" />
</el-button>

<!-- Child Component -->
<TriggerFormDialog
  :visible="dialogVisible"
  :data="currentTrigger"
  :application-api="applicationApi"
  :can-edit="hasEditPermission" <!-- Use computed -->
  @refresh="loadTriggers"
  @closed="handleFormClosed"
/>

<!-- Script Setup -->
<script setup>
import { computed } from 'vue';

// Assuming applicationTriggerPermission is imported
const hasEditPermission = computed(() => applicationTriggerPermission('triggerEdit'));

defineExpose({ open });
</script>

Summary of Actions:

  1. Verify child component respects :can-edit.
  2. Use computed property for permission checks to avoid repeated function calls.
  3. Ensure no UX degradation occurs due to always-rendering the edit button (if it should be hidden, keep v-if).

Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ defineExpose({ open })
<div class="flex-align-center shrink-0">
<!-- 编辑触发器 -->

<el-button v-if="toolTriggerPermission('triggerEdit')" text :disabled="loading" @click="handleOpenForm(trigger.id)"><MkIcon name="icon_edit_outlined" /></el-button>
<el-button text :disabled="loading" @click="handleOpenForm(trigger.id)"><MkIcon name="icon_edit_outlined" /></el-button>

<!-- 移除当前资源的触发器 -->

Expand All @@ -151,5 +151,6 @@ defineExpose({ open })
:resource-api="api"
@refresh="loadTriggers"
@closed="handleFormClosed"
:can-edit="toolTriggerPermission('triggerEdit')"
/>
</template>
4 changes: 2 additions & 2 deletions ui/src/views/trigger/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ onMounted(() => loadTriggers())

<div class="flex">
<!-- 编辑当前触发器 -->
<MkTooltip content="编辑" placement="top" v-if="perm.trigger.edit()">
<MkTooltip content="编辑" placement="top">
<el-button type="primary" text @click.stop="handleOpenTriggerDrawer(row)">
<MkIcon name="icon_edit_outlined" />
</el-button>
Expand Down Expand Up @@ -265,5 +265,5 @@ onMounted(() => loadTriggers())
</template>
</MkViewLayout>
<TriggerTaskRecordDrawer ref="recordDrawerRef" />
<TriggerFormDrawer ref="triggerDrawerRef" @refresh="loadTriggers" />
<TriggerFormDrawer :can-edit="perm.trigger.edit()" ref="triggerDrawerRef" @refresh="loadTriggers" />
</template>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the analysis of the provided code snippets, focusing on security, logic consistency, and UI/UX implications.

1. Critical Security & Logic Issue: Permission Enforcement Moved to Child Component

The most significant change is moving the permission check v-if="perm.trigger.edit() from the parent (the button rendering in the table) to a prop (:can-edit="...") passed to the child component <TriggerFormDrawer>.

  • UI Inconsistency: Previously, if a user lacked edit permissions, the "Edit" button was hidden entirely. Now, the button will always be visible. The permission check only affects behavior inside the drawer (presumably disabling form fields there). This leads to a confusing UX where a user can click "Edit," open a drawer, but potentially have all fields disabled or receive an error upon saving if the backend also enforces permissions.
  • Security Risk (UI Level): Exposing UI elements that are non-functional or partially functional due to permissions is generally considered poor practice unless explicitly designed as such (e.g., showing a lock icon with a tooltip "No Permission").
  • Recommendation: Keep the v-if (or disabled state) on the button itself. If you want to pass permission status to the child for internal validation, do so, but do not remove the visibility/enablement check at the trigger point.
<!-- RECOMMENDED CHANGE -->
<MkTooltip 
  content="编辑" 
  placement="top" 
  v-if="perm.trigger.edit()"> <!-- Keep this -->
  <el-button 
    type="primary" text 
    @click.stop="handleOpenTriggerDrawer(row)">
    <MkIcon name="icon_edit_outlined" />
  </el-button>
</MkTooltip>

<!-- Then pass permission to child if needed for fine-grained control -->
<TriggerFormDrawer 
  :can-edit="perm.trigger.edit()" 
  ref="triggerDrawerRef" 
  @refresh="loadTriggers" />

2. Potential Prop Naming & Type Safety

  • Missing Prop Definition Check: Ensure that TriggerFormDrawer actually defines can-edit (or canEdit) as a prop. If it doesn’t, this attribute will be ignored or treated as a DOM attribute, causing potential bugs.
  • Naming Convention: Vue props are typically camelCase in JavaScript (canEdit) and kebab-case in templates (:can-edit). This is correct here, but ensure the component script matches defineProps({ canEdit: Boolean }).

3. Unused Import / Variable Warning (Contextual)

Check if perm is still used elsewhere in <script setup> after this change. If perm.trigger.edit() is now only used in the template (for the prop binding), ensure the perm object is still accessible in the template scope. Most modern Vue setups handle this automatically via useXxx() composables, but verify no unused imports exist.

4. Performance Consideration

  • Function Call in Template: perm.trigger.edit() is called directly in the template (:can-edit="perm.trigger.edit()").
    • If perm.trigger.edit() is a cheap function (just reading a boolean/object property), this is fine.
    • If it involves complex computation or API calls, consider using a computed property to cache the result:
      const canEdit = computed(() => perm.trigger.edit());
      Then use :can-edit="canEdit" in the template.

Summary of Recommendations

Issue Severity Action
Button always visible even without permission 🔴 High Restore v-if="perm.trigger.edit()" on the <MkTooltip>/<el-button>.
Permission logic split between parent and child 🟡 Medium Decide on a consistent strategy: either hide/disable at parent level, or disable inside child. Prefer parent-level control for entry points.
Computed property optimization 🟢 Low Use computed for perm.trigger.edit() if it’s non-trivial.

Fixed Code Example

<!-- Template -->
<div class="flex">
  <!-- Edit current trigger -->
  <MkTooltip content="编辑" placement="top" v-if="perm.trigger.edit()">
    <el-button type="primary" text @click.stop="handleOpenTriggerDrawer(row)">
      <MkIcon name="icon_edit_outlined" />
    </el-button>
  </MkTooltip>
  
  <!-- ... other buttons ... -->
</div>

<!-- ... -->
<TriggerTaskRecordDrawer ref="recordDrawerRef" />
<TriggerFormDrawer 
  :can-edit="perm.trigger.edit()" 
  ref="triggerDrawerRef" 
  @refresh="loadTriggers" />

5 changes: 3 additions & 2 deletions ui/src/views/trigger/trigger-form/TriggerFormDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ const props = withDefaults(
applicationApi?: typeof ApplicationApi | typeof SystemResourceApplicationApi
toolApi?: typeof ToolApi | typeof SystemToolApi
toolWorkflowApi?: typeof WorkflowApi
canEdit?: boolean
}>(),
{ resourceApi: () => ResourceTriggerApi, applicationApi: () => ApplicationApi, toolApi: () => ToolApi, toolWorkflowApi: () => WorkflowApi },
{ resourceApi: () => ResourceTriggerApi, applicationApi: () => ApplicationApi, toolApi: () => ToolApi, toolWorkflowApi: () => WorkflowApi, canEdit: true },
)
const emit = defineEmits<{ refresh: []; closed: [] }>()
/* 抽屉生命周期与详情 */
Expand Down Expand Up @@ -432,7 +433,7 @@ defineExpose({ open })
<!-- 取消触发器编辑 -->
<el-button plain :disabled="saving" @click="visible = false">取消</el-button>
<!-- 保存触发器配置 -->
<el-button type="primary" :loading="saving" :disabled="loading || detailFailed" @click="handleSave">
<el-button v-if="canEdit" type="primary" :loading="saving" :disabled="loading || detailFailed" @click="handleSave">
{{ editingId ? '保存' : '创建' }}
</el-button>
</template>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a review of the provided code diff with identified issues, potential problems, and optimization suggestions.

1. Critical Issue: Prop Default Syntax

Location: { ..., canEdit: true }

The syntax canEdit: true is incorrect for Vue 3 withDefaults.

  • Primitive values (like booleans) should not be wrapped in a factory function, but they must align with the expected signature.
    However, more importantly, boolean primitives without a function wrapper are generally accepted, but let's check the strictness.
    Actually, looking closely at Vue's source code and common patterns:
  • For functions: () => Value
  • For non-functions (primitives like true, false, strings, numbers): You can pass them directly or wrap them in a function depending on version/team style.
  • BUT: In defineProps + withDefaults, if the prop type is boolean, passing true directly is technically allowed but inconsistent with the other defaults which use arrow functions (() => ResourceTriggerApi).
  • Risk: If canEdit was declared as Function or an object/array, it would need a factory. Since it's boolean, true works. However, standard practice favors consistency. More importantly, some linters or strict TypeScript interfaces might complain if not explicitly typed as boolean.

✅ Recommendation: Ensure consistency. While true works for booleans, many teams prefer explicit factory functions for all props to avoid subtle bugs if types change later (e.g., if someone changes canEdit from boolean to object). However, strictly speaking, canEdit: true is valid for boolean primitives.

2. UI/UX Logic Flaw: Missing "Cancel" Button Conditionality

Location:

<el-button plain :disabled="saving" @click="visible = false">取消</el-button>
<el-button v-if="canEdit" type="primary" ... >Save/Create</el-button>

If canEdit is false, the "Save" button is hidden, but the "Cancel" button remains visible.

  • This results in a dialog/drawer that only has a "Cancel" button.
  • This might confuse users who expect a way to close the window (e.g., pressing Esc, clicking outside). If clicking outside is disabled, they are trapped unless they click "Cancel".
  • ✅ Suggestion: If editing is forbidden, consider hiding the entire footer template or changing the "Cancel" button text to "Close". Alternatively, ensure visible = false is triggered by other means (close icon on drawer).

3. Accessibility & Semantics: v-if vs Loading State

When canEdit is false, the Save button disappears entirely (v-if). This causes layout shift if the button was previously loading/rendering.
✅ Optimization: If the layout must remain stable, use v-show instead of v-if for the Save button if the space should be preserved (though for actions buttons, v-if is fine). More often, you'd want to disable rather than hide if the user needs to know why they can't edit, but hiding is acceptable if it's a pure viewer mode.

4. Type Safety & Best Practices

Location: Props definition

canEdit?: boolean

And default:

canEdit: true

This defaults to true. This means if the parent component doesn't explicitly pass can-edit, it will be true.

  • Context: Since this is likely a shared trigger application component, defaulting to true implies write access by default. This is security-sensitive.
  • ⚠️ Risk: Accidentally allowing edits in read-only contexts if the parent forgets to bind :can-edit="false".
  • ✅ Recommendation: Consider defaulting to false (safer) unless this component is overwhelmingly used in editable contexts. Let the parent explicitly opt-in to editing.

5. Minor Code Style

  • The comment /* 抽屉生命周期与详情 */ is unrelated to the diff but present in context. No issue.
  • The Chinese strings (取消, 保存, 创建) assume a hardcoded locale. If i18n is required elsewhere, these should be moved to translation keys, even if currently hardcoded.

✅ Final Recommended Code

@@ -28,8 +28,9 @@ const props = withDefaults(
     applicationApi?: typeof ApplicationApi | typeof SystemResourceApplicationApi
     toolApi?: typeof ToolApi | typeof SystemToolApi
     toolWorkflowApi?: typeof WorkflowApi
     canEdit?: boolean // Consider making this mandatory if no unsafe default
   }>(),
-  { resourceApi: () => ResourceTriggerApi, applicationApi: () => ApplicationApi, toolApi: () => ToolApi, toolWorkflowApi: () => WorkflowApi },
+  { resourceApi: () => ResourceTriggerApi, applicationApi: () => ApplicationApi, toolApi: () => ToolApi, toolWorkflowApi: () => WorkflowApi, canEdit: true }, 
  /* NOTE: 'canEdit: true' is valid for primitives, but ensure your team linting accepts it. 
     For consistency, some prefer canEdit: () => true, though unnecessary for boolean. */
 )

@@ -432,7 +433,7 @@ defineExpose({ open })
       <!-- Cancel trigger edit -->
       <el-button plain :disabled="saving" @click="visible = false">
         <!-- Optional: Dynamically change text based on editability -->
         {{ canEdit ? '取消' : '关闭' }}
       </el-button>
       
       <!-- Save trigger configuration -->
-      <el-button type="primary" :loading="saving" :disabled="loading || detailFailed" @click="handleSave">
+      <el-button v-if="canEdit" type="primary" :loading="saving" :disabled="loading || detailFailed" @click="handleSave">
         {{ editingId ? '保存' : '创建' }}
       </el-button>
     </template>

Summary of Actions:

  1. Syntax Check: canEdit: true is syntactically correct for boolean primitives in withDefaults. No fix needed unless your linter enforces factory functions for all props (rare for booleans).
  2. UX Improvement: Changing "Cancel" text to "Close" when !canEdit improves clarity.
  3. Security Review: Verify if defaulting canEdit to true is intended. Safer default is false.

Expand Down
Loading