fix: improve org tree hierarchy and user menu dismissal
This commit is contained in:
parent
5ec8a98125
commit
eb0f2eff95
@ -19,3 +19,13 @@ describe("App reset password account picker", () => {
|
||||
assert.match(source, /formatResetPasswordUserLabel\(user\)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("App user menu interactions", () => {
|
||||
it("closes the avatar menu when the user clicks outside the menu shell", () => {
|
||||
assert.match(source, /ref="userMenuShellRef"/);
|
||||
assert.match(source, /function handleUserMenuOutsidePointerDown\(event\)/);
|
||||
assert.match(source, /userMenuShellRef\.value\?\.contains\(event\.target\)/);
|
||||
assert.match(source, /document\.addEventListener\("pointerdown", handleUserMenuOutsidePointerDown\)/);
|
||||
assert.match(source, /document\.removeEventListener\("pointerdown", handleUserMenuOutsidePointerDown\)/);
|
||||
});
|
||||
});
|
||||
|
||||
@ -110,7 +110,7 @@
|
||||
<span class="assistant-bot-eye"></span>
|
||||
</span>
|
||||
</button>
|
||||
<div class="user-menu-shell">
|
||||
<div ref="userMenuShellRef" class="user-menu-shell">
|
||||
<button class="user-avatar-button" type="button" @click="toggleUserMenu">
|
||||
<span class="user-avatar">{{ avatarInitial }}</span>
|
||||
<span class="user-avatar-meta">
|
||||
@ -307,6 +307,7 @@ const activeBroadcasts = ref([]);
|
||||
const activeBroadcastIndex = ref(0);
|
||||
const assistantDrawerOpen = ref(false);
|
||||
const userMenuOpen = ref(false);
|
||||
const userMenuShellRef = ref(null);
|
||||
const changePasswordOpen = ref(false);
|
||||
const resetPasswordOpen = ref(false);
|
||||
const passwordSubmitting = ref(false);
|
||||
@ -1011,9 +1012,20 @@ function handleTagContextAction(action) {
|
||||
function handleGlobalKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
closeTagContextMenu();
|
||||
userMenuOpen.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleUserMenuOutsidePointerDown(event) {
|
||||
if (!userMenuOpen.value) {
|
||||
return;
|
||||
}
|
||||
if (userMenuShellRef.value?.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
userMenuOpen.value = false;
|
||||
}
|
||||
|
||||
function updateShellViewportState() {
|
||||
narrowShell.value = window.innerWidth < 1024;
|
||||
if (narrowShell.value) {
|
||||
@ -1040,6 +1052,7 @@ onMounted(() => {
|
||||
window.addEventListener("resize", updateShellViewportState);
|
||||
window.addEventListener("click", closeTagContextMenu);
|
||||
window.addEventListener("keydown", handleGlobalKeydown);
|
||||
document.addEventListener("pointerdown", handleUserMenuOutsidePointerDown);
|
||||
void loadActiveBroadcast();
|
||||
void loadSmartOperationFeature();
|
||||
broadcastTimer = window.setInterval(() => {
|
||||
@ -1054,6 +1067,7 @@ onUnmounted(() => {
|
||||
window.removeEventListener("resize", updateShellViewportState);
|
||||
window.removeEventListener("click", closeTagContextMenu);
|
||||
window.removeEventListener("keydown", handleGlobalKeydown);
|
||||
document.removeEventListener("pointerdown", handleUserMenuOutsidePointerDown);
|
||||
if (broadcastTimer) {
|
||||
window.clearInterval(broadcastTimer);
|
||||
broadcastTimer = null;
|
||||
|
||||
17
frontend/src/components/OrgPermissionTree.test.js
Normal file
17
frontend/src/components/OrgPermissionTree.test.js
Normal file
@ -0,0 +1,17 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const source = readFileSync(join(__dirname, "OrgPermissionTree.vue"), "utf8");
|
||||
|
||||
describe("OrgPermissionTree hierarchy lines", () => {
|
||||
it("draws connector lines so parent-child hierarchy is easier to read", () => {
|
||||
assert.match(source, /\.org-tree-branch::before/);
|
||||
assert.match(source, /\.org-tree-node::before/);
|
||||
assert.match(source, /--tree-connector-x/);
|
||||
assert.match(source, /--tree-connector-color/);
|
||||
});
|
||||
});
|
||||
@ -164,7 +164,10 @@ const OrgTreeBranch = defineComponent({
|
||||
}
|
||||
|
||||
return () =>
|
||||
h("div", { class: "org-tree-branch" }, [
|
||||
h("div", {
|
||||
class: ["org-tree-branch", { "org-tree-branch-root": branchProps.level === 0 }],
|
||||
style: { "--tree-level": branchProps.level }
|
||||
}, [
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
@ -567,6 +570,8 @@ function removeEmployee(employee) {
|
||||
}
|
||||
|
||||
.org-tree-list {
|
||||
--tree-connector-color: rgba(64, 158, 255, 0.34);
|
||||
--tree-connector-x: calc(15px + var(--tree-level, 0) * 22px);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
@ -579,12 +584,34 @@ function removeEmployee(employee) {
|
||||
}
|
||||
|
||||
.org-tree-branch {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.org-tree-branch::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
left: var(--tree-connector-x);
|
||||
width: 1px;
|
||||
background: linear-gradient(180deg, transparent, var(--tree-connector-color) 12px, var(--tree-connector-color) calc(100% - 12px), transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.org-tree-branch-root::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.org-tree-branch:not(.org-tree-branch-root):last-child::before {
|
||||
bottom: calc(100% - 21px);
|
||||
background: var(--tree-connector-color);
|
||||
}
|
||||
|
||||
.org-tree-node {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 18px 24px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
@ -600,6 +627,17 @@ function removeEmployee(employee) {
|
||||
transition: background 0.14s ease, color 0.14s ease, box-shadow 0.14s ease;
|
||||
}
|
||||
|
||||
.org-tree-branch:not(.org-tree-branch-root) > .org-tree-node::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: var(--tree-connector-x);
|
||||
width: 14px;
|
||||
height: 1px;
|
||||
background: var(--tree-connector-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.org-tree-node:hover,
|
||||
.org-tree-node.active {
|
||||
border-color: transparent;
|
||||
@ -750,12 +788,34 @@ function removeEmployee(employee) {
|
||||
}
|
||||
|
||||
:deep(.org-tree-branch) {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
:deep(.org-tree-branch::before) {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
left: var(--tree-connector-x);
|
||||
width: 1px;
|
||||
background: linear-gradient(180deg, transparent, var(--tree-connector-color) 12px, var(--tree-connector-color) calc(100% - 12px), transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.org-tree-branch-root::before) {
|
||||
content: none;
|
||||
}
|
||||
|
||||
:deep(.org-tree-branch:not(.org-tree-branch-root):last-child::before) {
|
||||
bottom: calc(100% - 21px);
|
||||
background: var(--tree-connector-color);
|
||||
}
|
||||
|
||||
:deep(.org-tree-node) {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 18px 24px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
@ -772,6 +832,17 @@ function removeEmployee(employee) {
|
||||
transition: background 0.14s ease, color 0.14s ease, box-shadow 0.14s ease;
|
||||
}
|
||||
|
||||
:deep(.org-tree-branch:not(.org-tree-branch-root) > .org-tree-node::before) {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: var(--tree-connector-x);
|
||||
width: 14px;
|
||||
height: 1px;
|
||||
background: var(--tree-connector-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.org-tree-node:hover),
|
||||
:deep(.org-tree-node.active) {
|
||||
border-color: transparent;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user