refactor: extract workspace selector

This commit is contained in:
2026-04-30 01:44:03 -04:00
parent fcd80cd30f
commit d222e33667
2 changed files with 192 additions and 159 deletions

View File

@@ -1,11 +1,10 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
import WorkspaceSelector from './WorkspaceSelector.vue';
import {
mdiChevronDown,
mdiCogOutline,
mdiLogin,
mdiPlus,
@@ -23,20 +22,9 @@
});
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const workspaceStore = useWorkspaceStore();
const authStore = useAuthStore();
const isWorkspaceMenuOpen = ref(false);
const workspaceMenuRef = ref(null);
const canSwitchWorkspaces = computed(() => workspaceStore.workspaces.length > 1);
const canManageWorkspaces = computed(() => authStore.isManager);
const canOpenWorkspaceMenu = computed(() => canSwitchWorkspaces.value || canManageWorkspaces.value);
const activeWorkspaceName = computed(() =>
workspaceStore.activeWorkspace?.name || t('nav.noWorkspace')
);
const appBarActions = computed(() => {
if (!authStore.isAuthenticated) {
return [];
@@ -81,38 +69,6 @@
return [];
}
});
function toggleWorkspaceMenu() {
if (!canOpenWorkspaceMenu.value) {
return;
}
isWorkspaceMenuOpen.value = !isWorkspaceMenuOpen.value;
}
function chooseWorkspace(workspaceId) {
workspaceStore.setActiveWorkspace(workspaceId);
isWorkspaceMenuOpen.value = false;
}
async function openCreateWorkspace() {
isWorkspaceMenuOpen.value = false;
await router.push({ name: 'workspace-create' });
}
function handleDocumentClick(event) {
if (workspaceMenuRef.value && !workspaceMenuRef.value.contains(event.target)) {
isWorkspaceMenuOpen.value = false;
}
}
onMounted(() => {
document.addEventListener('click', handleDocumentClick);
});
onBeforeUnmount(() => {
document.removeEventListener('click', handleDocumentClick);
});
</script>
<template>
@@ -134,52 +90,9 @@
<div class="side-menu">
<div class="side-menu-items side-menu-left">
<div
<WorkspaceSelector
v-if="authStore.isAuthenticated"
ref="workspaceMenuRef"
class="user-menu-wrap"
>
<button
class="menu-item-action workspace-trigger"
:class="{ 'workspace-trigger-static': !canOpenWorkspaceMenu }"
@click.stop="toggleWorkspaceMenu"
>
<span class="workspace-trigger-mark">W</span>
<span class="label workspace-trigger-label">{{ activeWorkspaceName }}</span>
<v-icon
v-if="canOpenWorkspaceMenu"
:icon="mdiChevronDown"
class="user-trigger-icon"
:class="{ 'user-trigger-icon-open': isWorkspaceMenuOpen }"
/>
</button>
<div
v-if="isWorkspaceMenuOpen"
class="user-menu"
>
<button
v-for="workspace in workspaceStore.workspaces"
:key="workspace.id"
class="user-menu-item"
:class="{ 'user-menu-item-active': workspace.id === workspaceStore.activeWorkspaceId }"
@click="chooseWorkspace(workspace.id)"
>
<span>{{ workspace.name }}</span>
<small>{{ workspace.timeZone }}</small>
</button>
<button
v-if="canManageWorkspaces"
class="user-menu-item user-menu-item-create"
type="button"
@click="openCreateWorkspace"
>
<span>{{ t('workspaceSelector.createAction') }}</span>
<v-icon :icon="mdiPlus" />
</button>
</div>
</div>
/>
</div>
<div class="side-menu-items side-menu-right">
@@ -283,73 +196,6 @@
@apply text-xl;
}
.user-menu-wrap {
@apply relative;
z-index: 20;
}
.workspace-trigger {
@apply max-w-[18rem] pl-2 pr-3;
}
.user-trigger-icon {
@apply text-base;
}
.user-trigger-icon-open {
transform: rotate(180deg);
}
.workspace-trigger-static {
cursor: default;
}
.workspace-trigger-mark {
@apply flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-xl text-xs font-black uppercase;
background: linear-gradient(135deg, rgba(255, 138, 61, 0.16), rgba(239, 68, 68, 0.14));
color: #c2410c;
}
.workspace-trigger-label {
@apply max-w-[11rem] truncate;
}
.user-menu {
@apply absolute right-0 top-[calc(100%+0.75rem)] flex min-w-[14rem] flex-col gap-1 rounded-[1.25rem] border p-2;
background: rgba(255, 255, 255, 0.96);
border-color: rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.12);
z-index: 40;
}
.user-menu-item {
@apply flex items-center gap-3 rounded-[0.9rem] px-3 py-3 text-left text-sm font-semibold transition-colors;
color: #172033;
}
.user-menu-item:hover {
background: rgba(23, 32, 51, 0.06);
}
.user-menu-item-danger {
color: #b91c1c;
}
.user-menu-item-active {
background: rgba(255, 138, 61, 0.12);
color: #c2410c;
}
.user-menu-item small {
@apply ml-auto text-xs font-medium;
color: #526178;
}
.user-menu-item-create {
@apply justify-between border border-dashed;
border-color: rgba(23, 32, 51, 0.12);
}
.menu-action-link {
@apply no-underline;
}