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\)/);
|
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 class="assistant-bot-eye"></span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="user-menu-shell">
|
<div ref="userMenuShellRef" class="user-menu-shell">
|
||||||
<button class="user-avatar-button" type="button" @click="toggleUserMenu">
|
<button class="user-avatar-button" type="button" @click="toggleUserMenu">
|
||||||
<span class="user-avatar">{{ avatarInitial }}</span>
|
<span class="user-avatar">{{ avatarInitial }}</span>
|
||||||
<span class="user-avatar-meta">
|
<span class="user-avatar-meta">
|
||||||
@ -307,6 +307,7 @@ const activeBroadcasts = ref([]);
|
|||||||
const activeBroadcastIndex = ref(0);
|
const activeBroadcastIndex = ref(0);
|
||||||
const assistantDrawerOpen = ref(false);
|
const assistantDrawerOpen = ref(false);
|
||||||
const userMenuOpen = ref(false);
|
const userMenuOpen = ref(false);
|
||||||
|
const userMenuShellRef = ref(null);
|
||||||
const changePasswordOpen = ref(false);
|
const changePasswordOpen = ref(false);
|
||||||
const resetPasswordOpen = ref(false);
|
const resetPasswordOpen = ref(false);
|
||||||
const passwordSubmitting = ref(false);
|
const passwordSubmitting = ref(false);
|
||||||
@ -1011,9 +1012,20 @@ function handleTagContextAction(action) {
|
|||||||
function handleGlobalKeydown(event) {
|
function handleGlobalKeydown(event) {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
closeTagContextMenu();
|
closeTagContextMenu();
|
||||||
|
userMenuOpen.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleUserMenuOutsidePointerDown(event) {
|
||||||
|
if (!userMenuOpen.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (userMenuShellRef.value?.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
userMenuOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
function updateShellViewportState() {
|
function updateShellViewportState() {
|
||||||
narrowShell.value = window.innerWidth < 1024;
|
narrowShell.value = window.innerWidth < 1024;
|
||||||
if (narrowShell.value) {
|
if (narrowShell.value) {
|
||||||
@ -1040,6 +1052,7 @@ onMounted(() => {
|
|||||||
window.addEventListener("resize", updateShellViewportState);
|
window.addEventListener("resize", updateShellViewportState);
|
||||||
window.addEventListener("click", closeTagContextMenu);
|
window.addEventListener("click", closeTagContextMenu);
|
||||||
window.addEventListener("keydown", handleGlobalKeydown);
|
window.addEventListener("keydown", handleGlobalKeydown);
|
||||||
|
document.addEventListener("pointerdown", handleUserMenuOutsidePointerDown);
|
||||||
void loadActiveBroadcast();
|
void loadActiveBroadcast();
|
||||||
void loadSmartOperationFeature();
|
void loadSmartOperationFeature();
|
||||||
broadcastTimer = window.setInterval(() => {
|
broadcastTimer = window.setInterval(() => {
|
||||||
@ -1054,6 +1067,7 @@ onUnmounted(() => {
|
|||||||
window.removeEventListener("resize", updateShellViewportState);
|
window.removeEventListener("resize", updateShellViewportState);
|
||||||
window.removeEventListener("click", closeTagContextMenu);
|
window.removeEventListener("click", closeTagContextMenu);
|
||||||
window.removeEventListener("keydown", handleGlobalKeydown);
|
window.removeEventListener("keydown", handleGlobalKeydown);
|
||||||
|
document.removeEventListener("pointerdown", handleUserMenuOutsidePointerDown);
|
||||||
if (broadcastTimer) {
|
if (broadcastTimer) {
|
||||||
window.clearInterval(broadcastTimer);
|
window.clearInterval(broadcastTimer);
|
||||||
broadcastTimer = null;
|
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 () =>
|
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(
|
h(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
@ -567,6 +570,8 @@ function removeEmployee(employee) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.org-tree-list {
|
.org-tree-list {
|
||||||
|
--tree-connector-color: rgba(64, 158, 255, 0.34);
|
||||||
|
--tree-connector-x: calc(15px + var(--tree-level, 0) * 22px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -579,12 +584,34 @@ function removeEmployee(employee) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.org-tree-branch {
|
.org-tree-branch {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
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 {
|
.org-tree-node {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 18px 24px minmax(0, 1fr);
|
grid-template-columns: 18px 24px minmax(0, 1fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -600,6 +627,17 @@ function removeEmployee(employee) {
|
|||||||
transition: background 0.14s ease, color 0.14s ease, box-shadow 0.14s ease;
|
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:hover,
|
||||||
.org-tree-node.active {
|
.org-tree-node.active {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
@ -750,12 +788,34 @@ function removeEmployee(employee) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.org-tree-branch) {
|
:deep(.org-tree-branch) {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
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) {
|
:deep(.org-tree-node) {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 18px 24px minmax(0, 1fr);
|
grid-template-columns: 18px 24px minmax(0, 1fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -772,6 +832,17 @@ function removeEmployee(employee) {
|
|||||||
transition: background 0.14s ease, color 0.14s ease, box-shadow 0.14s ease;
|
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:hover),
|
||||||
:deep(.org-tree-node.active) {
|
:deep(.org-tree-node.active) {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user