Compare commits

..

2 Commits

Author SHA1 Message Date
bymyself
e04225dd49 feat: remove client-side cache-busting query parameters
Remove getRandParam() and urlWithTimestamp that added cache-busting
query params (&rand=... and &t=...) to output URLs.

This is now handled by the backend generating unique filenames
with timestamps (ComfyUI PR pending).

Updated files:
- app.ts: removed getRandParam() method
- queueStore.ts: removed urlWithTimestamp getter
- imagePreviewStore.ts: removed unused imports
- useCompletionSummary.ts: use .url instead of .urlWithTimestamp
- imageCompare.ts, useMaskEditorLoader.ts, audioUtils.ts,
  Load3dUtils.ts: removed getRandParam() usage

Amp-Thread-ID: https://ampcode.com/threads/T-019c17e5-1c0a-736f-970d-e411aae222fc
2026-03-12 13:11:47 -07:00
AustinMroz
ffda940e5a Make the vue toggle ring surround toggle (#9071)
| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/3d414c6e-972e-4c87-8765-c30dc8288ddb"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/a3ec3eb4-f61f-42ac-bcf3-bc4c766040d7"/>|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9071-Make-the-vue-toggle-ring-surround-toggle-30f6d73d365081e0a427cf270ef2763a)
by [Unito](https://www.unito.io)
2026-03-12 12:00:02 -07:00
14 changed files with 32 additions and 167 deletions

View File

@@ -59,10 +59,7 @@ function mkFileUrl(props: { ref: ImageRef; preview?: boolean }): string {
}
const pathPlusQueryParams = api.apiURL(
'/view?' +
params.toString() +
app.getPreviewFormatParam() +
app.getRandParam()
'/view?' + params.toString() + app.getPreviewFormatParam()
)
const imageElement = new Image()
imageElement.crossOrigin = 'anonymous'

View File

@@ -17,7 +17,7 @@ type MockTask = {
executionEndTimestamp?: number
previewOutput?: {
isImage: boolean
urlWithTimestamp: string
url: string
}
}
@@ -94,7 +94,7 @@ describe(useQueueNotificationBanners, () => {
if (previewUrl) {
task.previewOutput = {
isImage,
urlWithTimestamp: previewUrl
url: previewUrl
}
}

View File

@@ -231,7 +231,7 @@ export const useQueueNotificationBanners = () => {
completedCount++
const preview = task.previewOutput
if (preview?.isImage) {
imagePreviews.push(preview.urlWithTimestamp)
imagePreviews.push(preview.url)
}
} else if (state === 'failed') {
failedCount++

View File

@@ -1,7 +1,6 @@
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { NodeOutputWith } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useExtensionService } from '@/services/extensionService'
type ImageCompareOutput = NodeOutputWith<{
@@ -24,11 +23,10 @@ useExtensionService().registerExtension({
onExecuted?.call(this, output)
const { a_images: aImages, b_images: bImages } = output
const rand = app.getRandParam()
const toUrl = (record: Record<string, string>) => {
const params = new URLSearchParams(record)
return api.apiURL(`/view?${params}${rand}`)
return api.apiURL(`/view?${params}`)
}
const beforeImages =

View File

@@ -2,7 +2,6 @@ import type Load3d from '@/extensions/core/load3d/Load3d'
import { t } from '@/i18n'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
class Load3dUtils {
static async generateThumbnailIfNeeded(
@@ -133,8 +132,7 @@ class Load3dUtils {
const params = [
'filename=' + encodeURIComponent(filename),
'type=' + type,
'subfolder=' + subfolder,
app.getRandParam().substring(1)
'subfolder=' + subfolder
].join('&')
return `/view?${params}`

View File

@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { api } from '@/scripts/api'
import { refreshRemoteConfig } from './refreshRemoteConfig'
import { remoteConfig, remoteConfigState } from './remoteConfig'
import { remoteConfig } from './remoteConfig'
vi.mock('@/scripts/api', () => ({
api: {
@@ -16,7 +16,7 @@ vi.stubGlobal('fetch', vi.fn())
describe('refreshRemoteConfig', () => {
const mockConfig = { feature1: true, feature2: 'value' }
function mockSuccessResponse(config: Record<string, unknown> = mockConfig) {
function mockSuccessResponse(config = mockConfig) {
return {
ok: true,
json: async () => config
@@ -123,67 +123,4 @@ describe('refreshRemoteConfig', () => {
expect(window.__CONFIG__).toEqual(existingConfig)
})
})
describe('schema validation', () => {
it('accepts a valid remote config response', async () => {
const validConfig = {
team_workspaces_enabled: true,
subscription_required: false,
max_upload_size: 1024
}
vi.mocked(api.fetchApi).mockResolvedValue(
mockSuccessResponse(validConfig)
)
await refreshRemoteConfig()
expect(remoteConfig.value).toEqual(validConfig)
expect(window.__CONFIG__).toEqual(validConfig)
expect(remoteConfigState.value).toBe('authenticated')
})
it('rejects response with invalid type for boolean flag', async () => {
const invalidConfig = {
team_workspaces_enabled: 'not-a-boolean'
}
vi.mocked(api.fetchApi).mockResolvedValue(
mockSuccessResponse(invalidConfig)
)
await refreshRemoteConfig()
expect(remoteConfig.value).toEqual({})
expect(window.__CONFIG__).toEqual({})
expect(remoteConfigState.value).toBe('error')
})
it('rejects response with invalid type for number field', async () => {
const invalidConfig = {
max_upload_size: 'not-a-number'
}
vi.mocked(api.fetchApi).mockResolvedValue(
mockSuccessResponse(invalidConfig)
)
await refreshRemoteConfig()
expect(remoteConfig.value).toEqual({})
expect(window.__CONFIG__).toEqual({})
expect(remoteConfigState.value).toBe('error')
})
it('preserves unknown keys via passthrough', async () => {
const configWithExtra = {
team_workspaces_enabled: true,
some_future_flag: 'new-value'
}
vi.mocked(api.fetchApi).mockResolvedValue(
mockSuccessResponse(configWithExtra)
)
await refreshRemoteConfig()
expect(remoteConfig.value).toEqual(configWithExtra)
})
})
})

View File

@@ -1,10 +1,6 @@
import { fromZodError } from 'zod-validation-error'
import { api } from '@/scripts/api'
import { remoteConfig, remoteConfigState } from './remoteConfig'
import { remoteConfigSchema } from './remoteConfigSchema'
import type { RemoteConfig } from './types'
interface RefreshRemoteConfigOptions {
/**
@@ -34,21 +30,7 @@ export async function refreshRemoteConfig(
: await fetch('/api/features', { cache: 'no-store' })
if (response.ok) {
const json = await response.json()
const result = remoteConfigSchema.safeParse(json)
if (!result.success) {
console.warn(
'Invalid remote config response:',
fromZodError(result.error).message
)
window.__CONFIG__ = {}
remoteConfig.value = {}
remoteConfigState.value = 'error'
return
}
const config = result.data as RemoteConfig
const config = await response.json()
window.__CONFIG__ = config
remoteConfig.value = config
remoteConfigState.value = useAuth ? 'authenticated' : 'anonymous'

View File

@@ -1,50 +0,0 @@
import { z } from 'zod'
const zServerHealthAlert = z.object({
message: z.string(),
tooltip: z.string().optional(),
severity: z.enum(['info', 'warning', 'error']).optional(),
badge: z.string().optional()
})
const zFirebaseRuntimeConfig = z.object({
apiKey: z.string(),
authDomain: z.string(),
databaseURL: z.string().optional(),
projectId: z.string(),
storageBucket: z.string(),
messagingSenderId: z.string(),
appId: z.string(),
measurementId: z.string().optional()
})
export const remoteConfigSchema = z
.object({
gtm_container_id: z.string().optional(),
ga_measurement_id: z.string().optional(),
mixpanel_token: z.string().optional(),
posthog_project_token: z.string().optional(),
posthog_api_host: z.string().optional(),
posthog_config: z.record(z.unknown()).optional(),
subscription_required: z.boolean().optional(),
server_health_alert: zServerHealthAlert.optional(),
max_upload_size: z.number().optional(),
comfy_api_base_url: z.string().optional(),
comfy_platform_base_url: z.string().optional(),
firebase_config: zFirebaseRuntimeConfig.optional(),
telemetry_disabled_events: z.array(z.string()).optional(),
model_upload_button_enabled: z.boolean().optional(),
asset_rename_enabled: z.boolean().optional(),
private_models_enabled: z.boolean().optional(),
onboarding_survey_enabled: z.boolean().optional(),
linear_toggle_enabled: z.boolean().optional(),
team_workspaces_enabled: z.boolean().optional(),
user_secrets_enabled: z.boolean().optional(),
node_library_essentials_enabled: z.boolean().optional(),
free_tier_credits: z.number().optional(),
new_free_tier_subscriptions: z.boolean().optional(),
workflow_sharing_enabled: z.boolean().optional(),
comfyhub_upload_enabled: z.boolean().optional(),
comfyhub_profile_gate_enabled: z.boolean().optional()
})
.passthrough()

View File

@@ -1,5 +1,5 @@
<template>
<WidgetLayoutField :widget>
<WidgetLayoutField v-slot="{ borderStyle }" :widget :no-border="!hasLabels">
<!-- Use ToggleGroup when explicit labels are provided -->
<ToggleGroup
v-if="hasLabels"
@@ -25,7 +25,13 @@
<!-- Use ToggleSwitch for implicit boolean states -->
<div
v-else
:class="cn('flex w-fit items-center gap-2', hideLayoutField || 'ml-auto')"
:class="
cn(
'-m-1 flex w-fit items-center gap-2 rounded-full p-1',
hideLayoutField || 'ml-auto',
borderStyle
)
"
>
<ToggleSwitch
v-model="modelValue"

View File

@@ -1,17 +1,26 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { useHideLayoutField } from '@/types/widgetTypes'
import { cn } from '@/utils/tailwindUtil'
const { rootClass } = defineProps<{
const { widget, rootClass } = defineProps<{
widget: Pick<
SimplifiedWidget<string | number | undefined>,
'name' | 'label' | 'borderStyle'
>
rootClass?: string
noBorder?: boolean
}>()
const hideLayoutField = useHideLayoutField()
const borderStyle = computed(() =>
cn(
'focus-within:ring focus-within:ring-component-node-widget-background-highlighted',
widget.borderStyle
)
)
</script>
<template>
@@ -33,15 +42,15 @@ const hideLayoutField = useHideLayoutField()
<div
:class="
cn(
'min-w-0 cursor-default rounded-lg transition-all has-focus-visible:ring has-focus-visible:ring-component-node-widget-background-highlighted',
widget.borderStyle
'min-w-0 cursor-default rounded-lg transition-all',
!noBorder && borderStyle
)
"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
>
<slot />
<slot :border-style />
</div>
</div>
</div>

View File

@@ -1,5 +1,4 @@
import type { ResultItemType } from '@/schemas/apiSchema'
import { app } from '@/scripts/app'
/**
* Format time in MM:SS format
@@ -20,8 +19,7 @@ export function getResourceURL(
const params = [
'filename=' + encodeURIComponent(filename),
'type=' + type,
'subfolder=' + subfolder,
app.getRandParam().substring(1)
'subfolder=' + subfolder
].join('&')
return `/view?${params}`

View File

@@ -382,11 +382,6 @@ export class ComfyApp {
else return ''
}
getRandParam() {
if (isCloud) return ''
return '&rand=' + Math.random()
}
static onClipspaceEditorSave() {
if (ComfyApp.clipspace_return_node) {
ComfyApp.pasteFromClipspace(ComfyApp.clipspace_return_node)

View File

@@ -117,12 +117,11 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
const outputs = getNodeOutputs(node)
if (!outputs?.images?.length) return
const rand = app.getRandParam()
const previewParam = getPreviewParam(node, outputs)
return outputs.images.map((image) => {
const params = new URLSearchParams(image)
return api.apiURL(`/view?${params}${previewParam}${rand}`)
return api.apiURL(`/view?${params}${previewParam}`)
})
}

View File

@@ -104,10 +104,6 @@ export class ResultItemImpl {
return api.apiURL('/view?' + params)
}
get urlWithTimestamp(): string {
return `${this.url}&t=${+new Date()}`
}
get isVhsFormat(): boolean {
return !!this.format && !!this.frame_rate
}