diff --git a/admin/assets/css/main.css b/admin/assets/css/main.css
index 7d9adaf..09bd7ff 100644
--- a/admin/assets/css/main.css
+++ b/admin/assets/css/main.css
@@ -387,6 +387,17 @@ main {
background: var(--accent-dim);
}
+.icon-btn:disabled {
+ opacity: 0.35;
+ cursor: not-allowed;
+}
+
+.icon-btn:disabled:hover {
+ color: var(--muted);
+ border-color: var(--border);
+ background: transparent;
+}
+
.icon-btn.danger:hover {
color: var(--bad);
border-color: var(--bad);
diff --git a/admin/components/RouteCard.vue b/admin/components/RouteCard.vue
index 8d0eed5..b246606 100644
--- a/admin/components/RouteCard.vue
+++ b/admin/components/RouteCard.vue
@@ -19,6 +19,22 @@
{{ t("route.fallback") }}
+
+
@@ -67,11 +83,12 @@ import { fmtMatch } from "~/types";
const { t } = useI18n();
-const props = defineProps<{ route: Route }>();
+const props = defineProps<{ route: Route; atFirst?: boolean; atLast?: boolean }>();
const emit = defineEmits<{
(e: "toggle", route: Route): void;
(e: "edit", route: Route): void;
(e: "delete", route: Route): void;
+ (e: "move", route: Route, dir: -1 | 1): void;
}>();
function onToggle(event: Event): void {
diff --git a/admin/composables/useI18n.ts b/admin/composables/useI18n.ts
index 17ea390..77b6fc3 100644
--- a/admin/composables/useI18n.ts
+++ b/admin/composables/useI18n.ts
@@ -37,6 +37,8 @@ const en: Dict = {
"route.chat": "CHAT",
"route.topic": "TOPIC",
"route.fallback": "fallback",
+ "route.moveUp": "Move up",
+ "route.moveDown": "Move down",
"filter.event": "Event",
"filter.repo": "Repo",
"filter.actor": "Actor",
@@ -70,7 +72,7 @@ const en: Dict = {
"routeEditor.langHint": "en or zh; custom via KV i18n:",
"routeEditor.enabled": "Route enabled",
"routeEditor.fallback": "Fallback route",
- "routeEditor.fallbackHint": "Only fires when no other route matched; its filters are ignored",
+ "routeEditor.fallbackHint": "Only fires when no other route matched; filters still apply",
"routeEditor.filters": "Filters",
"routeEditor.filtersNote": "(all must match · AND)",
"routeEditor.matchPlaceholder": "match value",
@@ -192,6 +194,8 @@ const zh: Dict = {
"route.chat": "群组",
"route.topic": "话题",
"route.fallback": "兜底",
+ "route.moveUp": "上移",
+ "route.moveDown": "下移",
"filter.event": "事件",
"filter.repo": "仓库",
"filter.actor": "操作者",
@@ -225,7 +229,7 @@ const zh: Dict = {
"routeEditor.langHint": "en 或 zh;可通过 KV i18n: 自定义",
"routeEditor.enabled": "启用路由",
"routeEditor.fallback": "兜底路由",
- "routeEditor.fallbackHint": "仅当没有其它路由匹配该事件时才发送,其过滤器会被忽略",
+ "routeEditor.fallbackHint": "仅当没有其它路由匹配该事件时才发送,其过滤器仍会生效",
"routeEditor.filters": "过滤器",
"routeEditor.filtersNote": "(全部匹配 · AND)",
"routeEditor.matchPlaceholder": "匹配值",
diff --git a/admin/pages/admin.vue b/admin/pages/admin.vue
index f8f16f5..3712ac8 100644
--- a/admin/pages/admin.vue
+++ b/admin/pages/admin.vue
@@ -56,10 +56,13 @@
v-for="(r, i) in groupRoutes"
:key="r.id"
:route="r"
+ :at-first="i === 0"
+ :at-last="i === groupRoutes.length - 1"
:style="{ animationDelay: i * 45 + 'ms' }"
@toggle="onToggle"
@edit="openEdit"
@delete="onDelete"
+ @move="onMove"
/>
@@ -271,6 +274,22 @@ async function onSave(route: Route): Promise {
}
}
+async function onMove(route: Route, dir: -1 | 1): Promise {
+ const group = selectedGroup.value;
+ if (!group) return;
+ const i = groupRoutes.value.findIndex((r) => r.id === route.id);
+ const j = i + dir;
+ if (i < 0 || j < 0 || j >= groupRoutes.value.length) return;
+ const next = [...groupRoutes.value];
+ [next[i], next[j]] = [next[j]!, next[i]!];
+ try {
+ await saveGroupRoutes(group.id, next);
+ } catch (err) {
+ push(t("toast.saveFailed", { msg: err instanceof Error ? err.message : String(err) }), "bad");
+ loadGroupRoutes(group.id);
+ }
+}
+
async function onToggle(route: Route): Promise {
const group = selectedGroup.value;
if (!group) return;