refactor: extract workspace selector
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
||||||
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
import WorkspaceSelector from './WorkspaceSelector.vue';
|
||||||
import {
|
import {
|
||||||
mdiChevronDown,
|
|
||||||
mdiCogOutline,
|
mdiCogOutline,
|
||||||
mdiLogin,
|
mdiLogin,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
@@ -23,20 +22,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const workspaceStore = useWorkspaceStore();
|
|
||||||
const authStore = useAuthStore();
|
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(() => {
|
const appBarActions = computed(() => {
|
||||||
if (!authStore.isAuthenticated) {
|
if (!authStore.isAuthenticated) {
|
||||||
return [];
|
return [];
|
||||||
@@ -81,38 +69,6 @@
|
|||||||
return [];
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -134,52 +90,9 @@
|
|||||||
|
|
||||||
<div class="side-menu">
|
<div class="side-menu">
|
||||||
<div class="side-menu-items side-menu-left">
|
<div class="side-menu-items side-menu-left">
|
||||||
<div
|
<WorkspaceSelector
|
||||||
v-if="authStore.isAuthenticated"
|
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>
|
||||||
|
|
||||||
<div class="side-menu-items side-menu-right">
|
<div class="side-menu-items side-menu-right">
|
||||||
@@ -283,73 +196,6 @@
|
|||||||
@apply text-xl;
|
@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 {
|
.menu-action-link {
|
||||||
@apply no-underline;
|
@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