236 lines
9.0 KiB
Vue
236 lines
9.0 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from "vue";
|
|
import { useI18n } from "vue-i18n";
|
|
import { CheckSquare, Loader2, Search, Square } from "@lucide/vue";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { useConnectionStore } from "@/stores/connectionStore";
|
|
import { normalizeVisibleSchemaSelection } from "@/lib/database/visibleDatabases";
|
|
import * as api from "@/lib/backend/api";
|
|
|
|
const props = defineProps<{
|
|
open: boolean;
|
|
connectionId: string;
|
|
connectionName: string;
|
|
database: string;
|
|
/** Draft mode: parent provides schemas/loading/error externally. Component emits events instead of calling store. */
|
|
draftMode?: boolean;
|
|
draftSchemaNames?: string[];
|
|
draftInitialSelection?: string[];
|
|
draftLoading?: boolean;
|
|
draftError?: string;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
"update:open": [value: boolean];
|
|
"draft:save": [selectedNames: string[]];
|
|
"draft:showAll": [];
|
|
}>();
|
|
|
|
const { t } = useI18n();
|
|
const connectionStore = useConnectionStore();
|
|
|
|
const schemaNames = ref<string[]>([]);
|
|
const selectedNames = ref<Set<string>>(new Set());
|
|
const searchText = ref("");
|
|
const isLoading = ref(false);
|
|
const errorMessage = ref("");
|
|
|
|
const isDraftMode = computed(() => props.draftMode);
|
|
const isLoadingState = computed(() => (isDraftMode.value ? props.draftLoading : isLoading.value));
|
|
const errorMessageState = computed(() => (isDraftMode.value ? props.draftError || "" : errorMessage.value));
|
|
|
|
const filteredSchemaNames = computed(() => {
|
|
const query = searchText.value.trim().toLowerCase();
|
|
const names = isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value;
|
|
if (!query) return names;
|
|
return names.filter((name) => name.toLowerCase().includes(query));
|
|
});
|
|
const selectedCount = computed(() => selectedNames.value.size);
|
|
const totalCount = computed(() => {
|
|
const names = isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value;
|
|
return names.length;
|
|
});
|
|
const canSaveSelection = computed(() => selectedNames.value.size > 0);
|
|
|
|
watch(
|
|
() => props.open,
|
|
(open) => {
|
|
if (!open) return;
|
|
if (isDraftMode.value) {
|
|
initDraftMode();
|
|
} else {
|
|
loadSchemas().catch(() => {});
|
|
}
|
|
},
|
|
);
|
|
|
|
function initDraftMode() {
|
|
searchText.value = "";
|
|
const names = props.draftSchemaNames || [];
|
|
const configured = props.draftInitialSelection;
|
|
if (configured && configured.length > 0) {
|
|
const visibleSet = new Set(normalizeVisibleSchemaSelection(configured, names));
|
|
selectedNames.value = new Set(names.filter((name) => visibleSet.has(name)));
|
|
} else {
|
|
selectedNames.value = new Set(names);
|
|
}
|
|
}
|
|
|
|
async function loadSchemas() {
|
|
isLoading.value = true;
|
|
errorMessage.value = "";
|
|
searchText.value = "";
|
|
try {
|
|
const config = connectionStore.getConfig(props.connectionId);
|
|
const database = props.database || config?.database || "";
|
|
await connectionStore.ensureConnected(props.connectionId);
|
|
const names = await api.listSchemas(props.connectionId, database);
|
|
schemaNames.value = names;
|
|
const configured = config?.visible_schemas?.[database || ""];
|
|
if (Array.isArray(configured)) {
|
|
const visibleSet = new Set(normalizeVisibleSchemaSelection(configured, names));
|
|
selectedNames.value = new Set(names.filter((name) => visibleSet.has(name)));
|
|
} else {
|
|
selectedNames.value = new Set(names);
|
|
}
|
|
} catch (e: any) {
|
|
schemaNames.value = [];
|
|
selectedNames.value = new Set();
|
|
errorMessage.value = String(e?.message || e);
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function toggleSchema(schema: string) {
|
|
const next = new Set(selectedNames.value);
|
|
if (next.has(schema)) next.delete(schema);
|
|
else next.add(schema);
|
|
selectedNames.value = next;
|
|
}
|
|
|
|
const isSearching = computed(() => searchText.value.trim().length > 0);
|
|
|
|
function selectAll() {
|
|
const names = isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value;
|
|
selectedNames.value = new Set(names);
|
|
}
|
|
|
|
function selectFiltered() {
|
|
const next = new Set(selectedNames.value);
|
|
for (const name of filteredSchemaNames.value) {
|
|
next.add(name);
|
|
}
|
|
selectedNames.value = next;
|
|
}
|
|
|
|
function clearSelection() {
|
|
selectedNames.value = new Set();
|
|
}
|
|
|
|
async function showAllSchemas() {
|
|
if (isDraftMode.value) {
|
|
selectedNames.value = new Set(props.draftSchemaNames || []);
|
|
emit("draft:showAll");
|
|
emit("update:open", false);
|
|
return;
|
|
}
|
|
const config = connectionStore.getConfig(props.connectionId);
|
|
const database = props.database || config?.database || "";
|
|
if (config?.visible_schemas?.[database || ""]) {
|
|
await connectionStore.clearVisibleSchemas(props.connectionId, database);
|
|
}
|
|
selectedNames.value = new Set(schemaNames.value);
|
|
emit("update:open", false);
|
|
}
|
|
|
|
async function saveSelection() {
|
|
if (!canSaveSelection.value) return;
|
|
const names = isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value;
|
|
const normalized = normalizeVisibleSchemaSelection([...selectedNames.value], names);
|
|
if (isDraftMode.value) {
|
|
emit("draft:save", normalized);
|
|
emit("update:open", false);
|
|
return;
|
|
}
|
|
const config = connectionStore.getConfig(props.connectionId);
|
|
const database = props.database || config?.database || "";
|
|
await connectionStore.setVisibleSchemas(props.connectionId, database, normalized);
|
|
emit("update:open", false);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Dialog :open="open" @update:open="(value: boolean) => emit('update:open', value)">
|
|
<DialogContent class="sm:max-w-[460px]">
|
|
<DialogHeader>
|
|
<DialogTitle>{{ t("visibleSchemas.title") }}</DialogTitle>
|
|
<p class="text-sm text-muted-foreground">
|
|
{{ t("visibleSchemas.description", { connection: connectionName }) }}
|
|
</p>
|
|
</DialogHeader>
|
|
|
|
<div class="flex items-center gap-2 rounded-md border bg-background px-2">
|
|
<Search class="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
<Input v-model="searchText" :placeholder="t('visibleSchemas.searchPlaceholder')" class="h-8 border-0 px-0 shadow-none focus-visible:ring-0" :disabled="isLoadingState || !!errorMessageState" />
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
|
<span>{{ t("visibleSchemas.selectedCount", { selected: selectedCount, total: totalCount }) }}</span>
|
|
<div class="flex items-center gap-2">
|
|
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingState" @click="selectAll">
|
|
{{ t("visibleSchemas.selectAll") }}
|
|
</button>
|
|
<button v-if="isSearching" class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingState" @click="selectFiltered">
|
|
{{ t("visibleSchemas.selectFiltered") }}
|
|
</button>
|
|
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingState" @click="clearSelection">
|
|
{{ t("visibleSchemas.clear") }}
|
|
</button>
|
|
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingState" @click="showAllSchemas">
|
|
{{ t("visibleSchemas.showAll") }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p v-if="!isLoadingState && !errorMessageState && !canSaveSelection" class="text-xs text-destructive">
|
|
{{ t("visibleSchemas.emptySelection") }}
|
|
</p>
|
|
|
|
<div class="h-72 overflow-y-auto rounded-md border bg-background/50 p-1">
|
|
<div v-if="isLoadingState" class="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground">
|
|
<Loader2 class="h-4 w-4 animate-spin" />
|
|
{{ t("common.loading") }}
|
|
</div>
|
|
<div v-else-if="errorMessageState" class="p-3 text-sm text-destructive">
|
|
{{ t("visibleSchemas.loadFailed", { message: errorMessageState }) }}
|
|
</div>
|
|
<div v-else-if="!filteredSchemaNames.length" class="p-3 text-sm text-muted-foreground">
|
|
{{ t("grid.noSearchResults") }}
|
|
</div>
|
|
<template v-else>
|
|
<button
|
|
v-for="schema in filteredSchemaNames"
|
|
:key="schema"
|
|
type="button"
|
|
class="flex h-8 w-full min-w-0 items-center gap-2 rounded-sm px-2 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none"
|
|
@click="toggleSchema(schema)"
|
|
>
|
|
<CheckSquare v-if="selectedNames.has(schema)" class="h-4 w-4 shrink-0 text-primary" />
|
|
<Square v-else class="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
<span class="truncate">{{ schema }}</span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" @click="emit('update:open', false)">{{ t("dangerDialog.cancel") }}</Button>
|
|
<Button :disabled="isLoadingState || !!errorMessageState || !canSaveSelection" @click="saveSelection">
|
|
{{ t("visibleSchemas.save") }}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template>
|