Workspaces 4 members invites (#8245)

## Summary

  Add team workspace member management and invite system.

## Changes

- Add members panel with role management (owner/admin/member) and member
removal
- Add invite system with email invites, pending invite display, and
revoke functionality
   - Add invite URL loading for accepting invites
  - Add subscription panel updates for member management
  - Add i18n translations for member and invite features

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8245-Workspaces-4-members-invites-2f06d73d36508176b2caf852a1505c4a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Simula_r
2026-01-24 15:52:40 -08:00
committed by GitHub
parent aa6f9b7009
commit 4771565486
31 changed files with 1704 additions and 121 deletions

View File

@@ -14,7 +14,7 @@ const mockSubscriptionTier = ref<
const mockIsYearlySubscription = ref(false)
const mockAccessBillingPortal = vi.fn()
const mockReportError = vi.fn()
const mockGetAuthHeader = vi.fn(() =>
const mockGetFirebaseAuthHeader = vi.fn(() =>
Promise.resolve({ Authorization: 'Bearer test-token' })
)
@@ -53,7 +53,7 @@ vi.mock('@/composables/useErrorHandling', () => ({
vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: () => ({
getAuthHeader: mockGetAuthHeader
getFirebaseAuthHeader: mockGetFirebaseAuthHeader
}),
FirebaseAuthStoreError: class extends Error {}
}))

View File

@@ -68,7 +68,7 @@
<script setup lang="ts">
import TabPanel from 'primevue/tabpanel'
import { defineAsyncComponent } from 'vue'
import { computed, defineAsyncComponent } from 'vue'
import CloudBadge from '@/components/topbar/CloudBadge.vue'
import Button from '@/components/ui/button/Button.vue'
@@ -85,7 +85,9 @@ const SubscriptionPanelContentWorkspace = defineAsyncComponent(
)
const { flags } = useFeatureFlags()
const teamWorkspacesEnabled = isCloud && flags.teamWorkspacesEnabled
const teamWorkspacesEnabled = computed(
() => isCloud && flags.teamWorkspacesEnabled
)
const { buildDocsUrl, docsPaths } = useExternalLink()

View File

@@ -1,10 +1,12 @@
<template>
<div class="grow overflow-auto">
<div class="grow overflow-auto pt-6">
<div class="rounded-2xl border border-interface-stroke p-6">
<div>
<div class="flex items-center justify-between gap-2">
<div
class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between md:gap-2"
>
<!-- OWNER Unsubscribed State -->
<template v-if="isOwnerUnsubscribed">
<template v-if="showSubscribePrompt">
<div class="flex flex-col gap-2">
<div class="text-sm font-bold text-text-primary">
{{ $t('subscription.workspaceNotSubscribed') }}
@@ -15,6 +17,7 @@
</div>
<Button
variant="primary"
size="lg"
class="ml-auto rounded-lg px-4 py-2 text-sm font-normal"
@click="handleSubscribeWorkspace"
>
@@ -65,12 +68,14 @@
</div>
</div>
<template
<div
v-if="isActiveSubscription && permissions.canManageSubscription"
class="flex flex-wrap gap-2 md:ml-auto"
>
<Button
size="lg"
variant="secondary"
class="ml-auto rounded-lg px-4 py-2 text-sm font-normal text-text-primary bg-interface-menu-component-surface-selected"
class="rounded-lg px-4 text-sm font-normal text-text-primary bg-interface-menu-component-surface-selected"
@click="
async () => {
await authActions.accessBillingPortal()
@@ -80,23 +85,24 @@
{{ $t('subscription.managePayment') }}
</Button>
<Button
size="lg"
variant="primary"
class="rounded-lg px-4 py-2 text-sm font-normal text-text-primary"
class="rounded-lg px-4 text-sm font-normal text-text-primary"
@click="showSubscriptionDialog"
>
{{ $t('subscription.upgradePlan') }}
</Button>
<Button
v-tooltip="{ value: $t('g.moreOptions'), showDelay: 300 }"
variant="muted-textonly"
size="icon"
variant="secondary"
size="lg"
:aria-label="$t('g.moreOptions')"
@click="planMenu?.toggle($event)"
>
<i class="pi pi-ellipsis-h" />
</Button>
<Menu ref="planMenu" :model="planMenuItems" :popup="true" />
</template>
</div>
</template>
</div>
</div>
@@ -247,6 +253,7 @@ import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useDialogService } from '@/services/dialogService'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useSubscriptionActions } from '@/platform/cloud/subscription/composables/useSubscriptionActions'
import { useSubscriptionCredits } from '@/platform/cloud/subscription/composables/useSubscriptionCredits'
@@ -264,26 +271,34 @@ import { cn } from '@/utils/tailwindUtil'
const authActions = useFirebaseAuthActions()
const workspaceStore = useTeamWorkspaceStore()
const { isWorkspaceSubscribed } = storeToRefs(workspaceStore)
const { isWorkspaceSubscribed, isInPersonalWorkspace } =
storeToRefs(workspaceStore)
const { subscribeWorkspace } = workspaceStore
const { permissions, workspaceRole } = useWorkspaceUI()
const { t, n } = useI18n()
const { showBillingComingSoonDialog } = useDialogService()
// OWNER with unsubscribed workspace - can see subscribe button
const isOwnerUnsubscribed = computed(
() => workspaceRole.value === 'owner' && !isWorkspaceSubscribed.value
)
// Show subscribe prompt to owners without active subscription
const showSubscribePrompt = computed(() => {
if (workspaceRole.value !== 'owner') return false
if (isInPersonalWorkspace.value) return !isActiveSubscription.value
return !isWorkspaceSubscribed.value
})
// MEMBER view - members can't manage subscription, show read-only zero state
const isMemberView = computed(() => !permissions.value.canManageSubscription)
// Show zero state for credits (no real billing data yet)
const showZeroState = computed(
() => isOwnerUnsubscribed.value || isMemberView.value
() => showSubscribePrompt.value || isMemberView.value
)
// Demo: Subscribe workspace to PRO monthly plan
// Subscribe workspace - show billing coming soon dialog for team workspaces
function handleSubscribeWorkspace() {
if (!isInPersonalWorkspace.value) {
showBillingComingSoonDialog()
return
}
subscribeWorkspace('PRO_MONTHLY')
}

View File

@@ -35,8 +35,8 @@ export async function performSubscriptionCheckout(
): Promise<void> {
if (!isCloud) return
const { getAuthHeader } = useFirebaseAuthStore()
const authHeader = await getAuthHeader()
const { getFirebaseAuthHeader } = useFirebaseAuthStore()
const authHeader = await getFirebaseAuthHeader()
if (!authHeader) {
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))