diff --git a/src/composables/billing/useWorkspaceBilling.ts b/src/composables/billing/useWorkspaceBilling.ts index def368e17..368e80277 100644 --- a/src/composables/billing/useWorkspaceBilling.ts +++ b/src/composables/billing/useWorkspaceBilling.ts @@ -47,7 +47,7 @@ export function useWorkspaceBilling(): BillingState & BillingActions { tier: status.subscription_tier ?? null, duration: status.subscription_duration ?? null, planSlug: status.plan_slug ?? null, - renewalDate: null, // Workspace billing uses cancel_at for end date + renewalDate: status.renewal_date ?? null, endDate: status.cancel_at ?? null, isCancelled: status.subscription_status === 'canceled', hasFunds: status.has_funds diff --git a/src/composables/queue/useJobList.test.ts b/src/composables/queue/useJobList.test.ts index 0aaaa892c..684ffa636 100644 --- a/src/composables/queue/useJobList.test.ts +++ b/src/composables/queue/useJobList.test.ts @@ -134,6 +134,25 @@ vi.mock('@/stores/executionStore', () => ({ } })) +let jobPreviewStoreMock: { + previewsByPromptId: Record + isPreviewEnabled: boolean +} +const ensureJobPreviewStore = () => { + if (!jobPreviewStoreMock) { + jobPreviewStoreMock = reactive({ + previewsByPromptId: {} as Record, + isPreviewEnabled: true + }) + } + return jobPreviewStoreMock +} +vi.mock('@/stores/jobPreviewStore', () => ({ + useJobPreviewStore: () => { + return ensureJobPreviewStore() + } +})) + let workflowStoreMock: { activeWorkflow: null | { activeState?: { id?: string } } } @@ -186,6 +205,10 @@ const resetStores = () => { executionStore.activePromptId = null executionStore.executingNode = null + const jobPreviewStore = ensureJobPreviewStore() + jobPreviewStore.previewsByPromptId = {} + jobPreviewStore.isPreviewEnabled = true + const workflowStore = ensureWorkflowStore() workflowStore.activeWorkflow = null @@ -437,6 +460,44 @@ describe('useJobList', () => { expect(otherJob.computeHours).toBeCloseTo(1) }) + it('assigns preview urls for running jobs when previews enabled', async () => { + queueStoreMock.runningTasks = [ + createTask({ + promptId: 'live-preview', + queueIndex: 1, + mockState: 'running' + }) + ] + jobPreviewStoreMock.previewsByPromptId = { + 'live-preview': 'blob:preview-url' + } + jobPreviewStoreMock.isPreviewEnabled = true + + const { jobItems } = initComposable() + await flush() + + expect(jobItems.value[0].iconImageUrl).toBe('blob:preview-url') + }) + + it('omits preview urls when previews are disabled', async () => { + queueStoreMock.runningTasks = [ + createTask({ + promptId: 'disabled-preview', + queueIndex: 1, + mockState: 'running' + }) + ] + jobPreviewStoreMock.previewsByPromptId = { + 'disabled-preview': 'blob:preview-url' + } + jobPreviewStoreMock.isPreviewEnabled = false + + const { jobItems } = initComposable() + await flush() + + expect(jobItems.value[0].iconImageUrl).toBeUndefined() + }) + it('derives current node name from execution store fallbacks', async () => { const instance = initComposable() await flush() diff --git a/src/composables/queue/useJobList.ts b/src/composables/queue/useJobList.ts index 715778adb..8eda4fca5 100644 --- a/src/composables/queue/useJobList.ts +++ b/src/composables/queue/useJobList.ts @@ -7,6 +7,7 @@ import { st } from '@/i18n' import { isCloud } from '@/platform/distribution/types' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useExecutionStore } from '@/stores/executionStore' +import { useJobPreviewStore } from '@/stores/jobPreviewStore' import { useQueueStore } from '@/stores/queueStore' import type { TaskItemImpl } from '@/stores/queueStore' import type { JobState } from '@/types/queue' @@ -96,6 +97,7 @@ export function useJobList() { const { t, locale } = useI18n() const queueStore = useQueueStore() const executionStore = useExecutionStore() + const jobPreviewStore = useJobPreviewStore() const workflowStore = useWorkflowStore() const seenPendingIds = ref>(new Set()) @@ -256,6 +258,11 @@ export function useJobList() { String(task.promptId ?? '') === String(executionStore.activePromptId ?? '') const showAddedHint = shouldShowAddedHint(task, state) + const promptKey = taskIdToKey(task.promptId) + const promptPreviewUrl = + state === 'running' && jobPreviewStore.isPreviewEnabled && promptKey + ? jobPreviewStore.previewsByPromptId[promptKey] + : undefined const display = buildJobDisplay(task, state, { t, @@ -275,7 +282,7 @@ export function useJobList() { meta: display.secondary, state, iconName: display.iconName, - iconImageUrl: display.iconImageUrl, + iconImageUrl: promptPreviewUrl ?? display.iconImageUrl, showClear: display.showClear, taskRef: task, progressTotalPercent: diff --git a/src/platform/cloud/subscription/components/PricingTable.test.ts b/src/platform/cloud/subscription/components/PricingTable.test.ts index f7ac0aecd..1d24c7d13 100644 --- a/src/platform/cloud/subscription/components/PricingTable.test.ts +++ b/src/platform/cloud/subscription/components/PricingTable.test.ts @@ -1,7 +1,7 @@ import { createTestingPinia } from '@pinia/testing' import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { computed, ref } from 'vue' +import { computed, reactive, ref } from 'vue' import { createI18n } from 'vue-i18n' import PricingTable from '@/platform/cloud/subscription/components/PricingTable.vue' @@ -15,6 +15,7 @@ const mockIsYearlySubscription = ref(false) const mockAccessBillingPortal = vi.fn() const mockReportError = vi.fn() const mockTrackBeginCheckout = vi.fn() +const mockUserId = ref('user-123') const mockGetFirebaseAuthHeader = vi.fn(() => Promise.resolve({ Authorization: 'Bearer test-token' }) ) @@ -55,10 +56,11 @@ vi.mock('@/composables/useErrorHandling', () => ({ })) vi.mock('@/stores/firebaseAuthStore', () => ({ - useFirebaseAuthStore: () => ({ - getFirebaseAuthHeader: mockGetFirebaseAuthHeader, - userId: 'user-123' - }), + useFirebaseAuthStore: () => + reactive({ + getFirebaseAuthHeader: mockGetFirebaseAuthHeader, + userId: computed(() => mockUserId.value) + }), FirebaseAuthStoreError: class extends Error {} })) @@ -151,6 +153,7 @@ describe('PricingTable', () => { mockIsActiveSubscription.value = false mockSubscriptionTier.value = null mockIsYearlySubscription.value = false + mockUserId.value = 'user-123' mockTrackBeginCheckout.mockReset() vi.mocked(global.fetch).mockResolvedValue({ ok: true, @@ -201,6 +204,33 @@ describe('PricingTable', () => { expect(mockAccessBillingPortal).toHaveBeenCalledWith('pro-yearly') }) + it('should use the latest userId value when it changes after mount', async () => { + mockIsActiveSubscription.value = true + mockSubscriptionTier.value = 'STANDARD' + mockUserId.value = 'user-early' + + const wrapper = createWrapper() + await flushPromises() + + mockUserId.value = 'user-late' + + const creatorButton = wrapper + .findAll('button') + .find((btn) => btn.text().includes('Creator')) + + await creatorButton?.trigger('click') + await flushPromises() + + expect(mockTrackBeginCheckout).toHaveBeenCalledTimes(1) + expect(mockTrackBeginCheckout).toHaveBeenCalledWith({ + user_id: 'user-late', + tier: 'creator', + cycle: 'yearly', + checkout_type: 'change', + previous_tier: 'standard' + }) + }) + it('should not call accessBillingPortal when clicking current plan', async () => { mockIsActiveSubscription.value = true mockSubscriptionTier.value = 'CREATOR' diff --git a/src/platform/cloud/subscription/components/PricingTable.vue b/src/platform/cloud/subscription/components/PricingTable.vue index 0e4df0a81..3e4e8a17d 100644 --- a/src/platform/cloud/subscription/components/PricingTable.vue +++ b/src/platform/cloud/subscription/components/PricingTable.vue @@ -243,6 +243,7 @@