Merge pull request #804 from serenez/fix/528-detail-dialog-ui
fix(grid): polish cell/row/column detail dialogs (#528)
This commit is contained in:
9 files changed
+193
-46
No files matched your search
@@ -138,6 +138,7 @@ import {
|
||||
dataGridColumnDetailTsv,
|
||||
dataGridRowDetailJson,
|
||||
dataGridRowDetailTsv,
|
||||
filterDataGridDetailFields,
|
||||
type DataGridCellDetail,
|
||||
} from "@/lib/dataGridDetail";
|
||||
import {
|
||||
@@ -428,6 +429,9 @@ const rowDetailDialogOpen = ref(false);
|
||||
const rowDetailDialogRowId = ref<number | null>(null);
|
||||
const columnDetailDialogOpen = ref(false);
|
||||
const columnDetailDialogColumnIndex = ref<number | null>(null);
|
||||
const cellDetailJsonView = ref(false);
|
||||
const rowDetailSearch = ref("");
|
||||
const columnDetailSearch = ref("");
|
||||
const isResizingDetail = ref(false);
|
||||
const imagePreviewOpen = ref(false);
|
||||
const imagePreviewSrc = ref("");
|
||||
@@ -2699,16 +2703,31 @@ const columnDetail = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const filteredRowDetailFields = computed(() =>
|
||||
rowDetail.value ? filterDataGridDetailFields(rowDetail.value.fields, rowDetailSearch.value) : [],
|
||||
);
|
||||
|
||||
const filteredColumnDetailFields = computed(() =>
|
||||
columnDetail.value ? filterDataGridDetailFields(columnDetail.value.fields, columnDetailSearch.value) : [],
|
||||
);
|
||||
|
||||
watch(cellDetailDialogOpen, (open) => {
|
||||
if (!open) cellDetailDialogTarget.value = null;
|
||||
if (open) cellDetailJsonView.value = false;
|
||||
else cellDetailDialogTarget.value = null;
|
||||
});
|
||||
|
||||
watch(rowDetailDialogOpen, (open) => {
|
||||
if (!open) rowDetailDialogRowId.value = null;
|
||||
if (!open) {
|
||||
rowDetailDialogRowId.value = null;
|
||||
rowDetailSearch.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
watch(columnDetailDialogOpen, (open) => {
|
||||
if (!open) columnDetailDialogColumnIndex.value = null;
|
||||
if (!open) {
|
||||
columnDetailDialogColumnIndex.value = null;
|
||||
columnDetailSearch.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
watch(sideGeometryPreviewOpen, async (open) => {
|
||||
@@ -4469,6 +4488,14 @@ function copyDialogCellFormattedJson() {
|
||||
copyText(detail.formattedJson);
|
||||
}
|
||||
|
||||
function copyDialogCellCurrentValue() {
|
||||
if (cellDetailJsonView.value && dialogCellDetail.value?.formattedJson) {
|
||||
copyDialogCellFormattedJson();
|
||||
} else {
|
||||
copyDialogCellValue();
|
||||
}
|
||||
}
|
||||
|
||||
function copyDialogCellColumnName() {
|
||||
const detail = dialogCellDetail.value;
|
||||
if (!detail) return;
|
||||
@@ -7999,6 +8026,17 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="text-muted-foreground">{{ t("grid.cellValue") }}</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
v-if="dialogCellDetail.formattedJson"
|
||||
:variant="cellDetailJsonView ? 'secondary' : 'ghost'"
|
||||
size="sm"
|
||||
class="h-6 gap-1 px-2 text-xs"
|
||||
:title="t('grid.formattedJson')"
|
||||
@click="cellDetailJsonView = !cellDetailJsonView"
|
||||
>
|
||||
<Code2 class="h-3 w-3" />
|
||||
{{ t("grid.formattedJson") }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="dialogCellDetail.isEditable"
|
||||
variant="ghost"
|
||||
@@ -8014,7 +8052,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
:title="t('grid.copyValue')"
|
||||
@click="copyDialogCellValue"
|
||||
@click="copyDialogCellCurrentValue"
|
||||
>
|
||||
<Copy class="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -8079,46 +8117,37 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
<pre
|
||||
class="max-h-[44vh] overflow-auto rounded border bg-muted/20 p-3 font-mono text-xs whitespace-pre-wrap break-words"
|
||||
:class="{ 'italic text-muted-foreground': dialogCellDetail.value === null }"
|
||||
>{{ dialogCellDetail.rawValuePreview }}</pre
|
||||
>{{
|
||||
cellDetailJsonView && dialogCellDetail.formattedJson
|
||||
? dialogCellDetail.formattedJson
|
||||
: dialogCellDetail.rawValuePreview
|
||||
}}</pre
|
||||
>
|
||||
<div
|
||||
v-if="dialogCellDetail.isValuePreviewTruncated && !cellDetailJsonView"
|
||||
class="text-[11px] text-muted-foreground"
|
||||
>
|
||||
<div v-if="dialogCellDetail.isValuePreviewTruncated" class="text-[11px] text-muted-foreground">
|
||||
{{ t("grid.largeValuePreviewHint", { count: dialogCellDetail.rawValuePreview.length }) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="dialogCellDetail.displayValuePreview !== dialogCellDetail.rawValuePreview" class="space-y-1">
|
||||
<div
|
||||
v-if="dialogCellDetail.displayValuePreview !== dialogCellDetail.rawValuePreview && !cellDetailJsonView"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div class="text-muted-foreground">{{ t("grid.formattedValue") }}</div>
|
||||
<pre class="max-h-40 overflow-auto rounded border bg-muted/20 p-3 font-mono text-xs whitespace-pre-wrap">{{
|
||||
dialogCellDetail.displayValuePreview
|
||||
}}</pre>
|
||||
</div>
|
||||
|
||||
<div v-if="dialogCellDetail.formattedJson" class="space-y-1">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="text-muted-foreground">{{ t("grid.formattedJson") }}</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 px-2 text-xs"
|
||||
:title="t('grid.copyValue')"
|
||||
@click="copyDialogCellFormattedJson"
|
||||
>
|
||||
<Copy class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<pre
|
||||
class="max-h-[44vh] overflow-auto rounded border bg-muted/20 p-3 font-mono text-xs whitespace-pre-wrap"
|
||||
>{{ dialogCellDetail.formattedJson }}</pre
|
||||
class="max-h-40 overflow-auto rounded border bg-muted/20 p-3 font-mono text-xs whitespace-pre-wrap break-words"
|
||||
>{{ dialogCellDetail.displayValuePreview }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter class="shrink-0 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" class="h-7 text-xs" @click="copyDialogCellColumnName">
|
||||
<Copy class="mr-1.5 h-3 w-3" /> {{ t("grid.copyColumnName") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2"></div>
|
||||
<Button variant="ghost" size="sm" class="h-7 text-xs" @click="copyDialogCellColumnName">
|
||||
<Copy class="mr-1.5 h-3 w-3" /> {{ t("grid.copyColumnName") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -8132,8 +8161,18 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-3 text-xs text-muted-foreground">
|
||||
<span>{{ t("grid.columnsCount", { count: rowDetail.fields.length }) }}</span>
|
||||
<div class="flex shrink-0 items-center gap-2 text-xs text-muted-foreground">
|
||||
<span class="whitespace-nowrap">{{ t("grid.columnsCount", { count: rowDetail.fields.length }) }}</span>
|
||||
<div class="relative ml-auto w-56 max-w-full">
|
||||
<Search
|
||||
class="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<Input
|
||||
v-model="rowDetailSearch"
|
||||
:placeholder="t('grid.detailSearchPlaceholder')"
|
||||
class="h-7 pl-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-auto rounded border">
|
||||
@@ -8148,13 +8187,13 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(field, fieldIndex) in rowDetail.fields"
|
||||
v-for="(field, fieldIndex) in filteredRowDetailFields"
|
||||
:key="`${field.colIndex}:${field.column}`"
|
||||
class="border-b align-top last:border-b-0"
|
||||
>
|
||||
<td class="px-3 py-2 text-muted-foreground tabular-nums">{{ fieldIndex + 1 }}</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="font-medium break-all">{{ field.column }}</div>
|
||||
<div class="font-medium break-words">{{ field.column }}</div>
|
||||
<div
|
||||
:class="field.type ? typeColorClass(field.type) : 'text-muted-foreground'"
|
||||
class="mt-1 text-[11px]"
|
||||
@@ -8165,7 +8204,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
{{ field.comment }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="min-w-0 px-3 py-2">
|
||||
<td class="w-full max-w-0 px-3 py-2">
|
||||
<div class="mb-1 text-[11px] text-muted-foreground">
|
||||
{{ field.value === null ? t("grid.nullValue") : t("grid.valueLength") }}:
|
||||
{{ field.value === null ? "true" : field.length }}
|
||||
@@ -8216,15 +8255,23 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
v-if="rowDetailSearch && !filteredRowDetailFields.length"
|
||||
class="px-3 py-6 text-center text-xs text-muted-foreground"
|
||||
>
|
||||
{{ t("grid.detailSearchNoMatch") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter class="shrink-0 justify-start gap-2">
|
||||
<Button variant="outline" size="sm" class="h-7 text-xs" @click="copyRowDetailJson">
|
||||
<Copy class="mr-1.5 h-3 w-3" /> {{ t("grid.copyRow") }}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" class="h-7 text-xs" @click="copyRowDetailTsv">
|
||||
<Copy class="mr-1.5 h-3 w-3" /> {{ t("grid.copyRowTsv") }}
|
||||
</Button>
|
||||
<DialogFooter class="shrink-0 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" class="h-7 text-xs" @click="copyRowDetailJson">
|
||||
<Copy class="mr-1.5 h-3 w-3" /> {{ t("grid.copyRow") }}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" class="h-7 text-xs" @click="copyRowDetailTsv">
|
||||
<Copy class="mr-1.5 h-3 w-3" /> {{ t("grid.copyRowTsv") }}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -8261,6 +8308,20 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2 text-xs text-muted-foreground">
|
||||
<span class="whitespace-nowrap">{{ t("grid.rowCount") }}: {{ columnDetail.fields.length }}</span>
|
||||
<div class="relative ml-auto w-56 max-w-full">
|
||||
<Search
|
||||
class="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<Input
|
||||
v-model="columnDetailSearch"
|
||||
:placeholder="t('grid.detailSearchPlaceholder')"
|
||||
class="h-7 pl-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-auto rounded border">
|
||||
<table class="w-full min-w-[500px] text-xs">
|
||||
<thead class="sticky top-0 z-10 bg-muted/80 text-muted-foreground backdrop-blur">
|
||||
@@ -8272,12 +8333,12 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="field in columnDetail.fields"
|
||||
v-for="field in filteredColumnDetailFields"
|
||||
:key="`${field.rowId}:${field.colIndex}`"
|
||||
class="border-b align-top last:border-b-0"
|
||||
>
|
||||
<td class="px-3 py-2 tabular-nums">{{ field.rowNumber }}</td>
|
||||
<td class="min-w-0 px-3 py-2">
|
||||
<td class="w-full max-w-0 px-3 py-2">
|
||||
<div class="mb-1 text-[11px] text-muted-foreground">
|
||||
{{ field.value === null ? t("grid.nullValue") : t("grid.valueLength") }}:
|
||||
{{ field.value === null ? "true" : field.length }}
|
||||
@@ -8328,6 +8389,12 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
v-if="columnDetailSearch && !filteredColumnDetailFields.length"
|
||||
class="px-3 py-6 text-center text-xs text-muted-foreground"
|
||||
>
|
||||
{{ t("grid.detailSearchNoMatch") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter class="shrink-0 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
|
||||
Reference in new issue
Block a user