mirror of
https://github.com/unraid/api.git
synced 2026-01-21 16:09:39 -06:00
fix: context menu
This commit is contained in:
@@ -44,6 +44,10 @@ const emit = defineEmits<{
|
||||
e: 'row:select',
|
||||
payload: { id: string; type: string; name: string; selected: boolean; meta?: T }
|
||||
): void;
|
||||
(
|
||||
e: 'row:contextmenu',
|
||||
payload: { id: string; type: string; name: string; meta?: T; event: MouseEvent }
|
||||
): void;
|
||||
(e: 'row:drop', payload: DropEvent<T>): void;
|
||||
(e: 'update:selectedIds', value: string[]): void;
|
||||
}>();
|
||||
@@ -147,7 +151,7 @@ function toArray(value: unknown | unknown[]): unknown[] {
|
||||
return value !== undefined && value !== null ? [value] : [];
|
||||
}
|
||||
|
||||
function getRowSearchValues<T>(row: TreeRow<T>): string[] {
|
||||
function getRowSearchValues(row: TreeRow<T>): string[] {
|
||||
const values: unknown[] = [];
|
||||
|
||||
if (props.searchableKeys?.length) {
|
||||
@@ -178,7 +182,7 @@ function getRowSearchValues<T>(row: TreeRow<T>): string[] {
|
||||
.filter((str) => str.trim().length);
|
||||
}
|
||||
|
||||
function rowMatchesTerm<T>(row: TreeRow<T>, term: string): boolean {
|
||||
function rowMatchesTerm(row: TreeRow<T>, term: string): boolean {
|
||||
if (!term) return true;
|
||||
|
||||
return getRowSearchValues(row)
|
||||
@@ -186,7 +190,7 @@ function rowMatchesTerm<T>(row: TreeRow<T>, term: string): boolean {
|
||||
.some((value) => value.includes(term));
|
||||
}
|
||||
|
||||
function filterRowsByTerm<T>(rows: TreeRow<T>[], term: string): TreeRow<T>[] {
|
||||
function filterRowsByTerm(rows: TreeRow<T>[], term: string): TreeRow<T>[] {
|
||||
if (!term) {
|
||||
return rows;
|
||||
}
|
||||
@@ -284,6 +288,18 @@ function wrapCellWithRow(row: { original: TreeRow<T>; depth?: number }, cellCont
|
||||
}
|
||||
: undefined,
|
||||
onDragend: props.enableDragDrop ? handleDragEnd : undefined,
|
||||
onContextmenu: (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (
|
||||
target &&
|
||||
target.closest('input,button,textarea,a,[role=checkbox],[role=button],[data-stop-row-click]')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
const r = row.original;
|
||||
emit('row:contextmenu', { id: r.id, type: r.type, name: r.name, meta: r.meta, event: e });
|
||||
},
|
||||
},
|
||||
[cellContent]
|
||||
);
|
||||
@@ -428,7 +444,7 @@ defineExpose({
|
||||
<slot
|
||||
name="toolbar"
|
||||
:selected-count="selectedCount"
|
||||
:global-filter="globalFilter.value"
|
||||
:global-filter="globalFilter"
|
||||
:column-visibility="columnVisibility"
|
||||
:row-selection="rowSelection"
|
||||
:set-global-filter="setGlobalFilter"
|
||||
|
||||
@@ -12,6 +12,7 @@ import DockerOverview from '@/components/Docker/Overview.vue';
|
||||
import DockerPreview from '@/components/Docker/Preview.vue';
|
||||
|
||||
import type { DockerContainer, FlatOrganizerEntry } from '@/composables/gql/graphql';
|
||||
import type { LocationQueryRaw } from 'vue-router';
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
@@ -97,7 +98,7 @@ if (hasRouter) {
|
||||
const currentRouteId = normalizeContainerQuery(route!.query[ROUTE_QUERY_KEY]);
|
||||
if (nextId === currentRouteId) return;
|
||||
|
||||
const nextQuery = { ...route!.query } as Record<string, unknown>;
|
||||
const nextQuery: LocationQueryRaw = { ...route!.query };
|
||||
if (nextId) nextQuery[ROUTE_QUERY_KEY] = nextId;
|
||||
else delete nextQuery[ROUTE_QUERY_KEY];
|
||||
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import DockerContainerManagement from '@/components/Docker/DockerContainerManagement.vue';
|
||||
import DockerContainerOverview from '@/components/Docker/DockerContainerOverview.vue';
|
||||
import DockerEdit from '@/components/Docker/Edit.vue';
|
||||
import DockerLogs from '@/components/Docker/Logs.vue';
|
||||
import DockerOverview from '@/components/Docker/Overview.vue';
|
||||
import DockerPreview from '@/components/Docker/Preview.vue';
|
||||
|
||||
const item = { id: '1', label: 'Test', icon: 'test', badge: '1' };
|
||||
const details = {
|
||||
network: 'bridge',
|
||||
lanIpPort: '192.168.1.100:8080',
|
||||
containerIp: '192.168.1.100',
|
||||
uptime: '2 hours',
|
||||
containerPort: '8080',
|
||||
creationDate: new Date().toISOString(),
|
||||
containerId: '1234567890',
|
||||
maintainer: 'John Doe',
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="grid gap-8">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h, ref, resolveComponent, watch } from 'vue';
|
||||
import { computed, h, nextTick, ref, resolveComponent, watch } from 'vue';
|
||||
import { useMutation } from '@vue/apollo-composable';
|
||||
|
||||
import BaseTreeTable from '@/components/Common/BaseTreeTable.vue';
|
||||
@@ -51,6 +51,10 @@ const UDropdownMenu = resolveComponent('UDropdownMenu');
|
||||
const UModal = resolveComponent('UModal');
|
||||
const USkeleton = resolveComponent('USkeleton') as Component;
|
||||
const UIcon = resolveComponent('UIcon');
|
||||
const rowActionDropdownUi = {
|
||||
content: 'overflow-x-hidden z-50',
|
||||
item: 'bg-transparent hover:bg-transparent focus:bg-transparent border-0 ring-0 outline-none shadow-none data-[state=checked]:bg-transparent',
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'created-folder'): void;
|
||||
@@ -113,7 +117,7 @@ const containersRef = computed(() => props.containers);
|
||||
|
||||
const rootFolderId = computed<string>(() => props.rootFolderId || 'root');
|
||||
|
||||
const baseTableRef = ref<InstanceType<typeof BaseTreeTable> | null>(null);
|
||||
const baseTableRef = ref<{ columnVisibility?: { value: Record<string, boolean> } } | null>(null);
|
||||
|
||||
const searchableKeys = ['name', 'state', 'ports', 'autoStart', 'updates', 'containerId'];
|
||||
|
||||
@@ -137,7 +141,12 @@ const dockerSearchAccessor = (row: TreeRow<DockerContainer>): unknown[] => {
|
||||
|
||||
const columnVisibilityFallback = ref<Record<string, boolean>>({});
|
||||
const columnVisibility = computed({
|
||||
get: () => baseTableRef.value?.columnVisibility?.value ?? columnVisibilityFallback.value,
|
||||
get: (): Record<string, boolean> => {
|
||||
if (baseTableRef.value?.columnVisibility) {
|
||||
return baseTableRef.value.columnVisibility.value;
|
||||
}
|
||||
return columnVisibilityFallback.value;
|
||||
},
|
||||
set: (value: Record<string, boolean>) => {
|
||||
if (baseTableRef.value?.columnVisibility) {
|
||||
baseTableRef.value.columnVisibility.value = value;
|
||||
@@ -265,10 +274,7 @@ const columns = computed<TableColumn<TreeRow<DockerContainer>>[]>(() => {
|
||||
{
|
||||
items,
|
||||
size: 'md',
|
||||
ui: {
|
||||
content: 'overflow-x-hidden z-50',
|
||||
item: 'bg-transparent hover:bg-transparent focus:bg-transparent border-0 ring-0 outline-none shadow-none data-[state=checked]:bg-transparent',
|
||||
},
|
||||
ui: rowActionDropdownUi,
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
@@ -309,6 +315,26 @@ watch(
|
||||
type ActionDropdownItem = { label: string; icon?: string; onSelect?: (e?: Event) => void; as?: string };
|
||||
type DropdownMenuItems = ActionDropdownItem[][];
|
||||
|
||||
const contextMenuState = ref<{
|
||||
open: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
items: DropdownMenuItems;
|
||||
rowId: string | null;
|
||||
}>({
|
||||
open: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
items: [] as DropdownMenuItems,
|
||||
rowId: null,
|
||||
});
|
||||
|
||||
const contextMenuPopper = {
|
||||
strategy: 'fixed' as const,
|
||||
placement: 'bottom-start' as const,
|
||||
offset: 4,
|
||||
};
|
||||
|
||||
const columnsMenuItems = computed<DropdownMenuItems>(() => {
|
||||
const keysFromColumns = (columns.value || [])
|
||||
.map((col: TableColumn<TreeRow<DockerContainer>>) => {
|
||||
@@ -358,8 +384,8 @@ const { mutate: unpauseContainerMutation } = useMutation(UNPAUSE_DOCKER_CONTAINE
|
||||
declare global {
|
||||
interface Window {
|
||||
toast?: {
|
||||
success: (title: string, options?: { description?: string }) => void;
|
||||
error?: (title: string, options?: { description?: string }) => void;
|
||||
success: (title: string, options: { description?: string }) => void;
|
||||
error?: (title: string, options: { description?: string }) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -506,6 +532,36 @@ function handleRowAction(row: TreeRow<DockerContainer>, action: string) {
|
||||
showToast(`${action}: ${row.name}`);
|
||||
}
|
||||
|
||||
async function handleRowContextMenu(payload: {
|
||||
id: string;
|
||||
type: string;
|
||||
name: string;
|
||||
meta?: DockerContainer;
|
||||
event: MouseEvent;
|
||||
}) {
|
||||
payload.event.preventDefault();
|
||||
payload.event.stopPropagation();
|
||||
const row = getRowById(payload.id, treeData.value);
|
||||
if (!row) return;
|
||||
if (busyRowIds.value.has(row.id)) return;
|
||||
|
||||
const items = getRowActionItems(row as TreeRow<DockerContainer>);
|
||||
if (!items.length) return;
|
||||
|
||||
contextMenuState.value = {
|
||||
open: false,
|
||||
x: payload.event.clientX,
|
||||
y: payload.event.clientY,
|
||||
items,
|
||||
rowId: row.id,
|
||||
};
|
||||
|
||||
await nextTick();
|
||||
if (contextMenuState.value.rowId === row.id) {
|
||||
contextMenuState.value.open = true;
|
||||
}
|
||||
}
|
||||
|
||||
const bulkItems = computed<DropdownMenuItems>(() => [
|
||||
[
|
||||
{
|
||||
@@ -604,6 +660,7 @@ function getRowActionItems(row: TreeRow<DockerContainer>): DropdownMenuItems {
|
||||
containerId: payload.meta?.id,
|
||||
})
|
||||
"
|
||||
@row:contextmenu="handleRowContextMenu"
|
||||
@row:select="
|
||||
(payload) =>
|
||||
emit('row:select', {
|
||||
@@ -652,6 +709,19 @@ function getRowActionItems(row: TreeRow<DockerContainer>): DropdownMenuItems {
|
||||
</template>
|
||||
</BaseTreeTable>
|
||||
|
||||
<UDropdownMenu
|
||||
v-model:open="contextMenuState.open"
|
||||
:items="contextMenuState.items"
|
||||
size="md"
|
||||
:popper="contextMenuPopper"
|
||||
:ui="rowActionDropdownUi"
|
||||
>
|
||||
<div
|
||||
class="fixed h-px w-px"
|
||||
:style="{ top: `${contextMenuState.y}px`, left: `${contextMenuState.x}px` }"
|
||||
/>
|
||||
</UDropdownMenu>
|
||||
|
||||
<UModal
|
||||
v-model:open="folderOps.moveOpen"
|
||||
title="Move to folder"
|
||||
|
||||
@@ -3,22 +3,21 @@ import { ref } from 'vue';
|
||||
import { ContainerState } from '@/composables/gql/graphql';
|
||||
|
||||
import type { TreeRow } from '@/composables/useTreeData';
|
||||
import type { MutateFunction } from '@vue/apollo-composable';
|
||||
import type { DocumentNode } from 'graphql';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
interface MutationOptions {
|
||||
refetchQueries?: unknown[];
|
||||
awaitRefetchQueries?: boolean;
|
||||
}
|
||||
type ContainerMutationFn = MutateFunction<unknown, { id: string }>;
|
||||
|
||||
export interface ContainerActionOptions<T = unknown> {
|
||||
getRowById: (id: string, rows: TreeRow<T>[]) => TreeRow<T> | undefined;
|
||||
treeData: Ref<TreeRow<T>[]>;
|
||||
setRowsBusy: (ids: string[], busy: boolean) => void;
|
||||
startMutation: (args: { id: string }, options?: MutationOptions) => Promise<unknown>;
|
||||
stopMutation: (args: { id: string }, options?: MutationOptions) => Promise<unknown>;
|
||||
pauseMutation: (args: { id: string }, options?: MutationOptions) => Promise<unknown>;
|
||||
unpauseMutation: (args: { id: string }, options?: MutationOptions) => Promise<unknown>;
|
||||
refetchQuery: { query: unknown; variables: { skipCache: boolean } };
|
||||
startMutation: ContainerMutationFn;
|
||||
stopMutation: ContainerMutationFn;
|
||||
pauseMutation: ContainerMutationFn;
|
||||
unpauseMutation: ContainerMutationFn;
|
||||
refetchQuery: { query: DocumentNode; variables: { skipCache: boolean } };
|
||||
onSuccess?: (message: string) => void;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user