Files
dbx/apps/desktop/src/docs/components/TablePage.vue
T

113 lines
6.1 KiB
Vue

<script setup lang="ts">
import { computed } from "vue";
import type { Translate } from "../docsWarnings";
import { qualifiedTableKey } from "../docsKeys";
import { groupStyle } from "../groupColor";
import type { DocsEdit, DocTable, GroupAnnotation, Relationship, TableGroup } from "../types";
import ColumnTable from "./ColumnTable.vue";
import GroupPicker from "./GroupPicker.vue";
import NoteEditor from "./NoteEditor.vue";
import RelationshipList from "./RelationshipList.vue";
const props = defineProps<{
table: DocTable;
/** Every relationship in the snapshot; RelationshipList filters them. */
relationships: Relationship[];
/** The table's group, or null when it belongs to none. */
group: TableGroup | null;
/** The editable group records, which `group` above cannot be written back to. */
annotationGroups: GroupAnnotation[];
readonly: boolean;
translate: Translate;
}>();
const emit = defineEmits<{
select: [tableKey: string];
edit: [edit: DocsEdit];
createGroup: [tableKey: string];
}>();
const qualified = computed(() => qualifiedTableKey(props.table));
const kindLabel = computed(() => props.table.kind.toLowerCase().replace(/_/g, " "));
/**
* The database comment a local note replaced. Bound with `:title` so Vue
* escapes it — it is author text, exactly like the note itself.
*/
const shadowedTitle = computed(() => (props.table.shadowedNote ? `Database comment: ${props.table.shadowedNote}` : undefined));
</script>
<template>
<article class="flex flex-col gap-5">
<header class="flex flex-col gap-2">
<div class="flex flex-wrap items-center gap-2">
<h2 class="font-mono text-lg font-semibold text-foreground">{{ qualified }}</h2>
<span class="rounded bg-muted/50 px-1.5 py-0.5 text-[10px] uppercase text-muted-foreground">{{ kindLabel }}</span>
<span v-if="group" class="docs-group rounded px-1.5 py-0.5 text-[10px] font-medium" style="background-color: var(--group-tint); color: var(--group-c)" :style="groupStyle(group.hue)">
{{ group.name }}
</span>
<span v-if="table.estimatedRows !== null" class="text-[10px] text-muted-foreground"> ~{{ table.estimatedRows }} rows </span>
</div>
<!-- NoteEditor is fed the MERGED note, not the local one. It renders and
edits a single value, so seeding it from the annotation file would
show nothing for a note that came from a database comment. Writing
one shadows that comment, which is what noteSource and shadowedNote
below exist to disclose. -->
<div class="flex items-start gap-2">
<span v-if="table.noteSource === 'LOCAL'" class="mt-0.5 shrink-0 text-[10px] font-medium text-muted-foreground" :title="shadowedTitle">⬤ {{ translate("docs.localNote") }}</span>
<NoteEditor class="min-w-0 flex-1" :model-value="table.note ?? ''" :readonly="readonly" :translate="translate" @update:model-value="emit('edit', { kind: 'tableNote', tableKey: qualified, note: $event })" />
</div>
<GroupPicker v-if="!readonly" :groups="annotationGroups" :model-value="table.groupId" :translate="translate" @update:model-value="emit('edit', { kind: 'tableGroup', tableKey: qualified, groupId: $event })" @create="emit('createGroup', qualified)" />
</header>
<section>
<h3 class="mb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ translate("docs.columns") }}</h3>
<ColumnTable :columns="table.columns" :column-notes="table.columnNotes" :table-key="qualified" :readonly="readonly" :translate="translate" @edit="emit('edit', $event)" />
</section>
<section v-if="table.indexes.length > 0">
<h3 class="mb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ translate("docs.indexes") }}</h3>
<div class="overflow-hidden rounded-md border border-border">
<table class="w-full text-xs">
<thead>
<tr class="bg-muted/30">
<th class="px-2 py-1.5 text-left font-medium text-muted-foreground">{{ translate("docs.nameHeader") }}</th>
<th class="px-2 py-1.5 text-left font-medium text-muted-foreground">{{ translate("docs.columns") }}</th>
<th class="px-2 py-1.5 text-left font-medium text-muted-foreground">{{ translate("docs.settingsHeader") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="index in table.indexes" :key="index.name" class="border-t border-border align-top">
<td class="px-2 py-1.5 font-mono">{{ index.name }}</td>
<td class="px-2 py-1.5 font-mono text-muted-foreground">
{{ index.columns.join(", ") }}<template v-if="index.included_columns && index.included_columns.length > 0"> (include {{ index.included_columns.join(", ") }}) </template>
</td>
<td class="px-2 py-1.5">
<div class="flex flex-wrap gap-1 text-[10px] text-muted-foreground">
<span v-if="index.is_primary" class="rounded bg-muted/50 px-1.5 py-0.5">pk</span>
<span v-if="index.is_unique" class="rounded bg-muted/50 px-1.5 py-0.5">unique</span>
<span v-if="index.index_type" class="rounded bg-muted/50 px-1.5 py-0.5">{{ index.index_type }}</span>
<span v-if="index.filter" class="rounded bg-muted/50 px-1.5 py-0.5">where {{ index.filter }}</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section>
<h3 class="mb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ translate("docs.relationships") }}</h3>
<RelationshipList :relationships="relationships" :schema="table.schema" :table="table.name" :translate="translate" @select="emit('select', $event)" />
</section>
<section v-if="table.viewDefinition">
<h3 class="mb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ translate("docs.definitionHeader") }}</h3>
<pre class="overflow-x-auto rounded-md border border-border bg-muted/20 p-2 font-mono text-xs">{{ table.viewDefinition }}</pre>
</section>
</article>
</template>