Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/components/common/WorkspaceProfilePic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<div
class="flex size-6 items-center justify-center rounded-md text-base font-semibold text-white"
:style="{
background: gradient,
textShadow: '0 1px 2px rgba(0, 0, 0, 0.2)'
}"
>
{{ letter }}
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue'

const { workspaceName } = defineProps<{
workspaceName: string
}>()

const letter = computed(() => workspaceName?.charAt(0)?.toUpperCase() ?? '?')

const gradient = computed(() => {
const seed = letter.value.charCodeAt(0)

function mulberry32(a: number) {
return function () {
let t = (a += 0x6d2b79f5)
t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}

const rand = mulberry32(seed)

const hue1 = Math.floor(rand() * 360)
const hue2 = (hue1 + 40 + Math.floor(rand() * 80)) % 360
const sat = 65 + Math.floor(rand() * 20)
const light = 55 + Math.floor(rand() * 15)

return `linear-gradient(135deg, hsl(${hue1}, ${sat}%, ${light}%), hsl(${hue2}, ${sat}%, ${light}%))`
})
</script>
25 changes: 24 additions & 1 deletion src/components/dialog/GlobalDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
v-for="item in dialogStore.dialogStack"
:key="item.key"
v-model:visible="item.visible"
class="global-dialog"
:class="[
'global-dialog',
item.key === 'global-settings' && teamWorkspacesEnabled
? 'settings-dialog-workspace'
: ''
]"
v-bind="item.dialogComponentProps"
:pt="item.dialogComponentProps.pt"
:aria-labelledby="item.key"
Expand Down Expand Up @@ -37,9 +42,17 @@

<script setup lang="ts">
import Dialog from 'primevue/dialog'
import { computed } from 'vue'

import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { isCloud } from '@/platform/distribution/types'
import { useDialogStore } from '@/stores/dialogStore'

const { flags } = useFeatureFlags()
const teamWorkspacesEnabled = computed(
() => isCloud && flags.teamWorkspacesEnabled
)

const dialogStore = useDialogStore()
</script>

Expand All @@ -56,6 +69,16 @@ const dialogStore = useDialogStore()
@apply pt-0;
}

/* Workspace mode: wider settings dialog */
.settings-dialog-workspace {
width: 100%;
max-width: 1440px;
}

.settings-dialog-workspace .p-dialog-content {
width: 100%;
}

.manager-dialog {
height: 80vh;
max-width: 1724px;
Expand Down
11 changes: 11 additions & 0 deletions src/components/dialog/content/setting/WorkspacePanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<TabPanel value="Workspace" class="h-full">
<WorkspacePanelContent />
</TabPanel>
</template>

<script setup lang="ts">
import TabPanel from 'primevue/tabpanel'

import WorkspacePanelContent from '@/components/dialog/content/setting/WorkspacePanelContent.vue'
</script>
163 changes: 163 additions & 0 deletions src/components/dialog/content/setting/WorkspacePanelContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<template>
<div class="flex h-full w-full flex-col">
<div class="pb-8 flex items-center gap-4">
<WorkspaceProfilePic
class="size-12 !text-3xl"
:workspace-name="workspaceName"
/>
<h1 class="text-3xl text-base-foreground">
{{ workspaceName }}
</h1>
</div>
<Tabs :value="activeTab" @update:value="setActiveTab">
<div class="flex w-full items-center">
<TabList class="w-full">
<Tab value="plan">{{ $t('workspacePanel.tabs.planCredits') }}</Tab>
</TabList>

<template v-if="permissions.canAccessWorkspaceMenu">
<Button
v-tooltip="{ value: $t('g.moreOptions'), showDelay: 300 }"
variant="muted-textonly"
size="icon"
:aria-label="$t('g.moreOptions')"
@click="menu?.toggle($event)"
>
<i class="pi pi-ellipsis-h" />
</Button>
<Menu ref="menu" :model="menuItems" :popup="true">
<template #item="{ item }">
<div
v-tooltip="
item.disabled && deleteTooltip
? { value: deleteTooltip, showDelay: 0 }
: null
"
:class="[
'flex items-center gap-2 px-3 py-2',
item.class,
item.disabled ? 'pointer-events-auto' : ''
]"
@click="
item.command?.({
originalEvent: $event,
item
})
"
>
<i :class="item.icon" />
<span>{{ item.label }}</span>
</div>
</template>
</Menu>
</template>
</div>

