-
Notifications
You must be signed in to change notification settings - Fork 490
Workspaces 3 create a workspace #8221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
39e2691
64a569e
79d4762
21be6e7
5f98533
3c59ac7
7750490
c568c50
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| v-bind="item.dialogComponentProps" | ||
| :pt="item.dialogComponentProps.pt" | ||
| :aria-labelledby="item.key" | ||
|
|
@@ -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> | ||
|
|
@@ -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> | ||
| 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> |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid using Line 5 uses ♻️ Suggested fix <WorkspaceProfilePic
- class="size-12 !text-3xl"
+ class="size-12 text-3xl"
:workspace-name="workspaceName"
/>If the 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| <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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Use Per coding guidelines, use ♻️ 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 |
||||||||||||||||||
| @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> | ||||||||||||||||||
| 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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Comfy-Org/ComfyUI_frontend
Length of output: 125
🏁 Script executed:
Repository: Comfy-Org/ComfyUI_frontend
Length of output: 1492
Guard against empty workspace names to avoid
NaNgradients.An empty string produces
letter === ''(the nullish coalescing operator??only replacesnull/undefined, not empty strings), causingcharCodeAt(0)to returnNaN, which generates invalid CSS gradients.🐛 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents