mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-08-31 01:40:25 +08:00
fix: gray out commands of disabled plugins in behavior manager (#9664)
* fix: gray out commands of disabled plugins in behavior manager Show inactive-plugin commands as 未启用, dim the whole row with theme on-surface colors, and disable row actions. Fixes #9562 * fix: address review comments on disabled-plugin commands Keep the stored command enabled flag unchanged and expose plugin activation separately. Allow viewing details, keep hover tooltips, and cover serialized plugin_activated in unit tests. * fix: preserve tooltips for disabled command actions --------- Co-authored-by: Soulter <905617992@qq.com>
This commit is contained in:
@@ -250,6 +250,11 @@ async def list_command_conflicts() -> list[dict[str, Any]]:
|
||||
# Internal helpers ----------------------------------------------------------
|
||||
|
||||
|
||||
def _is_plugin_activated(desc: CommandDescriptor) -> bool:
|
||||
plugin_meta = star_map.get(desc.module_path)
|
||||
return bool(plugin_meta.activated) if plugin_meta else True
|
||||
|
||||
|
||||
def _collect_descriptors(include_sub_commands: bool) -> list[CommandDescriptor]:
|
||||
"""收集指令,按需包含子指令。"""
|
||||
descriptors: list[CommandDescriptor] = []
|
||||
@@ -465,7 +470,7 @@ def _group_conflicts(
|
||||
) -> dict[str, list[CommandDescriptor]]:
|
||||
conflicts: dict[str, list[CommandDescriptor]] = defaultdict(list)
|
||||
for desc in descriptors:
|
||||
if desc.effective_command and desc.enabled:
|
||||
if desc.effective_command and desc.enabled and _is_plugin_activated(desc):
|
||||
conflicts[desc.effective_command].append(desc)
|
||||
return {k: v for k, v in conflicts.items() if len(v) > 1}
|
||||
|
||||
@@ -531,6 +536,7 @@ def _descriptor_to_dict(desc: CommandDescriptor) -> dict[str, Any]:
|
||||
"aliases": desc.aliases,
|
||||
"permission": desc.permission,
|
||||
"enabled": desc.enabled,
|
||||
"plugin_activated": _is_plugin_activated(desc),
|
||||
"is_group": desc.is_group,
|
||||
"has_conflict": desc.has_conflict,
|
||||
"reserved": desc.reserved,
|
||||
|
||||
@@ -65,8 +65,13 @@ const getPermissionLabel = (permission: string): string => {
|
||||
}
|
||||
};
|
||||
|
||||
const isPluginInactive = (cmd: CommandItem) => !cmd.plugin_activated;
|
||||
|
||||
// 获取状态信息
|
||||
const getStatusInfo = (cmd: CommandItem): StatusInfo => {
|
||||
if (isPluginInactive(cmd)) {
|
||||
return { text: tm('status.pluginDisabled'), color: 'default', variant: 'outlined' };
|
||||
}
|
||||
if (cmd.has_conflict) {
|
||||
return { text: tm('status.conflict'), color: 'warning', variant: 'flat' };
|
||||
}
|
||||
@@ -88,6 +93,9 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
if (item.is_group) {
|
||||
classes.push('group-row');
|
||||
}
|
||||
if (isPluginInactive(item)) {
|
||||
classes.push('plugin-inactive-row');
|
||||
}
|
||||
return classes.length > 0 ? { class: classes.join(' ') } : {};
|
||||
};
|
||||
</script>
|
||||
@@ -154,6 +162,7 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
:color="getPermissionColor(item.permission)"
|
||||
size="small"
|
||||
class="font-weight-medium cursor-pointer"
|
||||
:disabled="isPluginInactive(item)"
|
||||
link
|
||||
>
|
||||
{{ getPermissionLabel(item.permission) }}
|
||||
@@ -193,31 +202,37 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-btn-group density="default" variant="text" color="primary">
|
||||
<v-btn
|
||||
v-if="!item.enabled"
|
||||
icon
|
||||
size="small"
|
||||
color="success"
|
||||
@click="emit('toggle-command', item)"
|
||||
>
|
||||
<v-icon size="22">mdi-play</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.enable') }}</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
icon
|
||||
size="small"
|
||||
color="error"
|
||||
@click="emit('toggle-command', item)"
|
||||
>
|
||||
<v-icon size="22">mdi-pause</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.disable') }}</v-tooltip>
|
||||
</v-btn>
|
||||
<span v-if="!item.enabled" class="command-action-tooltip">
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
color="success"
|
||||
:disabled="isPluginInactive(item)"
|
||||
@click="emit('toggle-command', item)"
|
||||
>
|
||||
<v-icon size="22">mdi-play</v-icon>
|
||||
</v-btn>
|
||||
<v-tooltip activator="parent" location="top">{{ isPluginInactive(item) ? tm('tooltips.pluginInactive') : tm('tooltips.enable') }}</v-tooltip>
|
||||
</span>
|
||||
<span v-else class="command-action-tooltip">
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
color="error"
|
||||
:disabled="isPluginInactive(item)"
|
||||
@click="emit('toggle-command', item)"
|
||||
>
|
||||
<v-icon size="22">mdi-pause</v-icon>
|
||||
</v-btn>
|
||||
<v-tooltip activator="parent" location="top">{{ isPluginInactive(item) ? tm('tooltips.pluginInactive') : tm('tooltips.disable') }}</v-tooltip>
|
||||
</span>
|
||||
|
||||
<v-btn icon size="small" color="warning" @click="emit('rename', item)">
|
||||
<v-icon size="22">mdi-pencil</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.rename') }}</v-tooltip>
|
||||
</v-btn>
|
||||
<span class="command-action-tooltip">
|
||||
<v-btn icon size="small" color="warning" :disabled="isPluginInactive(item)" @click="emit('rename', item)">
|
||||
<v-icon size="22">mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
<v-tooltip activator="parent" location="top">{{ isPluginInactive(item) ? tm('tooltips.pluginInactive') : tm('tooltips.rename') }}</v-tooltip>
|
||||
</span>
|
||||
|
||||
<v-btn icon size="small" @click="emit('view-details', item)">
|
||||
<v-icon size="22">mdi-information</v-icon>
|
||||
@@ -285,5 +300,41 @@ code.sub-command-code {
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
.command-action-tooltip {
|
||||
display: inline-flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-btn-group .command-action-tooltip .v-btn {
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-data-table .plugin-inactive-row,
|
||||
.v-data-table .plugin-inactive-row td,
|
||||
.v-data-table .plugin-inactive-row .v-data-table__td {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.06) !important;
|
||||
color: rgba(var(--v-theme-on-surface), 0.72) !important;
|
||||
}
|
||||
|
||||
.v-data-table .plugin-inactive-row:hover,
|
||||
.v-data-table .plugin-inactive-row:hover td,
|
||||
.v-data-table .plugin-inactive-row:hover .v-data-table__td {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.09) !important;
|
||||
}
|
||||
|
||||
.v-data-table .plugin-inactive-row .v-chip,
|
||||
.v-data-table .plugin-inactive-row code,
|
||||
.v-data-table .plugin-inactive-row .text-body-2,
|
||||
.v-data-table .plugin-inactive-row .text-subtitle-1 {
|
||||
color: rgba(var(--v-theme-on-surface), 0.72) !important;
|
||||
filter: grayscale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.v-data-table .plugin-inactive-row .command-action-tooltip,
|
||||
.v-data-table .plugin-inactive-row .v-chip.cursor-pointer.v-chip--disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface CommandItem {
|
||||
aliases: string[];
|
||||
permission: PermissionType;
|
||||
enabled: boolean;
|
||||
plugin_activated: boolean;
|
||||
is_group: boolean;
|
||||
has_conflict: boolean;
|
||||
reserved: boolean;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"pluginDisabled": "Plugin off",
|
||||
"conflict": "Conflict"
|
||||
},
|
||||
"permission": {
|
||||
@@ -39,7 +40,8 @@
|
||||
"enable": "Enable command",
|
||||
"disable": "Disable command",
|
||||
"rename": "Rename command",
|
||||
"viewDetails": "View details"
|
||||
"viewDetails": "View details",
|
||||
"pluginInactive": "Enable the plugin first"
|
||||
},
|
||||
"dialogs": {
|
||||
"rename": {
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"status": {
|
||||
"enabled": "Активна",
|
||||
"disabled": "Отключена",
|
||||
"pluginDisabled": "Плагин отключён",
|
||||
"conflict": "Конфликт"
|
||||
},
|
||||
"permission": {
|
||||
@@ -39,7 +40,8 @@
|
||||
"enable": "Включить",
|
||||
"disable": "Выключить",
|
||||
"rename": "Переименовать",
|
||||
"viewDetails": "Подробности"
|
||||
"viewDetails": "Подробности",
|
||||
"pluginInactive": "Сначала включите плагин"
|
||||
},
|
||||
"dialogs": {
|
||||
"rename": {
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"status": {
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"pluginDisabled": "未启用",
|
||||
"conflict": "有冲突"
|
||||
},
|
||||
"permission": {
|
||||
@@ -39,7 +40,8 @@
|
||||
"enable": "启用指令",
|
||||
"disable": "禁用指令",
|
||||
"rename": "重命名指令",
|
||||
"viewDetails": "查看详情"
|
||||
"viewDetails": "查看详情",
|
||||
"pluginInactive": "请先启用所属插件"
|
||||
},
|
||||
"dialogs": {
|
||||
"rename": {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Commands from disabled plugins should not look enabled in the dashboard."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from astrbot.core.star.command_management import (
|
||||
CommandDescriptor,
|
||||
_descriptor_to_dict,
|
||||
_group_conflicts,
|
||||
_is_plugin_activated,
|
||||
star_map,
|
||||
)
|
||||
|
||||
|
||||
def _descriptor(module_path: str, *, enabled: bool = True) -> CommandDescriptor:
|
||||
return CommandDescriptor(
|
||||
handler=SimpleNamespace(), # type: ignore[arg-type]
|
||||
module_path=module_path,
|
||||
enabled=enabled,
|
||||
effective_command="demo",
|
||||
)
|
||||
|
||||
|
||||
def test_plugin_activation_is_serialized_without_mutating_enabled():
|
||||
original = dict(star_map)
|
||||
try:
|
||||
star_map.clear()
|
||||
star_map["data.plugins.foo.main"] = SimpleNamespace(activated=True)
|
||||
star_map["data.plugins.bar.main"] = SimpleNamespace(activated=False)
|
||||
|
||||
active = _descriptor("data.plugins.foo.main", enabled=True)
|
||||
inactive = _descriptor("data.plugins.bar.main", enabled=True)
|
||||
unknown = _descriptor("data.plugins.missing.main", enabled=True)
|
||||
|
||||
assert _is_plugin_activated(active) is True
|
||||
assert _is_plugin_activated(inactive) is False
|
||||
assert _is_plugin_activated(unknown) is True
|
||||
assert active.enabled is True
|
||||
assert inactive.enabled is True
|
||||
assert unknown.enabled is True
|
||||
|
||||
active_dict = _descriptor_to_dict(active)
|
||||
inactive_dict = _descriptor_to_dict(inactive)
|
||||
unknown_dict = _descriptor_to_dict(unknown)
|
||||
|
||||
assert active_dict["enabled"] is True
|
||||
assert inactive_dict["enabled"] is True
|
||||
assert unknown_dict["enabled"] is True
|
||||
assert active_dict["plugin_activated"] is True
|
||||
assert inactive_dict["plugin_activated"] is False
|
||||
assert unknown_dict["plugin_activated"] is True
|
||||
finally:
|
||||
star_map.clear()
|
||||
star_map.update(original)
|
||||
|
||||
|
||||
def test_inactive_plugin_commands_are_excluded_from_conflicts():
|
||||
original = dict(star_map)
|
||||
try:
|
||||
star_map.clear()
|
||||
star_map["data.plugins.foo.main"] = SimpleNamespace(activated=True)
|
||||
star_map["data.plugins.bar.main"] = SimpleNamespace(activated=False)
|
||||
|
||||
live = _descriptor("data.plugins.foo.main")
|
||||
off = _descriptor("data.plugins.bar.main")
|
||||
conflicts = _group_conflicts([live, off])
|
||||
|
||||
assert conflicts == {}
|
||||
finally:
|
||||
star_map.clear()
|
||||
star_map.update(original)
|
||||
Reference in New Issue
Block a user