refactor: extract workspace selector
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
187
frontend/src/layouts/main/WorkspaceSelector.vue
Normal file
187
frontend/src/layouts/main/WorkspaceSelector.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRouter } 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 {
|
||||
mdiChevronDown,
|
||||
mdiPlus,
|
||||
} from '@mdi/js';
|
||||
|
||||
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')
|
||||
);
|
||||
|
||||
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>
|
||||
<div
|
||||
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>
|
||||
<span><small>{{ workspace.timeZone }}</small></span>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.label {
|
||||
@apply hidden text-nowrap md:inline;
|
||||
}
|
||||
|
||||
.menu-item-action {
|
||||
@apply flex h-11 items-center gap-3 rounded-full px-4 transition-colors;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
color: #172033;
|
||||
border: 1px solid rgba(23, 32, 51, 0.06);
|
||||
}
|
||||
|
||||
.menu-item-action:hover {
|
||||
background: #172033;
|
||||
color: #fffaf2;
|
||||
}
|
||||
|
||||
.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 flex-col;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.user-menu-item:hover {
|
||||
background: rgba(23, 32, 51, 0.06);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user