From 5cf6eaab157b8ceff34292fc4e8dc6ba620b32df Mon Sep 17 00:00:00 2001 From: AustinMroz Date: Fri, 17 Jul 2026 10:09:03 -0700 Subject: [PATCH] [Backport cloud/1.47] Add indicator for free tier quota system (#13747) Manual backport of #13657 to `cloud/1.47` --- .../fixtures/components/Actionbar.ts | 2 + .../fixtures/components/FreeTierQuota.ts | 21 +++ browser_tests/fixtures/selectors.ts | 3 +- browser_tests/tests/freeTierQuota.spec.ts | 63 ++++++++ src/components/TopMenuSection.test.ts | 2 +- src/components/TopMenuSection.vue | 134 +++++++++--------- src/components/actionbar/ComfyActionbar.vue | 2 + .../CloudRunButtonWrapper.test.ts | 12 +- .../ComfyRunButton/CloudRunButtonWrapper.vue | 6 +- src/composables/billing/types.ts | 1 + src/composables/billing/useBillingContext.ts | 12 ++ src/composables/node/usePriceBadge.ts | 26 +++- src/composables/useFeatureFlags.ts | 11 ++ src/locales/en/main.json | 3 + .../subscription/components/FreeTierQuota.vue | 65 +++++++++ .../composables/useFreeTierQuota.ts | 45 ++++++ src/platform/remoteConfig/types.ts | 5 + src/platform/telemetry/types.ts | 1 + .../linearMode/LinearControls.test.ts | 14 +- .../extensions/linearMode/LinearControls.vue | 16 +-- .../linearMode/PartnerNodesList.vue | 20 +-- src/scripts/app.ts | 2 + src/storybook/mocks/useBillingContext.ts | 1 + 23 files changed, 353 insertions(+), 114 deletions(-) create mode 100644 browser_tests/fixtures/components/FreeTierQuota.ts create mode 100644 browser_tests/tests/freeTierQuota.spec.ts create mode 100644 src/platform/cloud/subscription/components/FreeTierQuota.vue create mode 100644 src/platform/cloud/subscription/composables/useFreeTierQuota.ts diff --git a/browser_tests/fixtures/components/Actionbar.ts b/browser_tests/fixtures/components/Actionbar.ts index 0fad74979a..3bf96cc0dd 100644 --- a/browser_tests/fixtures/components/Actionbar.ts +++ b/browser_tests/fixtures/components/Actionbar.ts @@ -8,11 +8,13 @@ export class ComfyActionbar { public readonly root: Locator public readonly queueButton: ComfyQueueButton public readonly propertiesButton: Locator + public readonly dragHandle: Locator constructor(public readonly page: Page) { this.root = page.locator('.actionbar-container') this.queueButton = new ComfyQueueButton(this) this.propertiesButton = this.root.getByLabel('Toggle properties panel') + this.dragHandle = this.root.locator('.drag-handle') } async isDocked() { diff --git a/browser_tests/fixtures/components/FreeTierQuota.ts b/browser_tests/fixtures/components/FreeTierQuota.ts new file mode 100644 index 0000000000..798e262109 --- /dev/null +++ b/browser_tests/fixtures/components/FreeTierQuota.ts @@ -0,0 +1,21 @@ +import type { Locator } from '@playwright/test' + +import type { ComfyPage } from '@e2e/fixtures/ComfyPage' +import { TestIds } from '@e2e/fixtures/selectors' + +export class FreeTierQuota { + readonly root: Locator + + constructor(comfyPage: ComfyPage) { + this.root = comfyPage.page.getByTestId(TestIds.topbar.freeTierQuota) + } + + async getMax() { + const text = await this.root.textContent() + return text?.match(/(\d+) \/ (\d+)/)?.[2] + } + async getAvailable() { + const text = await this.root.textContent() + return text?.match(/(\d+) \/ (\d+)/)?.[1] + } +} diff --git a/browser_tests/fixtures/selectors.ts b/browser_tests/fixtures/selectors.ts index 385736733e..43023d8ee9 100644 --- a/browser_tests/fixtures/selectors.ts +++ b/browser_tests/fixtures/selectors.ts @@ -103,7 +103,8 @@ export const TestIds = { loginButtonPopoverLearnMore: 'login-button-popover-learn-more', workflowTabs: 'topbar-workflow-tabs', integratedTabBarActions: 'integrated-tab-bar-actions', - actionBarButtons: 'action-bar-buttons' + actionBarButtons: 'action-bar-buttons', + freeTierQuota: 'free-tier-quota' }, nodeLibrary: { bookmarksSection: 'node-library-bookmarks-section' diff --git a/browser_tests/tests/freeTierQuota.spec.ts b/browser_tests/tests/freeTierQuota.spec.ts new file mode 100644 index 0000000000..870311e6b8 --- /dev/null +++ b/browser_tests/tests/freeTierQuota.spec.ts @@ -0,0 +1,63 @@ +import { expect, mergeTests } from '@playwright/test' + +import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage' +import { jsonRoute } from '@e2e/fixtures/utils/jsonRoute' +import { FreeTierQuota } from '@e2e/fixtures/components/FreeTierQuota' +import { ExecutionHelper } from '@e2e/fixtures/helpers/ExecutionHelper' +import { webSocketFixture } from '@e2e/fixtures/ws' + +const wstest = mergeTests(test, webSocketFixture) + +test.describe('Free Tier Quota', { tag: ['@cloud', '@vue-nodes'] }, () => { + test.beforeEach(async ({ page }) => { + const features = { + free_tier_job_allowance_enabled: true, + free_tier_balance: { allowance: 5, remaining: 3, used: 0 } + } + await page.route('**/api/features', (r) => r.fulfill(jsonRoute(features))) + }) + + wstest('Free Tier Quota', async ({ comfyPage, comfyMouse, getWebSocket }) => { + const execution = new ExecutionHelper(comfyPage, await getWebSocket()) + const freeTierQuota = new FreeTierQuota(comfyPage) + + await test.step('Populates initial state from config', async () => { + await expect.poll(() => freeTierQuota.getAvailable()).toBe('3') + expect(await freeTierQuota.getMax()).toBe('5') + }) + + await test.step('available decrements on run', async () => { + await execution.run() + await expect.poll(() => freeTierQuota.getAvailable()).toBe('2') + }) + + await test.step('connects to detached run button', async () => { + const handle = comfyPage.actionbar.dragHandle + await comfyMouse.dragElementBy(handle, { x: -100, y: 100 }) + await expect.poll(() => comfyPage.actionbar.isDocked()).toBe(false) + expect(await freeTierQuota.getAvailable()).toBe('2') + await comfyMouse.dragElementBy(handle, { x: 100, y: -100 }) + await expect.poll(() => comfyPage.actionbar.isDocked()).toBe(true) + }) + + await test.step('Detects workflows with Partner nodes', async () => { + await comfyPage.searchBoxV2.addNode('Node With Price Badge') + const node = await comfyPage.vueNodes.getFixtureByTitle('Price Badge') + await expect.poll(() => freeTierQuota.getAvailable()).toBe(undefined) + await node.delete() + await expect.poll(() => freeTierQuota.getAvailable()).toBe('2') + }) + + await test.step('Does not decrease past 0', async () => { + await execution.run() + await expect.poll(() => freeTierQuota.getAvailable()).toBe('1') + await execution.run() + await expect.poll(() => freeTierQuota.getAvailable()).toBe(undefined) + await execution.run() + await execution.run() + await execution.run() + await comfyPage.nextFrame() + expect(await freeTierQuota.getAvailable()).toBe(undefined) + }) + }) +}) diff --git a/src/components/TopMenuSection.test.ts b/src/components/TopMenuSection.test.ts index d1fd9bdfea..2d9d3b5a56 100644 --- a/src/components/TopMenuSection.test.ts +++ b/src/components/TopMenuSection.test.ts @@ -629,7 +629,7 @@ describe('TopMenuSection', () => { await nextTick() expect(querySpy).toHaveBeenCalledTimes(1) - expect(actionbarContainer!.classList).toContain('px-2') + expect(actionbarContainer!.classList).not.toContain('w-0') } finally { unmount() vi.unstubAllGlobals() diff --git a/src/components/TopMenuSection.vue b/src/components/TopMenuSection.vue index 762dd9bcd1..0bf572b484 100644 --- a/src/components/TopMenuSection.vue +++ b/src/components/TopMenuSection.vue @@ -11,7 +11,7 @@
-
+
-
- - +
- - - - - -
- - + +
+ + + + + +
+ + +
+
@@ -147,6 +161,7 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser' import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags' import { useErrorHandling } from '@/composables/useErrorHandling' import { buildTooltipConfig } from '@/composables/useTooltipConfig' +import FreeTierQuota from '@/platform/cloud/subscription/components/FreeTierQuota.vue' import { useSettingStore } from '@/platform/settings/settingStore' import { useTelemetry } from '@/platform/telemetry' import { app } from '@/scripts/app' @@ -209,21 +224,6 @@ const hasDockedButtons = computed(() => { const isActionbarContainerEmpty = computed( () => isActionbarFloating.value && !hasDockedButtons.value ) -const actionbarContainerClass = computed(() => { - const base = - 'actionbar-container pointer-events-auto relative flex h-12 items-center gap-2 rounded-lg border bg-comfy-menu-bg shadow-interface' - - if (isActionbarContainerEmpty.value) { - return cn( - base, - '-ml-2 w-0 min-w-0 border-transparent shadow-none', - 'has-[.border-dashed]:ml-0 has-[.border-dashed]:w-auto has-[.border-dashed]:min-w-auto', - 'has-[.border-dashed]:border-interface-stroke has-[.border-dashed]:pl-2 has-[.border-dashed]:shadow-interface' - ) - } - - return cn(base, 'px-2', 'border-interface-stroke') -}) const isIntegratedTabBar = computed( () => settingStore.get('Comfy.UI.TabBarLayout') !== 'Legacy' ) diff --git a/src/components/actionbar/ComfyActionbar.vue b/src/components/actionbar/ComfyActionbar.vue index aa28b84c1b..b60784bf57 100644 --- a/src/components/actionbar/ComfyActionbar.vue +++ b/src/components/actionbar/ComfyActionbar.vue @@ -75,6 +75,7 @@
+ @@ -109,6 +110,7 @@ import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue' import Button from '@/components/ui/button/Button.vue' import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags' import { buildTooltipConfig } from '@/composables/useTooltipConfig' +import FreeTierQuota from '@/platform/cloud/subscription/components/FreeTierQuota.vue' import { useSettingStore } from '@/platform/settings/settingStore' import { useTelemetry } from '@/platform/telemetry' import { useCommandStore } from '@/stores/commandStore' diff --git a/src/components/actionbar/ComfyRunButton/CloudRunButtonWrapper.test.ts b/src/components/actionbar/ComfyRunButton/CloudRunButtonWrapper.test.ts index b1e3d3dfb8..65dce0c9c8 100644 --- a/src/components/actionbar/ComfyRunButton/CloudRunButtonWrapper.test.ts +++ b/src/components/actionbar/ComfyRunButton/CloudRunButtonWrapper.test.ts @@ -4,11 +4,11 @@ import { nextTick, ref } from 'vue' import CloudRunButtonWrapper from './CloudRunButtonWrapper.vue' -const mockIsActiveSubscription = ref(true) +const mockCanRunWorkflows = ref(true) vi.mock('@/composables/billing/useBillingContext', () => ({ useBillingContext: () => ({ - isActiveSubscription: mockIsActiveSubscription + canRunWorkflows: mockCanRunWorkflows }) })) @@ -32,7 +32,7 @@ function renderWrapper() { describe('CloudRunButtonWrapper', () => { beforeEach(() => { - mockIsActiveSubscription.value = true + mockCanRunWorkflows.value = true }) it('renders the runnable queue button when the subscription is active', () => { @@ -45,7 +45,7 @@ describe('CloudRunButtonWrapper', () => { }) it('locks the run button when the subscription is inactive', () => { - mockIsActiveSubscription.value = false + mockCanRunWorkflows.value = false renderWrapper() expect(screen.getByTestId('subscribe-to-run-button')).toBeInTheDocument() @@ -53,12 +53,12 @@ describe('CloudRunButtonWrapper', () => { }) it('unlocks the run button once the subscription becomes active again', async () => { - mockIsActiveSubscription.value = false + mockCanRunWorkflows.value = false renderWrapper() expect(screen.getByTestId('subscribe-to-run-button')).toBeInTheDocument() - mockIsActiveSubscription.value = true + mockCanRunWorkflows.value = true await nextTick() expect(screen.getByTestId('queue-button')).toBeInTheDocument() diff --git a/src/components/actionbar/ComfyRunButton/CloudRunButtonWrapper.vue b/src/components/actionbar/ComfyRunButton/CloudRunButtonWrapper.vue index 6aa543eeca..e5e8922a94 100644 --- a/src/components/actionbar/ComfyRunButton/CloudRunButtonWrapper.vue +++ b/src/components/actionbar/ComfyRunButton/CloudRunButtonWrapper.vue @@ -1,7 +1,7 @@ diff --git a/src/composables/billing/types.ts b/src/composables/billing/types.ts index e0be9c5996..c9bbfa55a9 100644 --- a/src/composables/billing/types.ts +++ b/src/composables/billing/types.ts @@ -113,4 +113,5 @@ export interface BillingContext extends BillingState, BillingActions { */ isLegacyTeamPlan: ComputedRef getMaxSeats: (tierKey: TierKey) => number + canRunWorkflows: ComputedRef } diff --git a/src/composables/billing/useBillingContext.ts b/src/composables/billing/useBillingContext.ts index b24e44f591..626be9ab26 100644 --- a/src/composables/billing/useBillingContext.ts +++ b/src/composables/billing/useBillingContext.ts @@ -6,6 +6,7 @@ import { getTierFeatures } from '@/platform/cloud/subscription/constants/tierPricing' import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing' +import { useFreeTierQuota } from '@/platform/cloud/subscription/composables/useFreeTierQuota' import type { SubscriptionDialogOptions } from '@/platform/cloud/subscription/composables/useSubscriptionDialog' import type { PreviewSubscribeOptions, @@ -129,6 +130,16 @@ function useBillingContextInternal(): BillingContext { const isFreeTier = computed(() => subscription.value?.tier === 'FREE') + const freeTierQuota = useFreeTierQuota() + + const canRunWorkflows = computed( + () => + isActiveSubscription.value && + (!isFreeTier.value || + !freeTierQuota.quotaEnabled.value || + freeTierQuota.freeTierExecutionPermitted.value) + ) + const isLegacyTeamPlan = computed( () => type.value === 'workspace' && @@ -297,6 +308,7 @@ function useBillingContextInternal(): BillingContext { isLoading, error, isActiveSubscription, + canRunWorkflows, isFreeTier, isLegacyTeamPlan, billingStatus, diff --git a/src/composables/node/usePriceBadge.ts b/src/composables/node/usePriceBadge.ts index def392b6e8..f3875e7eb4 100644 --- a/src/composables/node/usePriceBadge.ts +++ b/src/composables/node/usePriceBadge.ts @@ -1,12 +1,19 @@ +import { createSharedComposable } from '@vueuse/core' +import { computed, toValue } from 'vue' + import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph' import { LGraphBadge } from '@/lib/litegraph/src/litegraph' +import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle' import { useNodePricing } from '@/composables/node/useNodePricing' import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces' import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput' +import { trackNodePrice } from '@/renderer/extensions/vueNodes/composables/usePartitionedBadges' +import { app } from '@/scripts/app' +import { useWidgetValueStore } from '@/stores/widgetValueStore' import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { adjustColor } from '@/utils/colorUtil' -import { useWidgetValueStore } from '@/stores/widgetValueStore' +import { mapAllNodes } from '@/utils/graphTraversalUtil' type LinkedWidgetInput = INodeInputSlot & { _subgraphSlot?: SubgraphInput @@ -150,3 +157,20 @@ export const usePriceBadge = () => { updateSubgraphCredits } } +export const useCreditsBadgesInGraph = createSharedComposable(() => { + const { isCreditsBadge } = usePriceBadge() + const vueNodeLifecycle = useVueNodeLifecycle() + return computed(() => { + void vueNodeLifecycle.nodeManager.value?.vueNodeData.size + if (!app.graph) return [] + return mapAllNodes(app.graph, (node) => { + if (node.isSubgraphNode()) return + + const priceBadge = node.badges.find(isCreditsBadge) + if (!priceBadge) return + + trackNodePrice(node) + return [node.title, toValue(priceBadge).text, node.id] as const + }) + }) +}) diff --git a/src/composables/useFeatureFlags.ts b/src/composables/useFeatureFlags.ts index 2f4db71b5e..10829ab4a9 100644 --- a/src/composables/useFeatureFlags.ts +++ b/src/composables/useFeatureFlags.ts @@ -33,6 +33,7 @@ export enum ServerFeatureFlag { SHOW_SIGNIN_BUTTON = 'show_signin_button', UNIFIED_CLOUD_AUTH = 'unified_cloud_auth', CONSOLIDATED_BILLING_ENABLED = 'consolidated_billing_enabled', + FREE_TIER_JOB_ALLOWANCE_ENABLED = 'free_tier_job_allowance_enabled', SIGNUP_TURNSTILE = 'signup_turnstile' } @@ -202,6 +203,16 @@ export function useFeatureFlags() { cachedConsolidatedBillingEnabled ) }, + get freeTierJobAllowanceEnabled() { + const config = remoteConfig.value as typeof remoteConfig.value & { + free_tier_job_allowance_enabled?: boolean + } + return resolveFlag( + ServerFeatureFlag.FREE_TIER_JOB_ALLOWANCE_ENABLED, + config.free_tier_job_allowance_enabled, + false + ) + }, get signupTurnstileMode() { return resolveFlag( ServerFeatureFlag.SIGNUP_TURNSTILE, diff --git a/src/locales/en/main.json b/src/locales/en/main.json index a53dd6716f..6d6b1408d6 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -3528,6 +3528,9 @@ "dockToTop": "Dock to top", "feedback": "Feedback", "feedbackTooltip": "Feedback", + "freeTierRuns": "{available} / {MAX_AVAILABLE} runs left", + "freeTierRunsExhausted": "No runs left", + "freeTierPartner": "Partner nodes need a paid plan", "share": "Share", "shareTooltip": "Share workflow" }, diff --git a/src/platform/cloud/subscription/components/FreeTierQuota.vue b/src/platform/cloud/subscription/components/FreeTierQuota.vue new file mode 100644 index 0000000000..d9bfead271 --- /dev/null +++ b/src/platform/cloud/subscription/components/FreeTierQuota.vue @@ -0,0 +1,65 @@ + + diff --git a/src/platform/cloud/subscription/composables/useFreeTierQuota.ts b/src/platform/cloud/subscription/composables/useFreeTierQuota.ts new file mode 100644 index 0000000000..f03877e1d1 --- /dev/null +++ b/src/platform/cloud/subscription/composables/useFreeTierQuota.ts @@ -0,0 +1,45 @@ +import { createSharedComposable } from '@vueuse/core' +import { computed, ref, watch } from 'vue' + +import { useCreditsBadgesInGraph } from '@/composables/node/usePriceBadge' +import { useFeatureFlags } from '@/composables/useFeatureFlags' +import { remoteConfig } from '@/platform/remoteConfig/remoteConfig' + +export const useFreeTierQuota = createSharedComposable(function () { + const { flags } = useFeatureFlags() + const creditsBadges = useCreditsBadgesInGraph() + + const available = ref(0) + const maxAvailable = ref(0) + watch( + () => remoteConfig.value.free_tier_balance?.remaining, + (val) => (available.value = val ?? 0), + { immediate: true } + ) + watch( + () => remoteConfig.value.free_tier_balance?.allowance, + (val) => (maxAvailable.value = val ?? 0), + { immediate: true } + ) + + const quotaEnabled = computed( + () => flags.freeTierJobAllowanceEnabled && maxAvailable.value > 0 + ) + const hasInvalidNodes = computed(() => creditsBadges.value.length > 0) + const freeTierExecutionPermitted = computed( + () => !hasInvalidNodes.value && quotaEnabled.value && available.value > 0 + ) + + function trackRun() { + if (available.value > 0) available.value-- + } + + return { + available, + freeTierExecutionPermitted, + hasInvalidNodes, + maxAvailable, + quotaEnabled, + trackRun + } +}) diff --git a/src/platform/remoteConfig/types.ts b/src/platform/remoteConfig/types.ts index c3ab28322a..d4479fe09e 100644 --- a/src/platform/remoteConfig/types.ts +++ b/src/platform/remoteConfig/types.ts @@ -110,6 +110,11 @@ export type RemoteConfig = { user_secrets_enabled?: boolean node_library_essentials_enabled?: boolean free_tier_credits?: number + free_tier_balance?: { + allowance: number + used: number + remaining: number + } new_free_tier_subscriptions?: boolean workflow_sharing_enabled?: boolean comfyhub_upload_enabled?: boolean diff --git a/src/platform/telemetry/types.ts b/src/platform/telemetry/types.ts index 7534dc5d60..7848e34daf 100644 --- a/src/platform/telemetry/types.ts +++ b/src/platform/telemetry/types.ts @@ -33,6 +33,7 @@ export type PaymentIntentSource = | 'invite_member_upsell' | 'upload_model_upgrade' | 'team_upgrade_resume' + | 'free_tier_quota' export type SubscriptionCheckoutType = 'new' | 'change' export type SubscriptionCheckoutTier = TierKey | 'team' diff --git a/src/renderer/extensions/linearMode/LinearControls.test.ts b/src/renderer/extensions/linearMode/LinearControls.test.ts index a2aa743cee..4a9241ea20 100644 --- a/src/renderer/extensions/linearMode/LinearControls.test.ts +++ b/src/renderer/extensions/linearMode/LinearControls.test.ts @@ -12,7 +12,7 @@ import { useExecutionErrorStore } from '@/stores/executionErrorStore' import { toNodeId } from '@/types/nodeId' const billingMock = vi.hoisted(() => ({ - isActiveSubscription: true + canRunWorkflows: true })) const overlayMock = vi.hoisted(() => ({ @@ -22,7 +22,7 @@ const overlayMock = vi.hoisted(() => ({ vi.mock('@/composables/billing/useBillingContext', () => ({ useBillingContext: () => ({ - isActiveSubscription: billingMock.isActiveSubscription + canRunWorkflows: billingMock.canRunWorkflows }) })) @@ -77,14 +77,14 @@ const nodeErrors: Record = { function renderControls({ hasError = false, - isActiveSubscription = true, + canRunWorkflows = true, mobile = false }: { hasError?: boolean - isActiveSubscription?: boolean + canRunWorkflows?: boolean mobile?: boolean } = {}) { - billingMock.isActiveSubscription = isActiveSubscription + billingMock.canRunWorkflows = canRunWorkflows const pinia = createTestingPinia({ createSpy: vi.fn, @@ -120,7 +120,7 @@ function renderControls({ describe('LinearControls', () => { beforeEach(() => { vi.clearAllMocks() - billingMock.isActiveSubscription = true + billingMock.canRunWorkflows = true overlayMock.overlayMessage = 'KSampler is missing a required input: model' overlayMock.overlayTitle = 'Required input missing' }) @@ -187,7 +187,7 @@ describe('LinearControls', () => { ({ mobile }) => { renderControls({ hasError: true, - isActiveSubscription: false, + canRunWorkflows: false, mobile }) diff --git a/src/renderer/extensions/linearMode/LinearControls.vue b/src/renderer/extensions/linearMode/LinearControls.vue index 0877485e2f..4f05584201 100644 --- a/src/renderer/extensions/linearMode/LinearControls.vue +++ b/src/renderer/extensions/linearMode/LinearControls.vue @@ -11,6 +11,7 @@ import ScrubableNumberInput from '@/components/common/ScrubableNumberInput.vue' import Popover from '@/components/ui/Popover.vue' import Button from '@/components/ui/button/Button.vue' import { useBillingContext } from '@/composables/billing/useBillingContext' +import FreeTierQuota from '@/platform/cloud/subscription/components/FreeTierQuota.vue' import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue' import { useSettingStore } from '@/platform/settings/settingStore' import { useTelemetry } from '@/platform/telemetry' @@ -28,7 +29,7 @@ const { t } = useI18n() const commandStore = useCommandStore() const { batchCount } = storeToRefs(useQueueSettingsStore()) const settingStore = useSettingStore() -const { isActiveSubscription } = useBillingContext() +const { canRunWorkflows } = useBillingContext() const workflowStore = useWorkflowStore() const { isBuilderMode } = useAppMode() const appModeStore = useAppModeStore() @@ -54,7 +55,7 @@ const linearRunButtonTestId = 'linear-run-button' const showRunErrorWarning = computed( () => hasAnyError.value && - toValue(isActiveSubscription) && + toValue(canRunWorkflows) && toValue(overlayMessage).trim().length > 0 ) @@ -152,10 +153,7 @@ function handleDragDrop() { class="border-t border-node-component-border p-4 pb-6" > - +
@@ -210,10 +208,7 @@ function handleDragDrop() { :max="settingStore.get('Comfy.QueueButton.BatchCountLimit')" class="h-7 min-w-40" /> - + +
diff --git a/src/renderer/extensions/linearMode/PartnerNodesList.vue b/src/renderer/extensions/linearMode/PartnerNodesList.vue index f94a4af650..0db2460b16 100644 --- a/src/renderer/extensions/linearMode/PartnerNodesList.vue +++ b/src/renderer/extensions/linearMode/PartnerNodesList.vue @@ -4,33 +4,17 @@ import { CollapsibleRoot, CollapsibleTrigger } from 'reka-ui' -import { computed, toValue } from 'vue' import { useI18n } from 'vue-i18n' import Button from '@/components/ui/button/Button.vue' import Popover from '@/components/ui/Popover.vue' -import { usePriceBadge } from '@/composables/node/usePriceBadge' +import { useCreditsBadgesInGraph } from '@/composables/node/usePriceBadge' import PartnerNodeItem from '@/renderer/extensions/linearMode/PartnerNodeItem.vue' -import { trackNodePrice } from '@/renderer/extensions/vueNodes/composables/usePartitionedBadges' -import { app } from '@/scripts/app' -import { mapAllNodes } from '@/utils/graphTraversalUtil' defineProps<{ mobile?: boolean }>() -const { isCreditsBadge } = usePriceBadge() +const creditsBadges = useCreditsBadgesInGraph() const { t } = useI18n() - -const creditsBadges = computed(() => - mapAllNodes(app.graph, (node) => { - if (node.isSubgraphNode()) return - - const priceBadge = node.badges.find(isCreditsBadge) - if (!priceBadge) return - - trackNodePrice(node) - return [node.title, toValue(priceBadge).text, node.id] as const - }) -)