Polish workspace organization selector
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-05-04 17:44:39 -04:00
parent 58c1301054
commit 664eb07201
2 changed files with 89 additions and 33 deletions

View File

@@ -165,7 +165,9 @@
.sidebar-workspace-menu { .sidebar-workspace-menu {
@apply absolute bottom-[calc(100%+0.5rem)] left-0 right-0 z-30 flex flex-col gap-1 rounded-[1.25rem] border p-2; @apply absolute bottom-[calc(100%+0.5rem)] left-0 right-0 z-30 flex flex-col gap-1 rounded-[1.25rem] border p-2;
background: rgba(255, 255, 255, 0.98); isolation: isolate;
background: #fffdf8;
background-clip: padding-box;
border-color: rgba(23, 32, 51, 0.08); border-color: rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.12); box-shadow: 0 18px 40px rgba(23, 32, 51, 0.12);
} }

View File

@@ -13,6 +13,7 @@
mdiChevronDown, mdiChevronDown,
mdiCogOutline, mdiCogOutline,
mdiPlus, mdiPlus,
mdiSwapHorizontal,
} from '@mdi/js'; } from '@mdi/js';
const router = useRouter(); const router = useRouter();
@@ -22,6 +23,7 @@
const authStore = useAuthStore(); const authStore = useAuthStore();
const isWorkspaceMenuOpen = ref(false); const isWorkspaceMenuOpen = ref(false);
const isOrganizationListOpen = ref(false);
const workspaceMenuRef = ref(null); const workspaceMenuRef = ref(null);
const activeOrganization = computed(() => organizationStore.activeOrganization); const activeOrganization = computed(() => organizationStore.activeOrganization);
@@ -36,6 +38,11 @@
}); });
const canSwitchWorkspaces = computed(() => visibleWorkspaces.value.length > 1); const canSwitchWorkspaces = computed(() => visibleWorkspaces.value.length > 1);
const canSwitchOrganizations = computed(() => organizationStore.organizations.length > 1); const canSwitchOrganizations = computed(() => organizationStore.organizations.length > 1);
const switchableOrganizations = computed(() =>
organizationStore.organizations.filter(
organization => organization.id !== organizationStore.selectedOrganizationId
)
);
const canManageWorkspaces = computed(() => const canManageWorkspaces = computed(() =>
activeOrganization.value?.currentUserPermissions?.includes(organizationPermissions.createWorkspaces) || activeOrganization.value?.currentUserPermissions?.includes(organizationPermissions.createWorkspaces) ||
activeOrganization.value?.currentUserPermissions?.includes(organizationPermissions.manageWorkspaces) || activeOrganization.value?.currentUserPermissions?.includes(organizationPermissions.manageWorkspaces) ||
@@ -57,11 +64,13 @@
} }
isWorkspaceMenuOpen.value = !isWorkspaceMenuOpen.value; isWorkspaceMenuOpen.value = !isWorkspaceMenuOpen.value;
isOrganizationListOpen.value = false;
} }
function chooseWorkspace(workspaceId) { function chooseWorkspace(workspaceId) {
workspaceStore.setActiveWorkspace(workspaceId); workspaceStore.setActiveWorkspace(workspaceId);
isWorkspaceMenuOpen.value = false; isWorkspaceMenuOpen.value = false;
isOrganizationListOpen.value = false;
} }
function chooseOrganization(organizationId) { function chooseOrganization(organizationId) {
@@ -71,21 +80,29 @@
workspace => workspace.organizationId === organizationId workspace => workspace.organizationId === organizationId
); );
workspaceStore.setActiveWorkspace(nextWorkspace?.id ?? null); workspaceStore.setActiveWorkspace(nextWorkspace?.id ?? null);
isOrganizationListOpen.value = false;
}
function toggleOrganizationList() {
isOrganizationListOpen.value = !isOrganizationListOpen.value;
} }
async function openCreateWorkspace() { async function openCreateWorkspace() {
isWorkspaceMenuOpen.value = false; isWorkspaceMenuOpen.value = false;
isOrganizationListOpen.value = false;
await router.push({ name: 'workspace-create' }); await router.push({ name: 'workspace-create' });
} }
async function openOrganizationSettings(organizationId) { async function openOrganizationSettings(organizationId) {
isWorkspaceMenuOpen.value = false; isWorkspaceMenuOpen.value = false;
isOrganizationListOpen.value = false;
await router.push({ name: 'organization-settings', params: { organizationId } }); await router.push({ name: 'organization-settings', params: { organizationId } });
} }
function handleDocumentClick(event) { function handleDocumentClick(event) {
if (workspaceMenuRef.value && !workspaceMenuRef.value.contains(event.target)) { if (workspaceMenuRef.value && !workspaceMenuRef.value.contains(event.target)) {
isWorkspaceMenuOpen.value = false; isWorkspaceMenuOpen.value = false;
isOrganizationListOpen.value = false;
} }
} }
@@ -154,36 +171,57 @@
<v-icon :icon="mdiPlus" /> <v-icon :icon="mdiPlus" />
</button> </button>
<div class="organization-switcher"> <div
<div class="organization-switcher-label"> v-if="activeOrganization"
<span>{{ t('workspaceSelector.organizationLabel') }}</span> class="organization-switcher"
<strong>{{ activeOrganizationName }}</strong> >
<div class="organization-current-row">
<button
class="user-menu-item organization-current"
type="button"
@click="openOrganizationSettings(activeOrganization.id)"
>
<span class="organization-mark">{{ activeOrganization.name.slice(0, 1).toUpperCase() }}</span>
<span class="user-menu-item-copy">
<span>{{ activeOrganizationName }}</span>
<small>{{ t('workspaceSelector.organizationLabel') }}</small>
</span>
<v-icon
:icon="mdiCogOutline"
class="organization-action-icon"
/>
</button>
</div> </div>
<button <button
v-for="organization in organizationStore.organizations" v-if="canSwitchOrganizations"
:key="organization.id" class="organization-swap-button"
class="user-menu-item organization-option"
:class="{ 'user-menu-item-active': organization.id === organizationStore.selectedOrganizationId }"
type="button" type="button"
@click="chooseOrganization(organization.id)" :aria-expanded="isOrganizationListOpen"
@click="toggleOrganizationList"
> >
<span class="organization-mark">{{ organization.name.slice(0, 1).toUpperCase() }}</span> <span>Change organization</span>
<span class="user-menu-item-copy"> <v-icon :icon="mdiSwapHorizontal" />
<span>{{ organization.name }}</span>
<small>{{ t('workspaceSelector.organizationLabel') }}</small>
</span>
</button> </button>
<button <div
v-if="activeOrganization" v-if="isOrganizationListOpen"
class="user-menu-item organization-settings-link" class="organization-options"
type="button"
@click="openOrganizationSettings(activeOrganization.id)"
> >
<span>{{ t('workspaceSelector.organizationSettings') }}</span> <button
<v-icon :icon="mdiCogOutline" /> v-for="organization in switchableOrganizations"
</button> :key="organization.id"
class="user-menu-item organization-option"
type="button"
@click="chooseOrganization(organization.id)"
>
<span class="organization-mark">{{ organization.name.slice(0, 1).toUpperCase() }}</span>
<span class="user-menu-item-copy">
<span>{{ organization.name }}</span>
<small>{{ t('workspaceSelector.organizationLabel') }}</small>
</span>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -279,18 +317,18 @@
border-color: rgba(23, 32, 51, 0.08); border-color: rgba(23, 32, 51, 0.08);
} }
.organization-switcher-label { .organization-current-row {
@apply flex flex-col gap-0.5 px-3 py-2; @apply flex w-full min-w-0 flex-col gap-1;
} }
.organization-switcher-label span { .organization-current {
@apply text-xs font-bold uppercase tracking-[0.18em]; @apply w-full min-w-0 border;
color: #7a8799; background: rgba(23, 32, 51, 0.04);
border-color: rgba(23, 32, 51, 0.08);
} }
.organization-switcher-label strong { .organization-current:hover {
@apply truncate text-sm; background: rgba(23, 32, 51, 0.07);
color: #172033;
} }
.organization-mark { .organization-mark {
@@ -299,7 +337,23 @@
color: #172033; color: #172033;
} }
.organization-settings-link { .organization-action-icon {
@apply justify-between; @apply flex-shrink-0 text-base;
color: #526178;
}
.organization-swap-button {
@apply flex w-full items-center justify-between gap-3 rounded-[0.9rem] border px-3 py-2.5 text-sm font-semibold transition-colors;
background: rgba(23, 32, 51, 0.04);
border-color: rgba(23, 32, 51, 0.08);
color: #172033;
}
.organization-swap-button:hover {
background: rgba(23, 32, 51, 0.08);
}
.organization-options {
@apply flex flex-col gap-1;
} }
</style> </style>