Repository navigation
fix: workspace trigger error - #7790
Conversation
| <el-button v-if="canEdit" type="primary" :loading="saving" :disabled="loading || detailFailed" @click="handleSave"> | ||
| {{ editingId ? '保存' : '创建' }} | ||
| </el-button> | ||
| </template> |
There was a problem hiding this comment.
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 isboolean, passingtruedirectly is technically allowed but inconsistent with the other defaults which use arrow functions (() => ResourceTriggerApi). - Risk: If
canEditwas declared asFunctionor an object/array, it would need a factory. Since it'sboolean,trueworks. However, standard practice favors consistency. More importantly, some linters or strict TypeScript interfaces might complain if not explicitly typed asboolean.
✅ 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 = falseis 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?: booleanAnd default:
canEdit: trueThis 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
trueimplies 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:
- Syntax Check:
canEdit: trueis syntactically correct for boolean primitives inwithDefaults. No fix needed unless your linter enforces factory functions for all props (rare for booleans). - UX Improvement: Changing "Cancel" text to "Close" when
!canEditimproves clarity. - Security Review: Verify if defaulting
canEdittotrueis intended. Safer default isfalse.
| <TriggerTaskRecordDrawer ref="recordDrawerRef" /> | ||
| <TriggerFormDrawer ref="triggerDrawerRef" @refresh="loadTriggers" /> | ||
| <TriggerFormDrawer :can-edit="perm.trigger.edit()" ref="triggerDrawerRef" @refresh="loadTriggers" /> | ||
| </template> |
There was a problem hiding this comment.
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(ordisabledstate) 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
TriggerFormDraweractually definescan-edit(orcanEdit) 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 matchesdefineProps({ 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
computedproperty to cache the result:Then useconst canEdit = computed(() => perm.trigger.edit());
:can-edit="canEdit"in the template.
- If
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" />| @closed="handleFormClosed" | ||
| :can-edit="applicationTriggerPermission('triggerEdit')" | ||
| /> | ||
| </template> |
There was a problem hiding this comment.
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@eventand:propwithout clear grouping can reduce readability. - Suggestion: Consistency in prop ordering is preferred but not critical. However, ensure
:can-editis 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-editprop passed to a child component. - Risk:
- If the child component does not respect the
can-editprop (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-editcorrectly, this is a good refactoring as it centralizes permission logic inside the child.
- If the child component does not respect the
- Recommendation:
- Confirm that the child component (likely
TriggerFormDialogor similar) properly evaluatesprops.canEditbefore 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.
- Confirm that the child component (likely
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-editis kebab-case, which is correct for Vue templates. Ensure the corresponding prop in the child component is defined as eithercanEdit(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:
- Verify child component respects
:can-edit. - Use computed property for permission checks to avoid repeated function calls.
- Ensure no UX degradation occurs due to always-rendering the edit button (if it should be hidden, keep
v-if).
fix: workspace trigger error