feat(ui): add connection context menu with edit/delete/duplicate
Right-click connections for Connect, Edit, Duplicate, Delete. Right-click groups for New Connection, Rename, Delete. ConnectionEditDialog provides a full form for creating/editing connections with protocol toggle, group selector, color labels, and tags. Generic ContextMenu component positions at cursor with viewport edge detection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ae8127f33d
commit
95c2368ab5
97
frontend/src/components/common/ContextMenu.vue
Normal file
97
frontend/src/components/common/ContextMenu.vue
Normal file
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 z-50"
|
||||
@click="close"
|
||||
@contextmenu.prevent="close"
|
||||
>
|
||||
<div
|
||||
ref="menuRef"
|
||||
class="fixed bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl py-1 min-w-[160px] overflow-hidden"
|
||||
:style="{ left: position.x + 'px', top: position.y + 'px' }"
|
||||
@click.stop
|
||||
>
|
||||
<template v-for="(item, idx) in items" :key="idx">
|
||||
<!-- Separator -->
|
||||
<div v-if="item.separator" class="my-1 border-t border-[#30363d]" />
|
||||
|
||||
<!-- Menu item -->
|
||||
<button
|
||||
v-else
|
||||
class="w-full flex items-center gap-2.5 px-3 py-1.5 text-xs text-left transition-colors cursor-pointer"
|
||||
:class="item.danger
|
||||
? 'text-[var(--wraith-accent-red)] hover:bg-[var(--wraith-accent-red)]/10'
|
||||
: 'text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)]'
|
||||
"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<span v-if="item.icon" class="w-4 h-4 flex items-center justify-center shrink-0" v-html="item.icon" />
|
||||
<span class="flex-1">{{ item.label }}</span>
|
||||
<span v-if="item.shortcut" class="text-[10px] text-[var(--wraith-text-muted)]">{{ item.shortcut }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from "vue";
|
||||
|
||||
export interface ContextMenuItem {
|
||||
label?: string;
|
||||
icon?: string;
|
||||
shortcut?: string;
|
||||
danger?: boolean;
|
||||
separator?: boolean;
|
||||
action?: () => void;
|
||||
}
|
||||
|
||||
const visible = ref(false);
|
||||
const position = ref({ x: 0, y: 0 });
|
||||
const items = ref<ContextMenuItem[]>([]);
|
||||
const menuRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
function open(event: MouseEvent, menuItems: ContextMenuItem[]): void {
|
||||
items.value = menuItems;
|
||||
visible.value = true;
|
||||
|
||||
// Position at cursor, adjusting if near viewport edges
|
||||
nextTick(() => {
|
||||
const menu = menuRef.value;
|
||||
if (!menu) {
|
||||
position.value = { x: event.clientX, y: event.clientY };
|
||||
return;
|
||||
}
|
||||
|
||||
let x = event.clientX;
|
||||
let y = event.clientY;
|
||||
|
||||
const menuWidth = menu.offsetWidth;
|
||||
const menuHeight = menu.offsetHeight;
|
||||
|
||||
if (x + menuWidth > window.innerWidth) {
|
||||
x = window.innerWidth - menuWidth - 4;
|
||||
}
|
||||
if (y + menuHeight > window.innerHeight) {
|
||||
y = window.innerHeight - menuHeight - 4;
|
||||
}
|
||||
|
||||
position.value = { x, y };
|
||||
});
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function handleClick(item: ContextMenuItem): void {
|
||||
if (item.action) {
|
||||
item.action();
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
defineExpose({ open, close, visible });
|
||||
</script>
|
||||
292
frontend/src/components/connections/ConnectionEditDialog.vue
Normal file
292
frontend/src/components/connections/ConnectionEditDialog.vue
Normal file
@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
@click.self="close"
|
||||
@keydown.esc="close"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black/50" @click="close" />
|
||||
|
||||
<!-- Dialog -->
|
||||
<div class="relative w-full max-w-md bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[#30363d]">
|
||||
<h3 class="text-sm font-semibold text-[var(--wraith-text-primary)]">
|
||||
{{ isEditing ? "Edit Connection" : "New Connection" }}
|
||||
</h3>
|
||||
<button
|
||||
class="text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||
@click="close"
|
||||
>
|
||||
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.749.749 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.749.749 0 1 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="px-4 py-4 space-y-3 max-h-[60vh] overflow-y-auto">
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Name</label>
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
placeholder="My Server"
|
||||
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Hostname & Port -->
|
||||
<div class="flex gap-3">
|
||||
<div class="flex-1">
|
||||
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Hostname</label>
|
||||
<input
|
||||
v-model="form.hostname"
|
||||
type="text"
|
||||
placeholder="192.168.1.1"
|
||||
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-24">
|
||||
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Port</label>
|
||||
<input
|
||||
v-model.number="form.port"
|
||||
type="number"
|
||||
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Protocol -->
|
||||
<div>
|
||||
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Protocol</label>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="flex-1 py-2 text-sm rounded border transition-colors cursor-pointer"
|
||||
:class="form.protocol === 'ssh'
|
||||
? 'bg-[#3fb950]/10 border-[#3fb950] text-[#3fb950]'
|
||||
: 'bg-[#0d1117] border-[#30363d] text-[var(--wraith-text-muted)] hover:border-[var(--wraith-text-secondary)]'
|
||||
"
|
||||
@click="setProtocol('ssh')"
|
||||
>
|
||||
SSH
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 py-2 text-sm rounded border transition-colors cursor-pointer"
|
||||
:class="form.protocol === 'rdp'
|
||||
? 'bg-[#1f6feb]/10 border-[#1f6feb] text-[#1f6feb]'
|
||||
: 'bg-[#0d1117] border-[#30363d] text-[var(--wraith-text-muted)] hover:border-[var(--wraith-text-secondary)]'
|
||||
"
|
||||
@click="setProtocol('rdp')"
|
||||
>
|
||||
RDP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group -->
|
||||
<div>
|
||||
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Group</label>
|
||||
<select
|
||||
v-model="form.groupId"
|
||||
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors cursor-pointer"
|
||||
>
|
||||
<option :value="null">No Group</option>
|
||||
<option v-for="group in connectionStore.groups" :key="group.id" :value="group.id">
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div>
|
||||
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Tags (comma-separated)</label>
|
||||
<input
|
||||
v-model="tagsInput"
|
||||
type="text"
|
||||
placeholder="Prod, Linux, Web"
|
||||
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Color -->
|
||||
<div>
|
||||
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Color Label</label>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="color in colorOptions"
|
||||
:key="color.value"
|
||||
class="w-6 h-6 rounded-full border-2 transition-transform cursor-pointer hover:scale-110"
|
||||
:class="form.color === color.value ? 'border-white scale-110' : 'border-transparent'"
|
||||
:style="{ backgroundColor: color.hex }"
|
||||
:title="color.label"
|
||||
@click="form.color = color.value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div>
|
||||
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Notes</label>
|
||||
<textarea
|
||||
v-model="form.notes"
|
||||
rows="3"
|
||||
placeholder="Optional notes about this connection..."
|
||||
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-2 px-4 py-3 border-t border-[#30363d]">
|
||||
<button
|
||||
class="px-3 py-1.5 text-xs text-[var(--wraith-text-secondary)] hover:text-[var(--wraith-text-primary)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
|
||||
@click="close"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-xs text-white bg-[#238636] hover:bg-[#2ea043] rounded transition-colors cursor-pointer"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': !isValid }"
|
||||
:disabled="!isValid"
|
||||
@click="save"
|
||||
>
|
||||
{{ isEditing ? "Save Changes" : "Create" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { useConnectionStore, type Connection } from "@/stores/connection.store";
|
||||
|
||||
interface ConnectionForm {
|
||||
name: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
protocol: "ssh" | "rdp";
|
||||
groupId: number | null;
|
||||
color: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
const visible = ref(false);
|
||||
const isEditing = ref(false);
|
||||
const editingId = ref<number | null>(null);
|
||||
const tagsInput = ref("");
|
||||
|
||||
const form = ref<ConnectionForm>({
|
||||
name: "",
|
||||
hostname: "",
|
||||
port: 22,
|
||||
protocol: "ssh",
|
||||
groupId: null,
|
||||
color: "",
|
||||
notes: "",
|
||||
});
|
||||
|
||||
const colorOptions = [
|
||||
{ value: "", label: "None", hex: "#30363d" },
|
||||
{ value: "red", label: "Red", hex: "#f85149" },
|
||||
{ value: "orange", label: "Orange", hex: "#d29922" },
|
||||
{ value: "green", label: "Green", hex: "#3fb950" },
|
||||
{ value: "blue", label: "Blue", hex: "#58a6ff" },
|
||||
{ value: "purple", label: "Purple", hex: "#bc8cff" },
|
||||
{ value: "pink", label: "Pink", hex: "#f778ba" },
|
||||
];
|
||||
|
||||
const isValid = computed(() => {
|
||||
return form.value.name.trim() !== "" && form.value.hostname.trim() !== "" && form.value.port > 0;
|
||||
});
|
||||
|
||||
function setProtocol(protocol: "ssh" | "rdp"): void {
|
||||
form.value.protocol = protocol;
|
||||
if (protocol === "ssh" && form.value.port === 3389) {
|
||||
form.value.port = 22;
|
||||
} else if (protocol === "rdp" && form.value.port === 22) {
|
||||
form.value.port = 3389;
|
||||
}
|
||||
}
|
||||
|
||||
function openNew(groupId?: number): void {
|
||||
isEditing.value = false;
|
||||
editingId.value = null;
|
||||
form.value = {
|
||||
name: "",
|
||||
hostname: "",
|
||||
port: 22,
|
||||
protocol: "ssh",
|
||||
groupId: groupId ?? null,
|
||||
color: "",
|
||||
notes: "",
|
||||
};
|
||||
tagsInput.value = "";
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(conn: Connection): void {
|
||||
isEditing.value = true;
|
||||
editingId.value = conn.id;
|
||||
form.value = {
|
||||
name: conn.name,
|
||||
hostname: conn.hostname,
|
||||
port: conn.port,
|
||||
protocol: conn.protocol,
|
||||
groupId: conn.groupId,
|
||||
color: "",
|
||||
notes: "",
|
||||
};
|
||||
tagsInput.value = conn.tags?.join(", ") ?? "";
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
if (!isValid.value) return;
|
||||
|
||||
const tags = tagsInput.value
|
||||
.split(",")
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t.length > 0);
|
||||
|
||||
if (isEditing.value && editingId.value !== null) {
|
||||
// TODO: Replace with Wails binding — ConnectionService.UpdateConnection(id, input)
|
||||
const conn = connectionStore.connections.find((c) => c.id === editingId.value);
|
||||
if (conn) {
|
||||
conn.name = form.value.name;
|
||||
conn.hostname = form.value.hostname;
|
||||
conn.port = form.value.port;
|
||||
conn.protocol = form.value.protocol;
|
||||
conn.groupId = form.value.groupId ?? conn.groupId;
|
||||
conn.tags = tags;
|
||||
}
|
||||
} else {
|
||||
// TODO: Replace with Wails binding — ConnectionService.CreateConnection(input)
|
||||
const newId = Math.max(...connectionStore.connections.map((c) => c.id), 0) + 1;
|
||||
connectionStore.connections.push({
|
||||
id: newId,
|
||||
name: form.value.name,
|
||||
hostname: form.value.hostname,
|
||||
port: form.value.port,
|
||||
protocol: form.value.protocol,
|
||||
groupId: form.value.groupId ?? 1,
|
||||
tags,
|
||||
});
|
||||
}
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
defineExpose({ openNew, openEdit, close, visible });
|
||||
</script>
|
||||
@ -7,6 +7,7 @@
|
||||
<button
|
||||
class="w-full flex items-center gap-1.5 px-3 py-1.5 text-xs hover:bg-[var(--wraith-bg-tertiary)] transition-colors cursor-pointer"
|
||||
@click="toggleGroup(group.id)"
|
||||
@contextmenu.prevent="showGroupMenu($event, group)"
|
||||
>
|
||||
<!-- Chevron -->
|
||||
<svg
|
||||
@ -42,6 +43,7 @@
|
||||
:key="conn.id"
|
||||
class="w-full flex items-center gap-2 pl-8 pr-3 py-1.5 text-xs hover:bg-[var(--wraith-bg-tertiary)] transition-colors cursor-pointer"
|
||||
@dblclick="handleConnect(conn)"
|
||||
@contextmenu.prevent="showConnectionMenu($event, conn)"
|
||||
>
|
||||
<!-- Protocol dot -->
|
||||
<span
|
||||
@ -60,17 +62,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Context menu -->
|
||||
<ContextMenu ref="contextMenu" />
|
||||
|
||||
<!-- Edit dialog -->
|
||||
<ConnectionEditDialog ref="editDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useConnectionStore, type Connection } from "@/stores/connection.store";
|
||||
import { useConnectionStore, type Connection, type Group } from "@/stores/connection.store";
|
||||
import { useSessionStore } from "@/stores/session.store";
|
||||
import ContextMenu from "@/components/common/ContextMenu.vue";
|
||||
import type { ContextMenuItem } from "@/components/common/ContextMenu.vue";
|
||||
import ConnectionEditDialog from "@/components/connections/ConnectionEditDialog.vue";
|
||||
|
||||
const connectionStore = useConnectionStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const contextMenu = ref<InstanceType<typeof ContextMenu> | null>(null);
|
||||
const editDialog = ref<InstanceType<typeof ConnectionEditDialog> | null>(null);
|
||||
|
||||
// All groups expanded by default
|
||||
const expandedGroups = ref<Set<number>>(
|
||||
new Set(connectionStore.groups.map((g) => g.id)),
|
||||
@ -88,4 +102,95 @@ function toggleGroup(groupId: number): void {
|
||||
function handleConnect(conn: Connection): void {
|
||||
sessionStore.connect(conn.id);
|
||||
}
|
||||
|
||||
/** Show context menu for a connection. */
|
||||
function showConnectionMenu(event: MouseEvent, conn: Connection): void {
|
||||
const items: ContextMenuItem[] = [
|
||||
{
|
||||
label: "Connect",
|
||||
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25Zm5 1.22v8.06l4.97-4.03L6 3.97Z"/></svg>`,
|
||||
action: () => handleConnect(conn),
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: "Edit",
|
||||
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.25.25 0 0 0-.064.108l-.558 1.953 1.953-.558a.249.249 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z"/></svg>`,
|
||||
action: () => editDialog.value?.openEdit(conn),
|
||||
},
|
||||
{
|
||||
label: "Duplicate",
|
||||
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25ZM5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg>`,
|
||||
action: () => duplicateConnection(conn),
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: "Delete",
|
||||
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M11 1.75V3h2.25a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75ZM4.496 6.675l.66 6.6a.25.25 0 0 0 .249.225h5.19a.25.25 0 0 0 .249-.225l.66-6.6a.75.75 0 0 1 1.492.149l-.66 6.6A1.748 1.748 0 0 1 10.595 15h-5.19a1.75 1.75 0 0 1-1.741-1.575l-.66-6.6a.75.75 0 1 1 1.492-.15ZM6.5 1.75V3h3V1.75a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25Z"/></svg>`,
|
||||
danger: true,
|
||||
action: () => deleteConnection(conn),
|
||||
},
|
||||
];
|
||||
|
||||
contextMenu.value?.open(event, items);
|
||||
}
|
||||
|
||||
/** Show context menu for a group. */
|
||||
function showGroupMenu(event: MouseEvent, group: Group): void {
|
||||
const items: ContextMenuItem[] = [
|
||||
{
|
||||
label: "New Connection",
|
||||
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"/></svg>`,
|
||||
action: () => editDialog.value?.openNew(group.id),
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: "Rename",
|
||||
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.25.25 0 0 0-.064.108l-.558 1.953 1.953-.558a.249.249 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z"/></svg>`,
|
||||
action: () => {
|
||||
// TODO: Replace with Wails binding — ConnectionService.RenameGroup(id, name)
|
||||
const newName = prompt("Rename group:", group.name);
|
||||
if (newName && newName !== group.name) {
|
||||
group.name = newName;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M11 1.75V3h2.25a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75ZM4.496 6.675l.66 6.6a.25.25 0 0 0 .249.225h5.19a.25.25 0 0 0 .249-.225l.66-6.6a.75.75 0 0 1 1.492.149l-.66 6.6A1.748 1.748 0 0 1 10.595 15h-5.19a1.75 1.75 0 0 1-1.741-1.575l-.66-6.6a.75.75 0 1 1 1.492-.15ZM6.5 1.75V3h3V1.75a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25Z"/></svg>`,
|
||||
danger: true,
|
||||
action: () => deleteGroup(group),
|
||||
},
|
||||
];
|
||||
|
||||
contextMenu.value?.open(event, items);
|
||||
}
|
||||
|
||||
/** Duplicate a connection with a new name. */
|
||||
function duplicateConnection(conn: Connection): void {
|
||||
// TODO: Replace with Wails binding — ConnectionService.CreateConnection(input)
|
||||
const newId = Math.max(...connectionStore.connections.map((c) => c.id), 0) + 1;
|
||||
connectionStore.connections.push({
|
||||
...conn,
|
||||
id: newId,
|
||||
name: `${conn.name} (copy)`,
|
||||
});
|
||||
}
|
||||
|
||||
/** Delete a connection after confirmation. */
|
||||
function deleteConnection(conn: Connection): void {
|
||||
// TODO: Replace with Wails binding — ConnectionService.DeleteConnection(id)
|
||||
const idx = connectionStore.connections.findIndex((c) => c.id === conn.id);
|
||||
if (idx !== -1) {
|
||||
connectionStore.connections.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete a group after confirmation. */
|
||||
function deleteGroup(group: Group): void {
|
||||
// TODO: Replace with Wails binding — ConnectionService.DeleteGroup(id)
|
||||
const idx = connectionStore.groups.findIndex((g) => g.id === group.id);
|
||||
if (idx !== -1) {
|
||||
connectionStore.groups.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user