feat(documents): refresh detail pages and attachment cards

This commit is contained in:
Cheng Zhou
2026-06-08 11:03:34 +08:00
parent a3f6a04f35
commit 834b4f1d48
11 changed files with 2372 additions and 929 deletions
@@ -50,6 +50,38 @@ describe("DocumentDetail permissions", () => {
expect(source).toContain('can("project.members.list")');
expect(source).toContain("if (!canReadMembers.value)");
});
it("reuses attachment-style preview for document versions", () => {
const source = readSource();
expect(source).toContain("@click=\"previewVersion(row)\"");
expect(source).toContain("TEXT.common.labels.preview");
expect(source).toContain('return "image"');
expect(source).toContain('return "pdf"');
expect(source).toContain("TEXT.common.messages.previewNotSupported");
expect(source).toContain("URL.createObjectURL(blob)");
expect(source).toContain("URL.revokeObjectURL(previewObjectUrl.value)");
});
it("prefers backend creator display data instead of exposing creator ids", () => {
const source = readSource();
expect(source).toContain("displayCreator(row.created_by_user, row.created_by)");
expect(source).toContain("const displayCreator = (user?: UserInfo | null, userId?: string | null)");
expect(source).toContain("getUserDisplayName(user)");
expect(source).not.toContain("displayCreator(row.created_by)");
expect(source).not.toContain("return userMap.value[userId] || userId;");
});
it("uses the version record creation time for the updated-at column", () => {
const source = readSource();
expect(source).toContain("displayDateTime");
expect(source).toContain("splitDateTime(row.created_at).date");
expect(source).toContain("splitDateTime(row.created_at).time");
expect(source).toContain("const displayValue = displayDateTime(value)");
expect(source).not.toContain("splitDateTime(row.effective_at || row.created_at)");
});
});
describe("DocumentDetail breadcrumbs", () => {
@@ -76,4 +108,19 @@ describe("DocumentDetail editor overlays", () => {
expect(source).not.toContain("<el-dialog append-to=\".layout-main .content-wrapper\" v-model=\"uploadVisible\"");
expect(source).not.toContain("<el-dialog append-to=\".layout-main .content-wrapper\" v-model=\"distributeVisible\"");
});
it("guards dirty drawer closes while allowing mask close", () => {
const source = readSource();
expect(source).toContain("useDrawerDirtyGuard");
expect(source).toContain("const editorDirtyGuard = useDrawerDirtyGuard(() => editorForm);");
expect(source).toContain("const uploadDirtyGuard = useDrawerDirtyGuard(() => ({");
expect(source).toContain("const distributeDirtyGuard = useDrawerDirtyGuard(() => distributeForm);");
expect(source).toContain(':before-close="editorDirtyGuard.beforeClose"');
expect(source).toContain(':before-close="uploadDirtyGuard.beforeClose"');
expect(source).toContain(':before-close="distributeDirtyGuard.beforeClose"');
expect(source).toContain("editorDirtyGuard.syncBaseline()");
expect(source).toContain("uploadDirtyGuard.syncBaseline()");
expect(source).toContain("distributeDirtyGuard.syncBaseline()");
});
});
File diff suppressed because it is too large Load Diff
@@ -28,4 +28,24 @@ describe("DocumentList permissions", () => {
expect(source).not.toContain("<el-dialog");
expect(source).not.toContain('append-to=".layout-main .content-wrapper"');
});
it("guards dirty drawer closes while allowing mask close", () => {
const source = readSource();
expect(source).toContain("useDrawerDirtyGuard");
expect(source).toContain('const editorDirtyGuard = useDrawerDirtyGuard(() => editorForm);');
expect(source).toContain(':close-on-click-modal="true"');
expect(source).toContain(':before-close="editorDirtyGuard.beforeClose"');
expect(source).toContain("editorDirtyGuard.syncBaseline()");
});
it("uses the shared datetime formatter and splits updated-at into two lines", () => {
const source = readSource();
expect(source).toContain("displayDateTime");
expect(source).toContain("splitDateTime(row.updated_at).date");
expect(source).toContain("splitDateTime(row.updated_at).time");
expect(source).toContain("const displayValue = displayDateTime(value)");
expect(source).not.toContain('replace("T", " ").replace("Z", "")');
});
});
+110 -16
View File
@@ -70,17 +70,22 @@
</el-table-column>
<el-table-column prop="updated_at" :label="TEXT.modules.fileVersionManagement.columns.updatedAt" show-overflow-tooltip>
<template #default="{ row }">
<span class="text-secondary">{{ formatDate(row.updated_at) }}</span>
<span class="text-secondary datetime-stack">
<span>{{ splitDateTime(row.updated_at).date }}</span>
<span class="datetime-stack__time">{{ splitDateTime(row.updated_at).time }}</span>
</span>
</template>
</el-table-column>
<el-table-column v-if="canUpdate || canDelete" :label="TEXT.modules.fileVersionManagement.columns.actions" width="140" align="center">
<el-table-column v-if="canUpdate || canDelete" :label="TEXT.modules.fileVersionManagement.columns.actions" width="130" fixed="right">
<template #default="{ row }">
<el-button v-if="canUpdate" link type="primary" :disabled="isInactiveSite(row.site_id)" @click.stop="openEdit(row)">
{{ TEXT.common.actions.edit }}
</el-button>
<el-button v-if="canDelete" link type="danger" :disabled="isInactiveSite(row.site_id)" @click.stop="confirmDelete(row)">
{{ TEXT.common.actions.delete }}
</el-button>
<div class="cell-actions">
<el-button v-if="canUpdate" link type="primary" size="small" :disabled="isInactiveSite(row.site_id)" @click.stop="openEdit(row)">
{{ TEXT.common.actions.edit }}
</el-button>
<el-button v-if="canDelete" link type="danger" size="small" :disabled="isInactiveSite(row.site_id)" @click.stop="confirmDelete(row)">
{{ TEXT.common.actions.delete }}
</el-button>
</div>
</template>
</el-table-column>
<template #empty>
@@ -96,6 +101,7 @@
direction="rtl"
size="620px"
:close-on-click-modal="true"
:before-close="editorDirtyGuard.beforeClose"
:show-close="false"
destroy-on-close
class="document-editor-drawer"
@@ -167,9 +173,10 @@ import { fetchSites } from "../../api/sites";
import { useStudyStore } from "../../store/study";
import { useAuthStore } from "../../store/auth";
import { usePermission } from "../../utils/permission";
import { useDrawerDirtyGuard } from "../../utils/drawerDirtyGuard";
import type { DocumentSummary } from "../../types/documents";
import type { Site } from "../../types/api";
import { displayEnum, displayText } from "../../utils/display";
import { displayDateTime, displayEnum, displayText } from "../../utils/display";
import { TEXT } from "../../locales";
const route = useRoute();
@@ -239,6 +246,7 @@ const editorForm = reactive({
site_id: "",
doc_type: "",
});
const editorDirtyGuard = useDrawerDirtyGuard(() => editorForm);
const editorRules: FormRules = {
doc_no: [{ required: true, message: TEXT.common.messages.required, trigger: "blur" }],
@@ -318,6 +326,7 @@ const openCreate = () => {
}
editingDocumentId.value = "";
resetEditorForm();
editorDirtyGuard.syncBaseline();
editorVisible.value = true;
};
@@ -339,6 +348,7 @@ const openEdit = (row: DocumentSummary) => {
doc_type: row.doc_type || "",
});
editorFormRef.value?.clearValidate();
editorDirtyGuard.syncBaseline();
editorVisible.value = true;
};
@@ -416,9 +426,10 @@ const confirmDelete = async (row: DocumentSummary) => {
}
};
const formatDate = (value?: string | null) => {
if (!value) return TEXT.common.fallback;
return value.replace("T", " ").replace("Z", "").split(".")[0];
const splitDateTime = (value?: string | null) => {
const displayValue = displayDateTime(value);
const [date, time] = displayValue.split(" ");
return { date, time: time || "" };
};
onMounted(() => {
@@ -461,10 +472,6 @@ watch(
width: 132px;
}
.ctms-table :deep(.el-table__inner-wrapper::before) {
display: none;
}
.filter-spacer {
flex: 1;
}
@@ -524,6 +531,93 @@ watch(
gap: 10px;
}
/* ==================== Table Styling (DrugShipments style) ==================== */
.document-table-section {
background: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.04),
0 2px 8px rgba(0, 0, 0, 0.02);
}
.ctms-table {
--el-table-border-color: transparent;
--el-table-row-hover-bg-color: #f8f9fb;
}
.ctms-table :deep(.el-table__inner-wrapper::before) {
display: none;
}
.ctms-table :deep(th.el-table__cell) {
background: #f4f5f7;
color: #2a2a2a;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 14px 14px;
border-bottom: 2px solid #e0e0e0 !important;
}
.ctms-table :deep(td.el-table__cell) {
padding: 12px 14px;
color: #0a0a0a;
font-size: 14px;
font-weight: 500;
}
.ctms-table :deep(.el-table__body tr) {
cursor: pointer;
transition: background 0.1s ease;
}
.ctms-table :deep(.font-semibold) {
font-weight: 600;
color: #0a0a0a;
}
.ctms-table :deep(.text-secondary) {
color: #8a8a8a;
}
.ctms-table :deep(.datetime-stack) {
display: inline-flex;
flex-direction: column;
gap: 2px;
font-size: 12px;
font-variant-numeric: tabular-nums;
font-weight: 600;
line-height: 1.25;
white-space: nowrap;
}
.ctms-table :deep(.datetime-stack__time) {
color: #9a9a9a;
font-size: 11px;
}
.ctms-table :deep(.el-tag) {
font-weight: 600;
border: none;
}
.ctms-table :deep(.cell-actions) {
display: flex;
gap: 8px;
white-space: nowrap;
}
.ctms-table :deep(.cell-actions .el-button) {
font-size: 13px;
padding: 0;
height: auto;
}
.ctms-table :deep(.cell-actions .el-button + .el-button) {
margin-left: 0;
}
</style>
<style>