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)
Comment on lines +16 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd /root && find . -type f -name "WorkspaceProfilePic.vue" 2>/dev/null | head -5

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 125


🏁 Script executed:

cat -n src/components/common/WorkspaceProfilePic.vue

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1492


Guard against empty workspace names to avoid NaN gradients.

An empty string produces letter === '' (the nullish coalescing operator ?? only replaces null/undefined, not empty strings), causing charCodeAt(0) to return NaN, which generates invalid CSS gradients.

🐛 Proposed fix
-const letter = computed(() => workspaceName?.charAt(0)?.toUpperCase() ?? '?')
+const letter = computed(
+  () => workspaceName?.trim()?.charAt(0)?.toUpperCase() || '?'
+)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { workspaceName } = defineProps<{
workspaceName: string
}>()
const letter = computed(() => workspaceName?.charAt(0)?.toUpperCase() ?? '?')
const gradient = computed(() => {
const seed = letter.value.charCodeAt(0)
const { workspaceName } = defineProps<{
workspaceName: string
}>()
const letter = computed(
() => workspaceName?.trim()?.charAt(0)?.toUpperCase() || '?'
)
const gradient = computed(() => {
const seed = letter.value.charCodeAt(0)
🤖 Prompt for AI Agents
In `@src/components/common/WorkspaceProfilePic.vue` around lines 16 - 23, The
computed `letter` can be an empty string so `letter.value.charCodeAt(0)` returns
NaN and breaks the gradient; update the `letter` computed (or the `gradient`
computed) to guard against empty workspaceName by using a falsy fallback (e.g.,
use `|| '?'` when deriving `letter`) or by checking `letter.value` before
calling `charCodeAt`, and ensure `gradient` uses a fallback numeric seed (e.g.,
charCode of '?') when `charCodeAt` would be NaN; change references to
workspaceName, letter, gradient, and the charCodeAt call accordingly so the
gradient always receives a valid integer seed.


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>
38 changes: 37 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'
: ''
]"
Comment on lines +7 to +12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Replace :class array with cn() to match repo conventions.

The array-based :class binding violates the “no :class=[]” guideline and can be simplified with cn().

♻️ Suggested refactor
+import { cn } from '@/utils/tailwindUtil'
+
-    :class="[
-      'global-dialog',
-      item.key === 'global-settings' && teamWorkspacesEnabled
-        ? 'settings-dialog-workspace'
-        : ''
-    ]"
+    :class="cn(
+      'global-dialog',
+      item.key === 'global-settings' &&
+        teamWorkspacesEnabled &&
+        'settings-dialog-workspace'
+    )"

As per coding guidelines, avoid :class arrays and use cn() for Tailwind class merging.

🤖 Prompt for AI Agents
In `@src/components/dialog/GlobalDialog.vue` around lines 7 - 12, The template in
GlobalDialog.vue uses an array-based :class binding (the existing :class with
'global-dialog' and the conditional on item.key === 'global-settings' &&
teamWorkspacesEnabled) which violates the repo guideline; replace the array with
a call to the project cn() helper to compose classes (pass 'global-dialog' plus
the conditional 'settings-dialog-workspace' when item.key === 'global-settings'
&& teamWorkspacesEnabled) so Tailwind class merging follows conventions and
avoids :class=[]. Ensure the cn() import/usage matches other components.

v-bind="item.dialogComponentProps"
:pt="item.dialogComponentProps.pt"
:aria-labelledby="item.key"
Expand Down Expand Up @@ -38,7 +43,15 @@
<script setup lang="ts">
import Dialog from 'primevue/dialog'

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

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

const dialogStore = useDialogStore()
</script>
Expand All @@ -55,4 +68,27 @@ const dialogStore = useDialogStore()
@apply p-2 2xl:p-[var(--p-dialog-content-padding)];
@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;
max-height: 1026px;
}

@media (min-width: 3000px) {
.manager-dialog {
max-width: 2200px;
max-height: 1320px;
}
}
</style>
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"
/>
Comment on lines +4 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid using ! prefix for Tailwind classes.

Line 5 uses !text-3xl which violates coding guidelines. Per guidelines, never use !important or the ! prefix for Tailwind classes; instead fix interfering styles.

♻️ Suggested fix
       <WorkspaceProfilePic
-        class="size-12 !text-3xl"
+        class="size-12 text-3xl"
         :workspace-name="workspaceName"
       />

If the ! is needed to override internal component styles, consider adding a prop to WorkspaceProfilePic to control text size instead.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<WorkspaceProfilePic
class="size-12 !text-3xl"
:workspace-name="workspaceName"
/>
<WorkspaceProfilePic
class="size-12 text-3xl"
:workspace-name="workspaceName"
/>
🤖 Prompt for AI Agents
In `@src/components/dialog/content/setting/WorkspacePanelContent.vue` around lines
4 - 7, Remove the Tailwind "!" prefix from the class on the WorkspaceProfilePic
usage — change class="size-12 !text-3xl" to class="size-12 text-3xl" and if that
breaks internal styling of WorkspaceProfilePic, add a size/textSize prop to the
WorkspaceProfilePic component (e.g., textSize or variant) and pass the desired
value from this parent (WorkspaceProfilePic :text-size="'3xl'") so the component
can apply the correct text sizing internally instead of relying on "!important"
overrides.

<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' : ''
]"
Comment on lines +36 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Use cn() utility instead of array class binding.

Per coding guidelines, use cn() from @/utils/tailwindUtil to merge Tailwind class names instead of :class="[]".

♻️ Suggested refactor
+import { cn } from '@/utils/tailwindUtil'
+
+// In computed or inline:
+const menuItemClass = (item: MenuItem) => cn(
+  'flex items-center gap-2 px-3 py-2',
+  item.class,
+  item.disabled && 'pointer-events-auto'
+)

Then in template:

               <div
                 v-tooltip="..."
-                :class="[
-                  'flex items-center gap-2 px-3 py-2',
-                  item.class,
-                  item.disabled ? 'pointer-events-auto' : ''
-                ]"
+                :class="menuItemClass(item)"
🤖 Prompt for AI Agents
In `@src/components/dialog/content/setting/WorkspacePanelContent.vue` around lines
36 - 40, Replace the array-style :class binding in WorkspacePanelContent.vue
with the cn() utility from '@/utils/tailwindUtil': import cn in the component
script and change :class="['flex items-center gap-2 px-3 py-2', item.class,
item.disabled ? 'pointer-events-auto' : '']" to use :class="cn('flex
items-center gap-2 px-3 py-2', item.class, item.disabled ? 'pointer-events-auto'
: '')", preserving the conditional for item.disabled; ensure the cn import is
added to the script/setup or script block where the component is defined.

@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