<TabPanels>
<TabPanel value="plan">
<SubscriptionPanelContent />
</TabPanel>
</TabPanels>
</Tabs>
</div>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import Menu from 'primevue/menu'
import Tab from 'primevue/tab'
import TabList from 'primevue/tablist'
import TabPanel from 'primevue/tabpanel'
import TabPanels from 'primevue/tabpanels'
import Tabs from 'primevue/tabs'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'

import WorkspaceProfilePic from '@/components/common/WorkspaceProfilePic.vue'
import Button from '@/components/ui/button/Button.vue'
import SubscriptionPanelContent from '@/platform/cloud/subscription/components/SubscriptionPanelContentWorkspace.vue'
import { useWorkspaceUI } from '@/platform/workspace/composables/useWorkspaceUI'
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
import { useDialogService } from '@/services/dialogService'

const { defaultTab = 'plan' } = defineProps<{
defaultTab?: string
}>()

const { t } = useI18n()
const {
showLeaveWorkspaceDialog,
showDeleteWorkspaceDialog,
showEditWorkspaceDialog
} = useDialogService()
const workspaceStore = useTeamWorkspaceStore()
const { workspaceName, isWorkspaceSubscribed } = storeToRefs(workspaceStore)

const { activeTab, setActiveTab, permissions, uiConfig } = useWorkspaceUI()

const menu = ref<InstanceType<typeof Menu> | null>(null)

function handleLeaveWorkspace() {
showLeaveWorkspaceDialog()
}

function handleDeleteWorkspace() {
showDeleteWorkspaceDialog()
}

function handleEditWorkspace() {
showEditWorkspaceDialog()
}

// Disable delete when workspace has an active subscription (to prevent accidental deletion)
// Use workspace's own subscription status, not the global isActiveSubscription
const isDeleteDisabled = computed(
() =>
uiConfig.value.workspaceMenuAction === 'delete' &&
isWorkspaceSubscribed.value
)

const deleteTooltip = computed(() => {
if (!isDeleteDisabled.value) return null
const tooltipKey = uiConfig.value.workspaceMenuDisabledTooltip
return tooltipKey ? t(tooltipKey) : null
})

const menuItems = computed(() => {
const items = []

// Add edit option for owners
if (uiConfig.value.showEditWorkspaceMenuItem) {
items.push({
label: t('workspacePanel.menu.editWorkspace'),
icon: 'pi pi-pencil',
command: handleEditWorkspace
})
}

const action = uiConfig.value.workspaceMenuAction
if (action === 'delete') {
items.push({
label: t('workspacePanel.menu.deleteWorkspace'),
icon: 'pi pi-trash',
class: isDeleteDisabled.value
? 'text-danger/50 cursor-not-allowed'
: 'text-danger',
disabled: isDeleteDisabled.value,
command: isDeleteDisabled.value ? undefined : handleDeleteWorkspace
})
} else if (action === 'leave') {
items.push({
label: t('workspacePanel.menu.leaveWorkspace'),
icon: 'pi pi-sign-out',
command: handleLeaveWorkspace
})
}

return items
})

onMounted(() => {
setActiveTab(defaultTab)
})
</script>
19 changes: 19 additions & 0 deletions src/components/dialog/content/setting/WorkspaceSidebarItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<div class="flex items-center gap-2">
<WorkspaceProfilePic
class="size-6 text-xs"
:workspace-name="workspaceName"
/>

<span>{{ workspaceName }}</span>
</div>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'

import WorkspaceProfilePic from '@/components/common/WorkspaceProfilePic.vue'
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'

const { workspaceName } = storeToRefs(useTeamWorkspaceStore())
</script>
Loading
Loading