mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(grid): open cell http urls from hover overlay
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
ArrowDown,
|
||||
ArrowUpDown,
|
||||
ArrowUpRight,
|
||||
ExternalLink,
|
||||
Download,
|
||||
FileUp,
|
||||
Trash2,
|
||||
@@ -135,6 +136,7 @@ import {
|
||||
} from "@/lib/dataGrid/dataGridTranspose";
|
||||
import { canApplyGridSelectionValue, canDeleteGridRowItem, canEditGridCellDetail, matchesRowStatusFilter, shouldShowQuickEntryDraftRow, type RowStatus, type RowStatusFilter } from "@/lib/dataGrid/gridRowStatus";
|
||||
import { displayCellValue, firstLineCellDisplayValue, limitDataGridCellDisplay, SQLSERVER_DATA_GRID_CELL_DISPLAY_MAX_LENGTH, type CellValue } from "@/lib/dataGrid/cellValue";
|
||||
import { cellExternalUrl } from "@/lib/dataGrid/cellExternalUrl";
|
||||
import { getApplicablePreviewActions, type PreviewAction } from "@/lib/dataGrid/resultPreviewRegistry";
|
||||
import "@/lib/dataGrid/geometryMapPreview";
|
||||
import {
|
||||
@@ -7889,8 +7891,9 @@ const canvasDetailButtonCell = computed(() => {
|
||||
const visibleRight = viewportWidth > 0 ? Math.min(viewportWidth, rect.left + rect.width) : rect.left + rect.width;
|
||||
const canQuickDownload = canQuickDownloadCellValue(target.rowIndex, target.col);
|
||||
const foreignKey = canvasCellForeignKey(target.rowIndex, target.col);
|
||||
if (!cellDetailButtonEnabled.value && !canQuickDownload && !foreignKey) return null;
|
||||
const minWidth = canvasDataGridActionOverlayWidth(canQuickDownload, !!foreignKey, cellDetailButtonEnabled.value) + 2;
|
||||
const externalUrl = cellExternalUrl(displayItemAt(target.rowIndex)?.data[target.col]);
|
||||
if (!cellDetailButtonEnabled.value && !canQuickDownload && !foreignKey && !externalUrl) return null;
|
||||
const minWidth = canvasDataGridActionOverlayWidth(canQuickDownload, !!foreignKey, cellDetailButtonEnabled.value, !!externalUrl) + 2;
|
||||
if (rect.top < 0 || rect.top > viewportHeight - 1 || visibleRight - visibleLeft < minWidth) return null;
|
||||
return {
|
||||
rowIndex: target.rowIndex,
|
||||
@@ -7899,13 +7902,14 @@ const canvasDetailButtonCell = computed(() => {
|
||||
rect,
|
||||
canQuickDownload,
|
||||
foreignKey,
|
||||
externalUrl,
|
||||
};
|
||||
});
|
||||
|
||||
const canvasDetailButtonStyle = computed(() => {
|
||||
const cell = canvasDetailButtonCell.value;
|
||||
if (!cell) return {};
|
||||
const actionWidth = canvasDataGridActionOverlayWidth(cell.canQuickDownload, !!cell.foreignKey, cellDetailButtonEnabled.value);
|
||||
const actionWidth = canvasDataGridActionOverlayWidth(cell.canQuickDownload, !!cell.foreignKey, cellDetailButtonEnabled.value, !!cell.externalUrl);
|
||||
const edgeGap = 6;
|
||||
return {
|
||||
left: `${Math.max(rowNumberWidth.value, cell.rect.left + cell.rect.width - actionWidth - edgeGap)}px`,
|
||||
@@ -7919,7 +7923,7 @@ const canvasRightAlignedActionCell = computed(() => {
|
||||
return {
|
||||
rowIndex: cell.rowIndex,
|
||||
visibleColIdx: cell.visibleColIdx,
|
||||
reservedWidth: canvasDataGridActionReservedWidth(cell.canQuickDownload, !!cell.foreignKey, cellDetailButtonEnabled.value),
|
||||
reservedWidth: canvasDataGridActionReservedWidth(cell.canQuickDownload, !!cell.foreignKey, cellDetailButtonEnabled.value, !!cell.externalUrl),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9854,6 +9858,25 @@ function canQuickDownloadCellValue(rowIndex: number, columnIndex: number): boole
|
||||
return canDownloadDetailBinaryValue(cellDetailFor(rowIndex, columnIndex));
|
||||
}
|
||||
|
||||
function cellExternalUrlFor(rowIndex: number, columnIndex: number): string | null {
|
||||
return cellExternalUrl(displayItemAt(rowIndex)?.data[columnIndex]);
|
||||
}
|
||||
|
||||
function canOpenCellExternalUrl(rowIndex: number, columnIndex: number): boolean {
|
||||
return cellExternalUrlFor(rowIndex, columnIndex) !== null;
|
||||
}
|
||||
|
||||
async function openCellExternalUrl(rowIndex: number, columnIndex: number) {
|
||||
const url = cellExternalUrlFor(rowIndex, columnIndex);
|
||||
if (!url) return;
|
||||
try {
|
||||
const { open } = await import("@tauri-apps/plugin-shell");
|
||||
await open(url);
|
||||
} catch {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}
|
||||
|
||||
function downloadCellBinaryValue(rowIndex: number, columnIndex: number, mode: BinaryCellDownloadMode) {
|
||||
void downloadDetailBinaryValue(cellDetailFor(rowIndex, columnIndex), mode);
|
||||
}
|
||||
@@ -13033,6 +13056,16 @@ function openGridSnapshot() {
|
||||
</button>
|
||||
</template>
|
||||
</LightDropdownMenu>
|
||||
<button
|
||||
v-if="canOpenCellExternalUrl(cell.recordIndex, cell.valueIndex)"
|
||||
class="flex h-5 w-5 items-center justify-center rounded bg-background/90 text-muted-foreground shadow-sm ring-1 ring-border hover:text-foreground"
|
||||
:title="t('grid.openUrl')"
|
||||
:aria-label="t('grid.openUrl')"
|
||||
@mousedown.stop
|
||||
@click.stop="openCellExternalUrl(cell.recordIndex, cell.valueIndex)"
|
||||
>
|
||||
<ExternalLink class="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
v-if="cellDetailButtonEnabled"
|
||||
class="flex h-5 w-5 items-center justify-center rounded bg-background/90 text-muted-foreground shadow-sm ring-1 ring-border hover:text-foreground"
|
||||
@@ -13773,6 +13806,16 @@ function openGridSnapshot() {
|
||||
</button>
|
||||
</template>
|
||||
</LightDropdownMenu>
|
||||
<button
|
||||
v-if="canvasDetailButtonCell.externalUrl"
|
||||
class="flex h-5 w-5 items-center justify-center rounded bg-background/90 text-muted-foreground shadow-sm ring-1 ring-border hover:text-foreground"
|
||||
:title="t('grid.openUrl')"
|
||||
:aria-label="t('grid.openUrl')"
|
||||
@mousedown.stop
|
||||
@click.stop="openCellExternalUrl(canvasDetailButtonCell.rowIndex, canvasDetailButtonCell.actualColIdx)"
|
||||
>
|
||||
<ExternalLink class="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
v-if="canvasDetailButtonCell.foreignKey"
|
||||
class="flex h-5 w-5 items-center justify-center rounded bg-background/90 text-muted-foreground shadow-sm ring-1 ring-border hover:text-foreground"
|
||||
@@ -14005,6 +14048,16 @@ function openGridSnapshot() {
|
||||
</button>
|
||||
</template>
|
||||
</LightDropdownMenu>
|
||||
<button
|
||||
v-if="canOpenCellExternalUrl(item.displayIndex, col.actualColIdx)"
|
||||
class="flex h-5 w-5 items-center justify-center rounded bg-background/90 text-muted-foreground shadow-sm ring-1 ring-border hover:text-foreground"
|
||||
:title="t('grid.openUrl')"
|
||||
:aria-label="t('grid.openUrl')"
|
||||
@mousedown.stop
|
||||
@click.stop="openCellExternalUrl(item.displayIndex, col.actualColIdx)"
|
||||
>
|
||||
<ExternalLink class="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
v-if="cellDetailButtonEnabled"
|
||||
class="flex h-5 w-5 items-center justify-center rounded bg-background/90 text-muted-foreground shadow-sm ring-1 ring-border hover:text-foreground"
|
||||
|
||||
+4
-4
@@ -27,10 +27,10 @@ describe("DataGrid cell detail selection", () => {
|
||||
});
|
||||
|
||||
it("removes the Canvas action overlay and reservation only when no action remains", () => {
|
||||
expect(dataGridSource).toContain("if (!cellDetailButtonEnabled.value && !canQuickDownload && !foreignKey) return null;");
|
||||
expect(dataGridSource).toContain("canvasDataGridActionOverlayWidth(canQuickDownload, !!foreignKey, cellDetailButtonEnabled.value)");
|
||||
expect(dataGridSource).toContain("canvasDataGridActionOverlayWidth(cell.canQuickDownload, !!cell.foreignKey, cellDetailButtonEnabled.value)");
|
||||
expect(dataGridSource).toContain("canvasDataGridActionReservedWidth(cell.canQuickDownload, !!cell.foreignKey, cellDetailButtonEnabled.value)");
|
||||
expect(dataGridSource).toContain("if (!cellDetailButtonEnabled.value && !canQuickDownload && !foreignKey && !externalUrl) return null;");
|
||||
expect(dataGridSource).toContain("canvasDataGridActionOverlayWidth(canQuickDownload, !!foreignKey, cellDetailButtonEnabled.value, !!externalUrl)");
|
||||
expect(dataGridSource).toContain("canvasDataGridActionOverlayWidth(cell.canQuickDownload, !!cell.foreignKey, cellDetailButtonEnabled.value, !!cell.externalUrl)");
|
||||
expect(dataGridSource).toContain("canvasDataGridActionReservedWidth(cell.canQuickDownload, !!cell.foreignKey, cellDetailButtonEnabled.value, !!cell.externalUrl)");
|
||||
expect(dataGridSource).toMatch(/watch\([\s\S]*?cellDetailButtonEnabled,[\s\S]*?scheduleCanvasDraw/);
|
||||
});
|
||||
|
||||
|
||||
@@ -2143,6 +2143,7 @@ export default {
|
||||
columnRegularIndex: "Regular index",
|
||||
tableInfoForeignKeys: "Foreign Keys",
|
||||
foreignKeyNavigate: "Go to {table}",
|
||||
openUrl: "Open URL in browser",
|
||||
tableInfoConstraints: "Constraints",
|
||||
tableInfoConstraintDisabled: "Disabled",
|
||||
tableInfoConstraintNotValidated: "Not validated",
|
||||
|
||||
@@ -2002,6 +2002,7 @@ export default withEnglishFallback({
|
||||
columnRegularIndex: "Índice normal",
|
||||
tableInfoForeignKeys: "Claves foráneas",
|
||||
foreignKeyNavigate: "Ir a {table}",
|
||||
openUrl: "Abrir URL en el navegador",
|
||||
tableInfoConstraints: "Restricciones",
|
||||
tableInfoConstraintDisabled: "Deshabilitada",
|
||||
tableInfoConstraintNotValidated: "Sin validar",
|
||||
|
||||
@@ -1989,6 +1989,7 @@ export default withEnglishFallback({
|
||||
columnRegularIndex: "Indice normale",
|
||||
tableInfoForeignKeys: "Chiavi Esterne",
|
||||
foreignKeyNavigate: "Vai a {table}",
|
||||
openUrl: "Apri URL nel browser",
|
||||
tableInfoConstraints: "Vincoli",
|
||||
tableInfoConstraintDisabled: "Disabilitata",
|
||||
tableInfoConstraintNotValidated: "Non validata",
|
||||
|
||||
@@ -2001,6 +2001,7 @@ export default withEnglishFallback({
|
||||
columnRegularIndex: "通常インデックス",
|
||||
tableInfoForeignKeys: "外部キー",
|
||||
foreignKeyNavigate: "{table} へ移動",
|
||||
openUrl: "URLをブラウザで開く",
|
||||
tableInfoConstraints: "制約",
|
||||
tableInfoConstraintDisabled: "無効",
|
||||
tableInfoConstraintNotValidated: "未検証",
|
||||
|
||||
@@ -1965,6 +1965,7 @@ export default withEnglishFallback({
|
||||
columnRegularIndex: "일반 인덱스",
|
||||
tableInfoForeignKeys: "외래 키",
|
||||
foreignKeyNavigate: "{table}(으)로 이동",
|
||||
openUrl: "브라우저에서 URL 열기",
|
||||
tableInfoConstraints: "제약 조건",
|
||||
tableInfoConstraintDisabled: "비활성화됨",
|
||||
tableInfoConstraintNotValidated: "검증 안 됨",
|
||||
|
||||
@@ -1991,6 +1991,7 @@ export default withEnglishFallback({
|
||||
columnRegularIndex: "Índice comum",
|
||||
tableInfoForeignKeys: "Chaves Estrangeiras",
|
||||
foreignKeyNavigate: "Ir para {table}",
|
||||
openUrl: "Abrir URL no navegador",
|
||||
tableInfoConstraints: "Restrições",
|
||||
tableInfoConstraintDisabled: "Desabilitada",
|
||||
tableInfoConstraintNotValidated: "Não validada",
|
||||
|
||||
@@ -2067,6 +2067,7 @@ export default withEnglishFallback({
|
||||
columnRegularIndex: "普通索引",
|
||||
tableInfoForeignKeys: "外键",
|
||||
foreignKeyNavigate: "跳转到 {table}",
|
||||
openUrl: "在浏览器中打开 URL",
|
||||
tableInfoConstraints: "约束",
|
||||
tableInfoConstraintDisabled: "已禁用",
|
||||
tableInfoConstraintNotValidated: "未验证",
|
||||
|
||||
@@ -1994,6 +1994,7 @@ export default withEnglishFallback({
|
||||
columnRegularIndex: "一般索引",
|
||||
tableInfoForeignKeys: "外鍵",
|
||||
foreignKeyNavigate: "跳轉到 {table}",
|
||||
openUrl: "在瀏覽器中開啟 URL",
|
||||
tableInfoConstraints: "約束",
|
||||
tableInfoConstraintDisabled: "已停用",
|
||||
tableInfoConstraintNotValidated: "未驗證",
|
||||
|
||||
@@ -198,14 +198,14 @@ export function fitCanvasText(ctx: CanvasRenderingContext2D, text: string, maxWi
|
||||
return result;
|
||||
}
|
||||
|
||||
export function canvasDataGridActionReservedWidth(canQuickDownload: boolean, canNavigateForeignKey = false, showCellDetail = true): number {
|
||||
const overlayWidth = canvasDataGridActionOverlayWidth(canQuickDownload, canNavigateForeignKey, showCellDetail);
|
||||
export function canvasDataGridActionReservedWidth(canQuickDownload: boolean, canNavigateForeignKey = false, showCellDetail = true, canOpenExternalUrl = false): number {
|
||||
const overlayWidth = canvasDataGridActionOverlayWidth(canQuickDownload, canNavigateForeignKey, showCellDetail, canOpenExternalUrl);
|
||||
return overlayWidth > 0 ? overlayWidth + 6 : 0;
|
||||
}
|
||||
|
||||
/** 悬浮按钮组宽度:每个已启用按钮 20px + 2px 间距。 */
|
||||
export function canvasDataGridActionOverlayWidth(canQuickDownload: boolean, canNavigateForeignKey = false, showCellDetail = true): number {
|
||||
return (showCellDetail ? 22 : 0) + (canQuickDownload ? 22 : 0) + (canNavigateForeignKey ? 22 : 0);
|
||||
export function canvasDataGridActionOverlayWidth(canQuickDownload: boolean, canNavigateForeignKey = false, showCellDetail = true, canOpenExternalUrl = false): number {
|
||||
return (showCellDetail ? 22 : 0) + (canQuickDownload ? 22 : 0) + (canNavigateForeignKey ? 22 : 0) + (canOpenExternalUrl ? 22 : 0);
|
||||
}
|
||||
|
||||
export function resolveCanvasCellTextLayout(options: { drawX: number; colWidth: number; dpr: number; isRightAlign: boolean; reservedWidth?: number }): { textAnchorX: number; maxWidth: number } {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { CellValue } from "@/lib/dataGrid/cellValue";
|
||||
|
||||
/** Return a browser-safe external URL only when the whole cell is an HTTP(S) URL. */
|
||||
export function cellExternalUrl(value: CellValue | unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const text = value.trim();
|
||||
if (!text) return null;
|
||||
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(text);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
||||
return text;
|
||||
}
|
||||
+1
-1
@@ -47,7 +47,7 @@ test("DataGrid forwards hover action reservation only for right-aligned canvas c
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
assert.match(source, /columnAligns\.value\[cell\.visibleColIdx\] !== "right"/);
|
||||
assert.match(source, /reservedWidth: canvasDataGridActionReservedWidth\(cell\.canQuickDownload, !!cell\.foreignKey, cellDetailButtonEnabled\.value\)/);
|
||||
assert.match(source, /reservedWidth: canvasDataGridActionReservedWidth\(cell\.canQuickDownload, !!cell\.foreignKey, cellDetailButtonEnabled\.value, !!cell\.externalUrl\)/);
|
||||
assert.match(source, /rightAlignedActionCell: canvasRightAlignedActionCell\.value/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user