Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
507e667795 | ||
|
|
d617ab1c95 | ||
|
|
b93338cac7 | ||
|
|
fa2d187c83 | ||
|
|
6f22ca11c3 |
@@ -38,7 +38,7 @@ test.describe('Download page @smoke', () => {
|
||||
level: 1
|
||||
})
|
||||
})
|
||||
const downloadBtn = hero.getByRole('link', { name: /DOWNLOAD LOCAL/i })
|
||||
const downloadBtn = hero.getByRole('link', { name: /DOWNLOAD DESKTOP/i })
|
||||
await expect(downloadBtn).toBeVisible()
|
||||
await expect(downloadBtn).toHaveAttribute('target', '_blank')
|
||||
|
||||
@@ -176,7 +176,7 @@ test.describe('Download page mobile @mobile', () => {
|
||||
level: 1
|
||||
})
|
||||
})
|
||||
const downloadBtn = hero.getByRole('link', { name: /DOWNLOAD LOCAL/i })
|
||||
const downloadBtn = hero.getByRole('link', { name: /DOWNLOAD DESKTOP/i })
|
||||
const githubBtn = hero.getByRole('link', { name: /INSTALL FROM GITHUB/i })
|
||||
|
||||
await expect(downloadBtn).toBeVisible()
|
||||
|
||||
@@ -213,7 +213,7 @@ test.describe('Get started section links @smoke', () => {
|
||||
has: page.getByRole('heading', { name: 'Get started in minutes' })
|
||||
})
|
||||
|
||||
const downloadLink = section.getByRole('link', { name: 'Download Local' })
|
||||
const downloadLink = section.getByRole('link', { name: 'Download Desktop' })
|
||||
await expect(downloadLink).toBeVisible()
|
||||
await expect(downloadLink).toHaveAttribute('href', '/download')
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ test.describe('Desktop navigation @smoke', () => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopCTA = nav.getByTestId('desktop-nav-cta')
|
||||
await expect(
|
||||
desktopCTA.getByRole('link', { name: 'DOWNLOAD LOCAL' })
|
||||
desktopCTA.getByRole('link', { name: 'DOWNLOAD DESKTOP' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
desktopCTA.getByRole('link', { name: 'LAUNCH CLOUD' })
|
||||
@@ -55,7 +55,7 @@ test.describe('Desktop dropdown @interaction', () => {
|
||||
|
||||
const dropdown = productsButton.locator('..').getByTestId('nav-dropdown')
|
||||
for (const item of [
|
||||
'Comfy Local',
|
||||
'Comfy Desktop',
|
||||
'Comfy Cloud',
|
||||
'Comfy API',
|
||||
'Comfy Enterprise'
|
||||
@@ -69,7 +69,7 @@ test.describe('Desktop dropdown @interaction', () => {
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
await desktopLinks.getByRole('button', { name: /PRODUCTS/i }).hover()
|
||||
|
||||
const comfyLocal = nav.getByRole('link', { name: 'Comfy Local' }).first()
|
||||
const comfyLocal = nav.getByRole('link', { name: 'Comfy Desktop' }).first()
|
||||
await expect(comfyLocal).toBeVisible()
|
||||
|
||||
await page.locator('main').hover()
|
||||
@@ -81,7 +81,7 @@ test.describe('Desktop dropdown @interaction', () => {
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
await desktopLinks.getByRole('button', { name: /PRODUCTS/i }).hover()
|
||||
|
||||
const comfyLocal = nav.getByRole('link', { name: 'Comfy Local' }).first()
|
||||
const comfyLocal = nav.getByRole('link', { name: 'Comfy Desktop' }).first()
|
||||
await expect(comfyLocal).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
@@ -121,7 +121,7 @@ test.describe('Mobile menu @mobile', () => {
|
||||
const menu = page.locator('#site-mobile-menu')
|
||||
await menu.getByText('PRODUCTS').first().click()
|
||||
|
||||
await expect(menu.getByText('Comfy Local')).toBeVisible()
|
||||
await expect(menu.getByText('Comfy Desktop')).toBeVisible()
|
||||
await expect(menu.getByText('Comfy Cloud')).toBeVisible()
|
||||
|
||||
await menu.getByRole('button', { name: /BACK/i }).click()
|
||||
@@ -133,7 +133,7 @@ test.describe('Mobile menu @mobile', () => {
|
||||
|
||||
const menu = page.locator('#site-mobile-menu')
|
||||
await expect(
|
||||
menu.getByRole('link', { name: 'DOWNLOAD LOCAL' })
|
||||
menu.getByRole('link', { name: 'DOWNLOAD DESKTOP' })
|
||||
).toBeVisible()
|
||||
await expect(menu.getByRole('link', { name: 'LAUNCH CLOUD' })).toBeVisible()
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
@@ -2,7 +2,7 @@
|
||||
const {
|
||||
logoSrc = '/icons/logo.svg',
|
||||
logoAlt = 'Comfy',
|
||||
text = 'LOCAL'
|
||||
text = 'DESKTOP'
|
||||
} = defineProps<{
|
||||
logoSrc?: string
|
||||
logoAlt?: string
|
||||
@@ -20,7 +20,7 @@ const {
|
||||
/>
|
||||
|
||||
<span
|
||||
class="bg-primary-comfy-yellow text-primary-comfy-ink my-auto flex h-12 items-center justify-center lg:my-0 lg:h-auto lg:p-8"
|
||||
class="bg-primary-comfy-yellow my-auto flex h-12 items-center justify-center text-primary-comfy-ink lg:my-0 lg:h-auto lg:p-8"
|
||||
>
|
||||
<img
|
||||
:src="logoSrc"
|
||||
@@ -37,7 +37,7 @@ const {
|
||||
/>
|
||||
|
||||
<span
|
||||
class="bg-primary-comfy-yellow text-primary-comfy-ink my-auto flex h-7.25 items-center justify-center lg:h-15.5 lg:px-6"
|
||||
class="bg-primary-comfy-yellow my-auto flex h-7.25 items-center justify-center text-primary-comfy-ink lg:h-15.5 lg:px-6"
|
||||
>
|
||||
<span
|
||||
class="inline-block translate-y-0.5 text-2xl leading-none font-bold lg:text-3xl"
|
||||
|
||||
@@ -94,7 +94,7 @@ const ctaButtons = [
|
||||
{
|
||||
label: t('nav.downloadLocal', locale),
|
||||
prefix: 'DOWNLOAD',
|
||||
core: 'LOCAL',
|
||||
core: 'DESKTOP',
|
||||
href: routes.download,
|
||||
primary: false
|
||||
},
|
||||
@@ -164,7 +164,7 @@ onMounted(() => {
|
||||
/>
|
||||
|
||||
<nav
|
||||
class="bg-primary-comfy-ink fixed inset-x-0 top-0 z-50 flex items-center justify-between gap-4 px-6 py-5 lg:gap-4 lg:px-[clamp(0.25rem,4vw,5rem)] lg:py-8"
|
||||
class="fixed inset-x-0 top-0 z-50 flex items-center justify-between gap-4 bg-primary-comfy-ink px-6 py-5 lg:gap-4 lg:px-[clamp(0.25rem,4vw,5rem)] lg:py-8"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
<a
|
||||
|
||||
@@ -283,16 +283,16 @@ onUnmounted(() => {
|
||||
<div
|
||||
class="relative z-10 mt-17 w-full px-4 pb-16 lg:mt-0 lg:min-w-160 lg:flex-1 lg:translate-x-[10%] lg:px-20 lg:py-14"
|
||||
>
|
||||
<ProductHeroBadge />
|
||||
<ProductHeroBadge text="DESKTOP" />
|
||||
|
||||
<h1
|
||||
class="text-primary-comfy-canvas mt-6 text-3xl/tight font-light whitespace-pre-line md:text-4xl/tight lg:max-w-2xl lg:text-5xl/tight"
|
||||
class="mt-6 text-3xl/tight font-light whitespace-pre-line text-primary-comfy-canvas md:text-4xl/tight lg:max-w-2xl lg:text-5xl/tight"
|
||||
>
|
||||
{{ t('download.hero.heading', locale) }}
|
||||
</h1>
|
||||
|
||||
<p
|
||||
class="text-primary-comfy-canvas mt-6 max-w-md text-sm lg:mt-6 lg:text-base"
|
||||
class="mt-6 max-w-md text-sm text-primary-comfy-canvas lg:mt-6 lg:text-base"
|
||||
>
|
||||
{{ t('download.hero.subtitle', locale) }}
|
||||
</p>
|
||||
|
||||
@@ -124,8 +124,8 @@ const translations = {
|
||||
'zh-CN': '下载或注册'
|
||||
},
|
||||
'getStarted.step1.downloadLocal': {
|
||||
en: 'Download Local',
|
||||
'zh-CN': '下载本地版'
|
||||
en: 'Download Desktop',
|
||||
'zh-CN': '下载桌面版'
|
||||
},
|
||||
'getStarted.step1.launchCloud': {
|
||||
en: 'Launch Cloud',
|
||||
@@ -605,8 +605,8 @@ const translations = {
|
||||
'是的。基于 GPL-3.0 免费开源。没有功能限制、没有试用期、没有附加条件。'
|
||||
},
|
||||
'download.faq.4.q': {
|
||||
en: 'Why would I pay for Comfy Cloud if Local is free?',
|
||||
'zh-CN': '既然本地版免费,为什么还要付费使用 Comfy Cloud?'
|
||||
en: 'Why would I pay for Comfy Cloud if Desktop is free?',
|
||||
'zh-CN': '既然桌面版免费,为什么还要付费使用 Comfy Cloud?'
|
||||
},
|
||||
'download.faq.4.a': {
|
||||
en: 'Your machine or ours. Cloud gives you powerful GPUs on demand, pre-loaded models, end-to-end security and infrastructure out of the box and partner models cleared for commercial use.',
|
||||
@@ -623,8 +623,8 @@ const translations = {
|
||||
'Desktop:一键安装,自动更新。Portable:独立构建,可从任意文件夹运行。CLI:从 GitHub 克隆,完全开发者控制,适合想自定义环境或参与上游贡献的开发者。'
|
||||
},
|
||||
'download.faq.6.q': {
|
||||
en: 'Can I use my local workflows in Comfy Cloud?',
|
||||
'zh-CN': '我可以在 Comfy Cloud 中使用本地工作流吗?'
|
||||
en: 'Can I use my Desktop workflows in Comfy Cloud?',
|
||||
'zh-CN': '我可以在 Comfy Cloud 中使用桌面工作流吗?'
|
||||
},
|
||||
'download.faq.6.a': {
|
||||
en: 'Yes — same file, same results. No conversion, no rework.',
|
||||
@@ -665,8 +665,8 @@ const translations = {
|
||||
'zh-CN': '专业人士为何\n选择'
|
||||
},
|
||||
'download.reason.headingHighlight': {
|
||||
en: 'Local',
|
||||
'zh-CN': '本地版'
|
||||
en: 'Desktop',
|
||||
'zh-CN': '桌面版'
|
||||
},
|
||||
'download.reason.1.title': {
|
||||
en: 'Unlimited\nCustomization',
|
||||
@@ -715,8 +715,8 @@ const translations = {
|
||||
'zh-CN': '完整的 ComfyUI 引擎——开源、快速、可扩展,随你运行。'
|
||||
},
|
||||
'download.hero.downloadLocal': {
|
||||
en: 'DOWNLOAD LOCAL',
|
||||
'zh-CN': '下载本地版'
|
||||
en: 'DOWNLOAD DESKTOP',
|
||||
'zh-CN': '下载桌面版'
|
||||
},
|
||||
'download.hero.installGithub': {
|
||||
en: 'INSTALL FROM GITHUB',
|
||||
@@ -1810,7 +1810,7 @@ const translations = {
|
||||
'nav.community': { en: 'Community', 'zh-CN': '社区' },
|
||||
'nav.resources': { en: 'Resources', 'zh-CN': '资源' },
|
||||
'nav.company': { en: 'Company', 'zh-CN': '公司' },
|
||||
'nav.comfyLocal': { en: 'Comfy Local', 'zh-CN': 'Comfy 本地版' },
|
||||
'nav.comfyLocal': { en: 'Comfy Desktop', 'zh-CN': 'Comfy 桌面版' },
|
||||
'nav.comfyCloud': { en: 'Comfy Cloud', 'zh-CN': 'Comfy Cloud' },
|
||||
'nav.comfyApi': { en: 'Comfy API', 'zh-CN': 'Comfy API' },
|
||||
'nav.comfyEnterprise': {
|
||||
@@ -1828,7 +1828,7 @@ const translations = {
|
||||
'nav.aboutUs': { en: 'About Us', 'zh-CN': '关于我们' },
|
||||
'nav.careers': { en: 'Careers', 'zh-CN': '招聘' },
|
||||
'nav.customerStories': { en: 'Customer Stories', 'zh-CN': '客户故事' },
|
||||
'nav.downloadLocal': { en: 'DOWNLOAD LOCAL', 'zh-CN': '下载本地版' },
|
||||
'nav.downloadLocal': { en: 'DOWNLOAD DESKTOP', 'zh-CN': '下载桌面版' },
|
||||
'nav.launchCloud': { en: 'LAUNCH CLOUD', 'zh-CN': '启动云端' },
|
||||
'nav.menu': { en: 'Menu', 'zh-CN': '菜单' },
|
||||
'nav.toggleMenu': { en: 'Toggle menu', 'zh-CN': '切换菜单' },
|
||||
@@ -1879,8 +1879,9 @@ const translations = {
|
||||
'如果我们的网站包含指向第三方网站和服务的链接,请注意这些网站和服务有自己的隐私政策。在访问任何第三方内容的链接后,您应阅读其发布的关于如何收集和使用个人信息的隐私政策信息。本隐私政策不适用于您离开我们网站后的任何活动。'
|
||||
},
|
||||
'privacy.intro.block.3': {
|
||||
en: 'This policy is effective as of April 18, 2025.',
|
||||
'zh-CN': '本政策自 2025 年 4 月 18 日起生效。'
|
||||
en: 'This policy is effective as of April 18, 2025. For information specific to Comfy Desktop (the local install application), including named processors, lawful basis under GDPR/UK GDPR, retention periods, and your rights, see our <a href="/privacy/desktop" class="text-white underline">Desktop Privacy Policy</a>.',
|
||||
'zh-CN':
|
||||
'本政策自 2025 年 4 月 18 日起生效。有关 Comfy Desktop(本地安装应用程序)的具体信息,包括指定的数据处理方、GDPR/UK GDPR 下的合法依据、保留期限以及您的权利,请参阅我们的<a href="/zh-CN/privacy/desktop" class="text-white underline">Desktop 隐私政策</a>。'
|
||||
},
|
||||
'privacy.information-we-collect.label': {
|
||||
en: 'INFORMATION',
|
||||
@@ -2130,6 +2131,181 @@ const translations = {
|
||||
'<a href="mailto:support@comfy.org" class="text-white underline">support@comfy.org</a>'
|
||||
},
|
||||
|
||||
// ── Desktop Privacy Policy ────────────────────────────────────────
|
||||
'desktop_privacy.intro.label': { en: 'OVERVIEW', 'zh-CN': 'OVERVIEW' },
|
||||
'desktop_privacy.intro.block.0': {
|
||||
en: 'Effective 3 June 2026. Applies to the Comfy Desktop application.',
|
||||
'zh-CN': 'Effective 3 June 2026. Applies to the Comfy Desktop application.'
|
||||
},
|
||||
'desktop_privacy.intro.block.1': {
|
||||
en: 'This Privacy Policy describes the personal data we process when you use Comfy Desktop, the purposes and lawful bases for that processing, the recipients of the data, and the rights available to you. The same policy is shown in the application on first run and is available at any time from Settings → About → Privacy Policy.',
|
||||
'zh-CN':
|
||||
'This Privacy Policy describes the personal data we process when you use Comfy Desktop, the purposes and lawful bases for that processing, the recipients of the data, and the rights available to you. The same policy is shown in the application on first run and is available at any time from Settings → About → Privacy Policy.'
|
||||
},
|
||||
|
||||
'desktop_privacy.controller.label': {
|
||||
en: 'CONTROLLER',
|
||||
'zh-CN': 'CONTROLLER'
|
||||
},
|
||||
'desktop_privacy.controller.title': {
|
||||
en: 'Controller',
|
||||
'zh-CN': 'Controller'
|
||||
},
|
||||
'desktop_privacy.controller.block.0': {
|
||||
en: 'Comfy Organization Inc ("Comfy Org", "we", "us") is the data controller for personal data processed in connection with your use of Comfy Desktop. We are established in San Francisco, USA. For privacy enquiries: <a href="mailto:support@comfy.org" class="text-white underline">support@comfy.org</a>.',
|
||||
'zh-CN':
|
||||
'Comfy Organization Inc ("Comfy Org", "we", "us") is the data controller for personal data processed in connection with your use of Comfy Desktop. We are established in San Francisco, USA. For privacy enquiries: <a href="mailto:support@comfy.org" class="text-white underline">support@comfy.org</a>.'
|
||||
},
|
||||
|
||||
'desktop_privacy.data.label': {
|
||||
en: 'DATA WE PROCESS',
|
||||
'zh-CN': 'DATA WE PROCESS'
|
||||
},
|
||||
'desktop_privacy.data.title': {
|
||||
en: 'Personal data we process',
|
||||
'zh-CN': 'Personal data we process'
|
||||
},
|
||||
'desktop_privacy.data.block.0': {
|
||||
en: 'If you have enabled telemetry, either on the first-run consent screen or at Settings → Telemetry, we process the following categories of data:',
|
||||
'zh-CN':
|
||||
'If you have enabled telemetry, either on the first-run consent screen or at Settings → Telemetry, we process the following categories of data:'
|
||||
},
|
||||
'desktop_privacy.data.block.1': {
|
||||
en: 'Device identifier. A pseudonymous identifier generated locally on first run. Before you sign in to Comfy Cloud it is not linked to your name, email address, or hardware. When you sign in, it is associated with your Comfy account.\nTechnical metadata. Application version, operating system, and processor architecture.\nProduct usage events. Feature interactions, navigation between views, installation and update milestones, and approximate timing.\nCustom node identifiers. Public package names of custom nodes you install through Manager (for example, "comfyui-impact-pack"). The local installation path is not transmitted.\nCrash and error diagnostics. Stack traces, error messages, and short stdout/stderr fragments captured at the moment of failure.',
|
||||
'zh-CN':
|
||||
'Device identifier. A pseudonymous identifier generated locally on first run. Before you sign in to Comfy Cloud it is not linked to your name, email address, or hardware. When you sign in, it is associated with your Comfy account.\nTechnical metadata. Application version, operating system, and processor architecture.\nProduct usage events. Feature interactions, navigation between views, installation and update milestones, and approximate timing.\nCustom node identifiers. Public package names of custom nodes you install through Manager (for example, "comfyui-impact-pack"). The local installation path is not transmitted.\nCrash and error diagnostics. Stack traces, error messages, and short stdout/stderr fragments captured at the moment of failure.'
|
||||
},
|
||||
'desktop_privacy.data.block.2': {
|
||||
en: 'Before crash or error diagnostic data is transmitted, we apply automated redaction to home-directory paths and to well-known credential patterns (Bearer tokens, OpenAI <code>sk-*</code> and Hugging Face <code>hf_*</code> keys, basic-auth URLs, and <code>KEY=</code> / <code>SECRET=</code> environment assignments).',
|
||||
'zh-CN':
|
||||
'Before crash or error diagnostic data is transmitted, we apply automated redaction to home-directory paths and to well-known credential patterns (Bearer tokens, OpenAI <code>sk-*</code> and Hugging Face <code>hf_*</code> keys, basic-auth URLs, and <code>KEY=</code> / <code>SECRET=</code> environment assignments).'
|
||||
},
|
||||
'desktop_privacy.data.block.3': {
|
||||
en: 'We do not process:',
|
||||
'zh-CN': 'We do not process:'
|
||||
},
|
||||
'desktop_privacy.data.block.4': {
|
||||
en: 'Workflow content (the graph, the nodes you connect, their parameters)\nPrompts you write\nGenerated images, video, or audio\nModel weights, or the local filenames under which you save them\nNetwork activity outside the application',
|
||||
'zh-CN':
|
||||
'Workflow content (the graph, the nodes you connect, their parameters)\nPrompts you write\nGenerated images, video, or audio\nModel weights, or the local filenames under which you save them\nNetwork activity outside the application'
|
||||
},
|
||||
'desktop_privacy.data.block.5': {
|
||||
en: 'Your workflow files, your models, the outputs you generate, the list of installations you create, and your local settings remain on your device. They are not transmitted to Comfy Org, and they are not accessible to us.',
|
||||
'zh-CN':
|
||||
'Your workflow files, your models, the outputs you generate, the list of installations you create, and your local settings remain on your device. They are not transmitted to Comfy Org, and they are not accessible to us.'
|
||||
},
|
||||
|
||||
'desktop_privacy.purposes.label': { en: 'PURPOSES', 'zh-CN': 'PURPOSES' },
|
||||
'desktop_privacy.purposes.title': {
|
||||
en: 'Purposes and lawful bases',
|
||||
'zh-CN': 'Purposes and lawful bases'
|
||||
},
|
||||
'desktop_privacy.purposes.block.0': {
|
||||
en: 'We process personal data on the following lawful bases under GDPR and UK GDPR:',
|
||||
'zh-CN':
|
||||
'We process personal data on the following lawful bases under GDPR and UK GDPR:'
|
||||
},
|
||||
'desktop_privacy.purposes.block.1': {
|
||||
en: 'Product usage analytics: consent under Article 6(1)(a).\nCrash and error diagnostics: consent under Article 6(1)(a).\nDelivery of software updates and integrity verification: legitimate interests under Article 6(1)(f).\nAuthentication when you sign in to Comfy Cloud: performance of a contract under Article 6(1)(b).',
|
||||
'zh-CN':
|
||||
'Product usage analytics: consent under Article 6(1)(a).\nCrash and error diagnostics: consent under Article 6(1)(a).\nDelivery of software updates and integrity verification: legitimate interests under Article 6(1)(f).\nAuthentication when you sign in to Comfy Cloud: performance of a contract under Article 6(1)(b).'
|
||||
},
|
||||
'desktop_privacy.purposes.block.2': {
|
||||
en: 'Consent for analytics and crash diagnostics is opt-in, and you may withdraw it at any time at Settings → Telemetry. Withdrawal does not affect the lawfulness of processing carried out before withdrawal. To object to processing on the basis of legitimate interests, contact <a href="mailto:support@comfy.org" class="text-white underline">support@comfy.org</a>.',
|
||||
'zh-CN':
|
||||
'Consent for analytics and crash diagnostics is opt-in, and you may withdraw it at any time at Settings → Telemetry. Withdrawal does not affect the lawfulness of processing carried out before withdrawal. To object to processing on the basis of legitimate interests, contact <a href="mailto:support@comfy.org" class="text-white underline">support@comfy.org</a>.'
|
||||
},
|
||||
'desktop_privacy.purposes.block.3': {
|
||||
en: 'We do not carry out automated decision-making, including profiling, that produces legal or similarly significant effects. We do not sell personal data, and we do not share personal data for cross-context behavioural advertising.',
|
||||
'zh-CN':
|
||||
'We do not carry out automated decision-making, including profiling, that produces legal or similarly significant effects. We do not sell personal data, and we do not share personal data for cross-context behavioural advertising.'
|
||||
},
|
||||
|
||||
'desktop_privacy.processors.label': {
|
||||
en: 'RECIPIENTS',
|
||||
'zh-CN': 'RECIPIENTS'
|
||||
},
|
||||
'desktop_privacy.processors.title': {
|
||||
en: 'Recipients',
|
||||
'zh-CN': 'Recipients'
|
||||
},
|
||||
'desktop_privacy.processors.block.0': {
|
||||
en: 'We engage the following processors under Data Processing Agreements:',
|
||||
'zh-CN':
|
||||
'We engage the following processors under Data Processing Agreements:'
|
||||
},
|
||||
'desktop_privacy.processors.block.1': {
|
||||
en: 'PostHog (product usage analytics)\nDatadog (crash and error diagnostics)\nToDesktop (application distribution and software updates)\nComfy Org analytics warehouse (long-term aggregate analytics, operated by Comfy Org)',
|
||||
'zh-CN':
|
||||
'PostHog (product usage analytics)\nDatadog (crash and error diagnostics)\nToDesktop (application distribution and software updates)\nComfy Org analytics warehouse (long-term aggregate analytics, operated by Comfy Org)'
|
||||
},
|
||||
|
||||
'desktop_privacy.transfers.label': { en: 'TRANSFERS', 'zh-CN': 'TRANSFERS' },
|
||||
'desktop_privacy.transfers.title': {
|
||||
en: 'International transfers',
|
||||
'zh-CN': 'International transfers'
|
||||
},
|
||||
'desktop_privacy.transfers.block.0': {
|
||||
en: 'Comfy Organization Inc is established in the United States. Personal data of users in the EU, UK, EEA, or other jurisdictions outside the United States may be transferred to the United States and to other locations where our processors operate. Where required, we rely on the European Commission Standard Contractual Clauses (and the UK International Data Transfer Addendum where applicable) as the transfer mechanism under Chapter V GDPR.',
|
||||
'zh-CN':
|
||||
'Comfy Organization Inc is established in the United States. Personal data of users in the EU, UK, EEA, or other jurisdictions outside the United States may be transferred to the United States and to other locations where our processors operate. Where required, we rely on the European Commission Standard Contractual Clauses (and the UK International Data Transfer Addendum where applicable) as the transfer mechanism under Chapter V GDPR.'
|
||||
},
|
||||
|
||||
'desktop_privacy.retention.label': { en: 'RETENTION', 'zh-CN': 'RETENTION' },
|
||||
'desktop_privacy.retention.title': { en: 'Retention', 'zh-CN': 'Retention' },
|
||||
'desktop_privacy.retention.block.0': {
|
||||
en: 'Product usage analytics: up to 24 months from the event, then aggregated or deleted.\nCrash and error diagnostics: 15 days at full fidelity, then sampled or aggregated.\nAggregate analytics: up to 36 months in aggregated form.\nUpdate-server logs: 90 days.\nLocal device identifier: stored on your device only, and removed when you uninstall the application.',
|
||||
'zh-CN':
|
||||
'Product usage analytics: up to 24 months from the event, then aggregated or deleted.\nCrash and error diagnostics: 15 days at full fidelity, then sampled or aggregated.\nAggregate analytics: up to 36 months in aggregated form.\nUpdate-server logs: 90 days.\nLocal device identifier: stored on your device only, and removed when you uninstall the application.'
|
||||
},
|
||||
|
||||
'desktop_privacy.rights.label': { en: 'YOUR RIGHTS', 'zh-CN': 'YOUR RIGHTS' },
|
||||
'desktop_privacy.rights.title': { en: 'Your rights', 'zh-CN': 'Your rights' },
|
||||
'desktop_privacy.rights.block.0': {
|
||||
en: 'If you are in the EU, UK, or EEA, you have the following rights under GDPR and UK GDPR: access, rectification, erasure, restriction of processing, objection, portability, and withdrawal of consent.',
|
||||
'zh-CN':
|
||||
'If you are in the EU, UK, or EEA, you have the following rights under GDPR and UK GDPR: access, rectification, erasure, restriction of processing, objection, portability, and withdrawal of consent.'
|
||||
},
|
||||
'desktop_privacy.rights.block.1': {
|
||||
en: 'If you are a California resident, you have rights under CCPA and CPRA: to know what we collect, to delete, to correct, and to limit use of sensitive personal information. We do not sell personal information, and we do not share it for cross-context behavioural advertising.',
|
||||
'zh-CN':
|
||||
'If you are a California resident, you have rights under CCPA and CPRA: to know what we collect, to delete, to correct, and to limit use of sensitive personal information. We do not sell personal information, and we do not share it for cross-context behavioural advertising.'
|
||||
},
|
||||
'desktop_privacy.rights.block.2': {
|
||||
en: "You also have the right to lodge a complaint with your supervisory authority, such as the UK Information Commissioner's Office, your EU member-state data protection authority, or the California Privacy Protection Agency.",
|
||||
'zh-CN':
|
||||
"You also have the right to lodge a complaint with your supervisory authority, such as the UK Information Commissioner's Office, your EU member-state data protection authority, or the California Privacy Protection Agency."
|
||||
},
|
||||
'desktop_privacy.rights.block.3': {
|
||||
en: 'To exercise any of these rights, contact <a href="mailto:support@comfy.org" class="text-white underline">support@comfy.org</a>. If you have signed in to Comfy Cloud, your account verifies your identity. If you have not signed in, please tell us your approximate install date, platform, and application version, and we will attempt to match these against our records. We aim to respond within 30 days.',
|
||||
'zh-CN':
|
||||
'To exercise any of these rights, contact <a href="mailto:support@comfy.org" class="text-white underline">support@comfy.org</a>. If you have signed in to Comfy Cloud, your account verifies your identity. If you have not signed in, please tell us your approximate install date, platform, and application version, and we will attempt to match these against our records. We aim to respond within 30 days.'
|
||||
},
|
||||
|
||||
'desktop_privacy.children.label': { en: 'CHILDREN', 'zh-CN': 'CHILDREN' },
|
||||
'desktop_privacy.children.title': { en: 'Children', 'zh-CN': 'Children' },
|
||||
'desktop_privacy.children.block.0': {
|
||||
en: 'Comfy Desktop is not intended for, and we do not knowingly collect personal data from, individuals under 13 years of age.',
|
||||
'zh-CN':
|
||||
'Comfy Desktop is not intended for, and we do not knowingly collect personal data from, individuals under 13 years of age.'
|
||||
},
|
||||
|
||||
'desktop_privacy.changes.label': { en: 'CHANGES', 'zh-CN': 'CHANGES' },
|
||||
'desktop_privacy.changes.title': { en: 'Changes', 'zh-CN': 'Changes' },
|
||||
'desktop_privacy.changes.block.0': {
|
||||
en: 'We will revise this Privacy Policy when our processing changes materially. The Effective date at the top of this policy reflects the date of the most recent revision.',
|
||||
'zh-CN':
|
||||
'We will revise this Privacy Policy when our processing changes materially. The Effective date at the top of this policy reflects the date of the most recent revision.'
|
||||
},
|
||||
|
||||
'desktop_privacy.contact.label': { en: 'CONTACT', 'zh-CN': 'CONTACT' },
|
||||
'desktop_privacy.contact.title': { en: 'Contact', 'zh-CN': 'Contact' },
|
||||
'desktop_privacy.contact.block.0': {
|
||||
en: 'For any privacy enquiry, contact <a href="mailto:support@comfy.org" class="text-white underline">support@comfy.org</a>.',
|
||||
'zh-CN':
|
||||
'For any privacy enquiry, contact <a href="mailto:support@comfy.org" class="text-white underline">support@comfy.org</a>.'
|
||||
},
|
||||
|
||||
// ── Terms of Service ──────────────────────────────────────────────
|
||||
'tos.effectiveDateLabel': {
|
||||
en: 'Effective Date',
|
||||
|
||||
13
apps/website/src/pages/privacy/desktop.astro
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import ContentSection from '../../components/common/ContentSection.vue'
|
||||
import HeroSection from '../../components/legal/HeroSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Desktop Privacy Policy — Comfy"
|
||||
description="Privacy policy for Comfy Desktop. Named processors, lawful basis under GDPR/UK GDPR, retention periods, and your rights."
|
||||
>
|
||||
<HeroSection title="Desktop Privacy Policy" />
|
||||
<ContentSection prefix="desktop_privacy" client:load />
|
||||
</BaseLayout>
|
||||
13
apps/website/src/pages/zh-CN/privacy/desktop.astro
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro'
|
||||
import ContentSection from '../../../components/common/ContentSection.vue'
|
||||
import HeroSection from '../../../components/legal/HeroSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Desktop 隐私政策 — Comfy"
|
||||
description="Comfy Desktop 隐私政策。命名的数据处理方、GDPR/UK GDPR 下的合法依据、保留期限和您的权利。"
|
||||
>
|
||||
<HeroSection title="Desktop 隐私政策" />
|
||||
<ContentSection prefix="desktop_privacy" locale="zh-CN" client:load />
|
||||
</BaseLayout>
|
||||
@@ -545,4 +545,54 @@ test.describe('Keybinding Panel', { tag: '@keyboard' }, () => {
|
||||
await expect(expansionContent).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Responsive Layout', () => {
|
||||
test('Action buttons stay on screen without horizontal scroll at narrow widths', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { page } = comfyPage
|
||||
|
||||
await searchKeybindings(page, MULTI_BINDING_COMMAND)
|
||||
const row = getCommandRow(page, MULTI_BINDING_COMMAND)
|
||||
await expect(row).toBeVisible()
|
||||
|
||||
await page.setViewportSize({ width: 480, height: 800 })
|
||||
|
||||
await expect(
|
||||
row.getByRole('button', { name: /Delete/i })
|
||||
).toBeInViewport()
|
||||
await expect(
|
||||
row.getByRole('button', { name: /Add new keybinding/i })
|
||||
).toBeInViewport()
|
||||
|
||||
const hasHorizontalScroll = await page
|
||||
.locator('.keybinding-panel .p-datatable-table-container')
|
||||
.evaluate((el) => el.scrollWidth > el.clientWidth + 1)
|
||||
expect(hasHorizontalScroll).toBe(false)
|
||||
})
|
||||
|
||||
test('Keybinding column compresses with width while actions stay reachable', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { page } = comfyPage
|
||||
|
||||
await searchKeybindings(page, MULTI_BINDING_COMMAND)
|
||||
const row = getCommandRow(page, MULTI_BINDING_COMMAND)
|
||||
const keybindingList = row.getByTestId('keybinding-list')
|
||||
await expect(keybindingList).toBeVisible()
|
||||
|
||||
const listWidthAt = async (viewportWidth: number) => {
|
||||
await page.setViewportSize({ width: viewportWidth, height: 800 })
|
||||
return keybindingList.evaluate((el) => el.getBoundingClientRect().width)
|
||||
}
|
||||
|
||||
const wideWidth = await listWidthAt(1280)
|
||||
const narrowWidth = await listWidthAt(560)
|
||||
|
||||
expect(narrowWidth).toBeLessThan(wideWidth)
|
||||
await expect(
|
||||
row.getByRole('button', { name: /Delete/i })
|
||||
).toBeInViewport()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,13 +28,15 @@ export type {
|
||||
BillingPlansResponse,
|
||||
BillingStatus,
|
||||
BillingStatusResponse,
|
||||
BindingErrorResponse,
|
||||
BulkRevokeApiKeysResponse,
|
||||
BulkRevokeWorkspaceMemberApiKeysData,
|
||||
BulkRevokeWorkspaceMemberApiKeysError,
|
||||
BulkRevokeWorkspaceMemberApiKeysErrors,
|
||||
BulkRevokeWorkspaceMemberApiKeysResponse,
|
||||
BulkRevokeWorkspaceMemberApiKeysResponses,
|
||||
CancelAssetSeedData,
|
||||
CancelAssetSeedResponse,
|
||||
CancelAssetSeedResponses,
|
||||
CancelJobData,
|
||||
CancelJobError,
|
||||
CancelJobErrors,
|
||||
@@ -57,11 +59,14 @@ export type {
|
||||
CheckHubUsernameResponse,
|
||||
CheckHubUsernameResponses,
|
||||
ClientOptions,
|
||||
CreateAssetData,
|
||||
CreateAssetDownloadData,
|
||||
CreateAssetDownloadError,
|
||||
CreateAssetDownloadErrors,
|
||||
CreateAssetDownloadResponse,
|
||||
CreateAssetDownloadResponses,
|
||||
CreateAssetError,
|
||||
CreateAssetErrors,
|
||||
CreateAssetExportData,
|
||||
CreateAssetExportError,
|
||||
CreateAssetExportErrors,
|
||||
@@ -72,6 +77,8 @@ export type {
|
||||
CreateAssetFromHashErrors,
|
||||
CreateAssetFromHashResponse,
|
||||
CreateAssetFromHashResponses,
|
||||
CreateAssetResponse,
|
||||
CreateAssetResponses,
|
||||
CreateDeletionRequestData,
|
||||
CreateDeletionRequestError,
|
||||
CreateDeletionRequestErrors,
|
||||
@@ -208,6 +215,8 @@ export type {
|
||||
ForkWorkflowRequest,
|
||||
ForkWorkflowResponse,
|
||||
ForkWorkflowResponses,
|
||||
FreeMemoryData,
|
||||
FreeMemoryResponses,
|
||||
GetAllSettingsData,
|
||||
GetAllSettingsError,
|
||||
GetAllSettingsErrors,
|
||||
@@ -221,6 +230,9 @@ export type {
|
||||
GetAssetByIdErrors,
|
||||
GetAssetByIdResponse,
|
||||
GetAssetByIdResponses,
|
||||
GetAssetSeedStatusData,
|
||||
GetAssetSeedStatusResponse,
|
||||
GetAssetSeedStatusResponses,
|
||||
GetAssetTagHistogramData,
|
||||
GetAssetTagHistogramError,
|
||||
GetAssetTagHistogramErrors,
|
||||
@@ -259,6 +271,9 @@ export type {
|
||||
GetDeletionRequestErrors,
|
||||
GetDeletionRequestResponse,
|
||||
GetDeletionRequestResponses,
|
||||
GetEmbeddingsData,
|
||||
GetEmbeddingsResponse,
|
||||
GetEmbeddingsResponses,
|
||||
GetExtensionsData,
|
||||
GetExtensionsResponse,
|
||||
GetExtensionsResponses,
|
||||
@@ -305,6 +320,18 @@ export type {
|
||||
GetHubWorkflowErrors,
|
||||
GetHubWorkflowResponse,
|
||||
GetHubWorkflowResponses,
|
||||
GetI18nData,
|
||||
GetI18nResponse,
|
||||
GetI18nResponses,
|
||||
GetInternalFolderPathsData,
|
||||
GetInternalFolderPathsResponse,
|
||||
GetInternalFolderPathsResponses,
|
||||
GetInternalLogsData,
|
||||
GetInternalLogsRawData,
|
||||
GetInternalLogsRawResponse,
|
||||
GetInternalLogsRawResponses,
|
||||
GetInternalLogsResponse,
|
||||
GetInternalLogsResponses,
|
||||
GetJobDetailData,
|
||||
GetJobDetailError,
|
||||
GetJobDetailErrors,
|
||||
@@ -356,10 +383,7 @@ export type {
|
||||
GetModelFoldersResponse,
|
||||
GetModelFoldersResponses,
|
||||
GetModelPreviewData,
|
||||
GetModelPreviewError,
|
||||
GetModelPreviewErrors,
|
||||
GetModelPreviewResponse,
|
||||
GetModelPreviewResponses,
|
||||
GetModelsInFolderData,
|
||||
GetModelsInFolderError,
|
||||
GetModelsInFolderErrors,
|
||||
@@ -389,8 +413,26 @@ export type {
|
||||
GetNodeReplacementsErrors,
|
||||
GetNodeReplacementsResponse,
|
||||
GetNodeReplacementsResponses,
|
||||
GetOpenapiSpecData,
|
||||
GetOpenapiSpecResponses,
|
||||
GetOAuthAuthorizationServerData,
|
||||
GetOAuthAuthorizationServerError,
|
||||
GetOAuthAuthorizationServerErrors,
|
||||
GetOAuthAuthorizationServerResponse,
|
||||
GetOAuthAuthorizationServerResponses,
|
||||
GetOAuthAuthorizeData,
|
||||
GetOAuthAuthorizeError,
|
||||
GetOAuthAuthorizeErrors,
|
||||
GetOAuthAuthorizeResponse,
|
||||
GetOAuthAuthorizeResponses,
|
||||
GetOAuthProtectedResourceByPathData,
|
||||
GetOAuthProtectedResourceByPathError,
|
||||
GetOAuthProtectedResourceByPathErrors,
|
||||
GetOAuthProtectedResourceByPathResponse,
|
||||
GetOAuthProtectedResourceByPathResponses,
|
||||
GetOAuthProtectedResourceData,
|
||||
GetOAuthProtectedResourceError,
|
||||
GetOAuthProtectedResourceErrors,
|
||||
GetOAuthProtectedResourceResponse,
|
||||
GetOAuthProtectedResourceResponses,
|
||||
GetPaymentPortalData,
|
||||
GetPaymentPortalError,
|
||||
GetPaymentPortalErrors,
|
||||
@@ -427,11 +469,11 @@ export type {
|
||||
GetSecretErrors,
|
||||
GetSecretResponse,
|
||||
GetSecretResponses,
|
||||
GetSettingByKeyData,
|
||||
GetSettingByKeyError,
|
||||
GetSettingByKeyErrors,
|
||||
GetSettingByKeyResponse,
|
||||
GetSettingByKeyResponses,
|
||||
GetSettingByIdData,
|
||||
GetSettingByIdError,
|
||||
GetSettingByIdErrors,
|
||||
GetSettingByIdResponse,
|
||||
GetSettingByIdResponses,
|
||||
GetStaticExtensionsData,
|
||||
GetStaticExtensionsErrors,
|
||||
GetStaticExtensionsResponses,
|
||||
@@ -447,6 +489,7 @@ export type {
|
||||
GetTaskResponses,
|
||||
GetTemplateProxyData,
|
||||
GetTemplateProxyErrors,
|
||||
GetTemplateProxyResponses,
|
||||
GetUserData,
|
||||
GetUserdataData,
|
||||
GetUserdataError,
|
||||
@@ -534,6 +577,11 @@ export type {
|
||||
ImportPublishedAssetsResponse,
|
||||
ImportPublishedAssetsResponse2,
|
||||
ImportPublishedAssetsResponses,
|
||||
InsertDynamicConfigData,
|
||||
InsertDynamicConfigError,
|
||||
InsertDynamicConfigErrors,
|
||||
InsertDynamicConfigResponse,
|
||||
InsertDynamicConfigResponses,
|
||||
InterruptJobData,
|
||||
InterruptJobError,
|
||||
InterruptJobErrors,
|
||||
@@ -642,6 +690,17 @@ export type {
|
||||
MoveUserdataFileResponse,
|
||||
MoveUserdataFileResponses,
|
||||
NodeInfo,
|
||||
OAuthAuthorizationServerMetadata,
|
||||
OAuthAuthorizeRedirectResponse,
|
||||
OAuthConsentChallenge,
|
||||
OAuthConsentChallengeWorkspace,
|
||||
OAuthProtectedResourceMetadata,
|
||||
OAuthRegisterBadRequestResponse,
|
||||
OAuthRegisterError,
|
||||
OAuthRegisterRequest,
|
||||
OAuthRegisterResponse,
|
||||
OAuthTokenError,
|
||||
OAuthTokenResponse,
|
||||
PaginationInfo,
|
||||
PartnerUsageRequest,
|
||||
PartnerUsageResponse,
|
||||
@@ -663,6 +722,21 @@ export type {
|
||||
PostMonitoringTasksSubpathData,
|
||||
PostMonitoringTasksSubpathErrors,
|
||||
PostMonitoringTasksSubpathResponses,
|
||||
PostOAuthAuthorizeData,
|
||||
PostOAuthAuthorizeError,
|
||||
PostOAuthAuthorizeErrors,
|
||||
PostOAuthAuthorizeResponse,
|
||||
PostOAuthAuthorizeResponses,
|
||||
PostOAuthRegisterData,
|
||||
PostOAuthRegisterError,
|
||||
PostOAuthRegisterErrors,
|
||||
PostOAuthRegisterResponse,
|
||||
PostOAuthRegisterResponses,
|
||||
PostOAuthTokenData,
|
||||
PostOAuthTokenError,
|
||||
PostOAuthTokenErrors,
|
||||
PostOAuthTokenResponse,
|
||||
PostOAuthTokenResponses,
|
||||
PostPprofSymbolData,
|
||||
PostPprofSymbolResponses,
|
||||
PostUserdataFileData,
|
||||
@@ -687,6 +761,9 @@ export type {
|
||||
PromptInfo,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
PruneAssetsData,
|
||||
PruneAssetsResponse,
|
||||
PruneAssetsResponses,
|
||||
PublishedWorkflowDetail,
|
||||
PublishHubWorkflowData,
|
||||
PublishHubWorkflowError,
|
||||
@@ -732,6 +809,9 @@ export type {
|
||||
RevokeWorkspaceInviteResponses,
|
||||
SecretListResponse,
|
||||
SecretResponse,
|
||||
SeedAssetsData,
|
||||
SeedAssetsResponse,
|
||||
SeedAssetsResponses,
|
||||
SetReviewStatusData,
|
||||
SetReviewStatusError,
|
||||
SetReviewStatusErrors,
|
||||
@@ -751,6 +831,8 @@ export type {
|
||||
SubscribeResponse,
|
||||
SubscribeResponse2,
|
||||
SubscribeResponses,
|
||||
SubscribeToLogsData,
|
||||
SubscribeToLogsResponses,
|
||||
SubscriptionDuration,
|
||||
SubscriptionTier,
|
||||
SyncApiKeyData,
|
||||
@@ -771,11 +853,6 @@ export type {
|
||||
UpdateAssetErrors,
|
||||
UpdateAssetResponse,
|
||||
UpdateAssetResponses,
|
||||
UpdateAssetTagsData,
|
||||
UpdateAssetTagsError,
|
||||
UpdateAssetTagsErrors,
|
||||
UpdateAssetTagsResponse,
|
||||
UpdateAssetTagsResponses,
|
||||
UpdateHubProfileData,
|
||||
UpdateHubProfileError,
|
||||
UpdateHubProfileErrors,
|
||||
@@ -799,11 +876,11 @@ export type {
|
||||
UpdateSecretRequest,
|
||||
UpdateSecretResponse,
|
||||
UpdateSecretResponses,
|
||||
UpdateSettingByKeyData,
|
||||
UpdateSettingByKeyError,
|
||||
UpdateSettingByKeyErrors,
|
||||
UpdateSettingByKeyResponse,
|
||||
UpdateSettingByKeyResponses,
|
||||
UpdateSettingByIdData,
|
||||
UpdateSettingByIdError,
|
||||
UpdateSettingByIdErrors,
|
||||
UpdateSettingByIdResponse,
|
||||
UpdateSettingByIdResponses,
|
||||
UpdateSubscriptionCacheData,
|
||||
UpdateSubscriptionCacheError,
|
||||
UpdateSubscriptionCacheErrors,
|
||||
@@ -821,11 +898,6 @@ export type {
|
||||
UpdateWorkspaceRequest,
|
||||
UpdateWorkspaceResponse,
|
||||
UpdateWorkspaceResponses,
|
||||
UploadAssetData,
|
||||
UploadAssetError,
|
||||
UploadAssetErrors,
|
||||
UploadAssetResponse,
|
||||
UploadAssetResponses,
|
||||
UploadImageData,
|
||||
UploadImageError,
|
||||
UploadImageErrors,
|
||||
|
||||
1243
packages/ingest-types/src/types.gen.ts
generated
587
packages/ingest-types/src/zod.gen.ts
generated
@@ -399,13 +399,18 @@ export const zCreateWorkflowVersionRequest = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* Offset/limit-based pagination metadata included in list responses.
|
||||
* Pagination metadata included in list responses. Supports both legacy
|
||||
* offset/limit pagination and cursor-based pagination. When cursor-based
|
||||
* pagination is used, `next_cursor` is the primary pagination token and
|
||||
* `offset`/`total` may be zero.
|
||||
*
|
||||
*/
|
||||
export const zPaginationInfo = z.object({
|
||||
offset: z.number().int().gte(0),
|
||||
limit: z.number().int().gte(1),
|
||||
total: z.number().int().gte(0),
|
||||
has_more: z.boolean()
|
||||
has_more: z.boolean(),
|
||||
next_cursor: z.string().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -879,6 +884,155 @@ export const zJwkKey = z.object({
|
||||
y: z.string()
|
||||
})
|
||||
|
||||
/**
|
||||
* RFC 6749 §5.2 error response.
|
||||
*/
|
||||
export const zOAuthTokenError = z.object({
|
||||
error: z.string(),
|
||||
error_description: z.string().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* RFC 6749 §5.1 successful token response.
|
||||
*/
|
||||
export const zOAuthTokenResponse = z.object({
|
||||
access_token: z.string(),
|
||||
token_type: z.enum(['Bearer']),
|
||||
expires_in: z.number().int(),
|
||||
refresh_token: z.string(),
|
||||
scope: z.string()
|
||||
})
|
||||
|
||||
/**
|
||||
* One workspace option presented in the OAuth consent challenge. Promoted to a named schema so the generated Go type is referenceable in handlers and tests rather than re-declared as an anonymous struct at every callsite.
|
||||
*
|
||||
*/
|
||||
export const zOAuthConsentChallengeWorkspace = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.enum(['personal', 'team']),
|
||||
role: z.enum(['owner', 'member'])
|
||||
})
|
||||
|
||||
/**
|
||||
* Redirect target produced after a JSON consent submission. The frontend must navigate the browser to this URL so custom-scheme client callbacks work without relying on fetch-visible 302 headers.
|
||||
*/
|
||||
export const zOAuthAuthorizeRedirectResponse = z.object({
|
||||
redirect_url: z.string().url()
|
||||
})
|
||||
|
||||
/**
|
||||
* Server-side state describing the OAuth consent decision the user is being asked to make. Returned by GET /oauth/authorize when a valid Cloud session exists; the frontend renders the consent UI from this payload and POSTs the decision back. Browser never sees the original OAuth params on resume.
|
||||
*
|
||||
*/
|
||||
export const zOAuthConsentChallenge = z.object({
|
||||
oauth_request_id: z.string().uuid(),
|
||||
csrf_token: z.string(),
|
||||
client_display_name: z.string(),
|
||||
resource_display_name: z.string(),
|
||||
scopes: z.array(z.string()),
|
||||
workspaces: z.array(zOAuthConsentChallengeWorkspace)
|
||||
})
|
||||
|
||||
/**
|
||||
* OAuth 2.1 protected-resource metadata (RFC 9728).
|
||||
*/
|
||||
export const zOAuthProtectedResourceMetadata = z.object({
|
||||
resource: z.string().url(),
|
||||
authorization_servers: z.array(z.string().url()),
|
||||
scopes_supported: z.array(z.string()),
|
||||
bearer_methods_supported: z.array(z.string()).optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* RFC 7591 §3.2.2 error response.
|
||||
*/
|
||||
export const zOAuthRegisterError = z.object({
|
||||
error: z.enum(['invalid_redirect_uri', 'invalid_client_metadata']),
|
||||
error_description: z.string().nullish()
|
||||
})
|
||||
|
||||
/**
|
||||
* Standard error response with a machine-readable code and human-readable message.
|
||||
*/
|
||||
export const zErrorResponse = z.object({
|
||||
code: z.string(),
|
||||
message: z.string(),
|
||||
details: z.record(z.unknown()).optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Union of the two 400 shapes /oauth/register can emit. `OAuthRegisterError` is the handler-shaped RFC 7591 §3.2.2 error; `ErrorResponse` is the strict-server binding-layer error fired when the request body fails OpenAPI-schema validation before the handler runs, normalized to the standard {code, message} shape by the custom Echo HTTPErrorHandler (BE-1178).
|
||||
*
|
||||
*/
|
||||
export const zOAuthRegisterBadRequestResponse = z.union([
|
||||
zOAuthRegisterError,
|
||||
zErrorResponse
|
||||
])
|
||||
|
||||
/**
|
||||
* RFC 7591 §3.2.1 successful registration response.
|
||||
*/
|
||||
export const zOAuthRegisterResponse = z.object({
|
||||
client_id: z.string(),
|
||||
client_id_issued_at: z.coerce
|
||||
.bigint()
|
||||
.min(BigInt('-9223372036854775808'), {
|
||||
message: 'Invalid value: Expected int64 to be >= -9223372036854775808'
|
||||
})
|
||||
.max(BigInt('9223372036854775807'), {
|
||||
message: 'Invalid value: Expected int64 to be <= 9223372036854775807'
|
||||
}),
|
||||
client_name: z.string().optional(),
|
||||
redirect_uris: z.array(z.string()),
|
||||
grant_types: z.array(z.string()),
|
||||
response_types: z.array(z.string()),
|
||||
token_endpoint_auth_method: z.enum(['none']),
|
||||
application_type: z.enum(['native', 'web'])
|
||||
})
|
||||
|
||||
/**
|
||||
* RFC 7591 §2 client metadata document. Only the fields the server honors are listed; presence of `scope` or `resource_grants` in the request is rejected (`invalid_client_metadata`) because those are server-owned for dynamic clients. `additionalProperties: false` mirrors the runtime middleware that rejects any unknown metadata key.
|
||||
*
|
||||
*/
|
||||
export const zOAuthRegisterRequest = z.object({
|
||||
redirect_uris: z.array(z.string()).min(1).max(5),
|
||||
client_name: z.string().max(100).optional(),
|
||||
application_type: z.enum(['native', 'web']).optional(),
|
||||
token_endpoint_auth_method: z.enum(['none']).optional(),
|
||||
grant_types: z
|
||||
.array(z.enum(['authorization_code', 'refresh_token']))
|
||||
.optional(),
|
||||
response_types: z.array(z.enum(['code'])).optional(),
|
||||
scope: z.string().nullish(),
|
||||
resource_grants: z.record(z.array(z.string())).nullish(),
|
||||
client_uri: z.string().nullish(),
|
||||
logo_uri: z.string().nullish(),
|
||||
tos_uri: z.string().nullish(),
|
||||
policy_uri: z.string().nullish(),
|
||||
software_id: z.string().nullish(),
|
||||
software_version: z.string().nullish(),
|
||||
contacts: z.array(z.string()).nullish(),
|
||||
jwks: z.record(z.unknown()).nullish(),
|
||||
jwks_uri: z.string().nullish()
|
||||
})
|
||||
|
||||
/**
|
||||
* OAuth 2.1 authorization-server metadata (RFC 8414).
|
||||
*/
|
||||
export const zOAuthAuthorizationServerMetadata = z.object({
|
||||
issuer: z.string().url(),
|
||||
authorization_endpoint: z.string().url(),
|
||||
token_endpoint: z.string().url(),
|
||||
jwks_uri: z.string().url(),
|
||||
registration_endpoint: z.string().url().optional(),
|
||||
response_types_supported: z.array(z.string()),
|
||||
grant_types_supported: z.array(z.string()),
|
||||
code_challenge_methods_supported: z.array(z.string()),
|
||||
token_endpoint_auth_methods_supported: z.array(z.string()),
|
||||
scopes_supported: z.array(z.string()).optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* JSON Web Key Set containing the public keys used to verify Cloud JWTs.
|
||||
*/
|
||||
@@ -940,6 +1094,7 @@ export const zWorkspaceApiKeyInfo = z.object({
|
||||
workspace_id: z.string(),
|
||||
user_id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().max(5000),
|
||||
key_prefix: z.string(),
|
||||
expires_at: z.string().datetime().optional(),
|
||||
last_used_at: z.string().datetime().optional(),
|
||||
@@ -960,6 +1115,7 @@ export const zListWorkspaceApiKeysResponse = z.object({
|
||||
export const zCreateWorkspaceApiKeyResponse = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().max(5000),
|
||||
key: z.string(),
|
||||
key_prefix: z.string(),
|
||||
expires_at: z.string().datetime().optional(),
|
||||
@@ -971,6 +1127,7 @@ export const zCreateWorkspaceApiKeyResponse = z.object({
|
||||
*/
|
||||
export const zCreateWorkspaceApiKeyRequest = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().max(5000).optional(),
|
||||
expires_at: z.string().datetime().optional()
|
||||
})
|
||||
|
||||
@@ -1353,7 +1510,8 @@ export const zListTagsResponse = z.object({
|
||||
export const zAsset = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
asset_hash: z
|
||||
display_name: z.string().nullish(),
|
||||
hash: z
|
||||
.string()
|
||||
.regex(/^blake3:[a-f0-9]{64}$/)
|
||||
.optional(),
|
||||
@@ -1364,14 +1522,14 @@ export const zAsset = z.object({
|
||||
})
|
||||
.max(BigInt('9223372036854775807'), {
|
||||
message: 'Invalid value: Expected int64 to be <= 9223372036854775807'
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
mime_type: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
user_metadata: z.record(z.unknown()).optional(),
|
||||
metadata: z.record(z.unknown()).readonly().optional(),
|
||||
preview_url: z.string().url().optional(),
|
||||
preview_id: z.string().uuid().nullish(),
|
||||
prompt_id: z.string().uuid().nullish(),
|
||||
job_id: z.string().uuid().nullish(),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
@@ -1385,7 +1543,8 @@ export const zAsset = z.object({
|
||||
export const zListAssetsResponse = z.object({
|
||||
assets: z.array(zAsset),
|
||||
total: z.number().int(),
|
||||
has_more: z.boolean()
|
||||
has_more: z.boolean(),
|
||||
next_cursor: z.string().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -1394,13 +1553,15 @@ export const zListAssetsResponse = z.object({
|
||||
export const zAssetUpdated = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string().optional(),
|
||||
asset_hash: z
|
||||
display_name: z.string().nullish(),
|
||||
hash: z
|
||||
.string()
|
||||
.regex(/^blake3:[a-f0-9]{64}$/)
|
||||
.optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
mime_type: z.string().optional(),
|
||||
user_metadata: z.record(z.unknown()).optional(),
|
||||
job_id: z.string().uuid().nullish(),
|
||||
updated_at: z.string().datetime()
|
||||
})
|
||||
|
||||
@@ -1753,21 +1914,6 @@ export const zExportDownloadUrlResponse = z.object({
|
||||
expires_at: z.string().datetime().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Error shape returned when request binding or validation fails before the handler runs.
|
||||
*/
|
||||
export const zBindingErrorResponse = z.object({
|
||||
message: z.string()
|
||||
})
|
||||
|
||||
/**
|
||||
* Standard error response with a machine-readable code and human-readable message.
|
||||
*/
|
||||
export const zErrorResponse = z.object({
|
||||
code: z.string(),
|
||||
message: z.string()
|
||||
})
|
||||
|
||||
/**
|
||||
* Response returned after successfully queuing a workflow prompt.
|
||||
*/
|
||||
@@ -1796,7 +1942,8 @@ export const zPromptRequest = z.object({
|
||||
export const zAssetWritable = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
asset_hash: z
|
||||
display_name: z.string().nullish(),
|
||||
hash: z
|
||||
.string()
|
||||
.regex(/^blake3:[a-f0-9]{64}$/)
|
||||
.optional(),
|
||||
@@ -1807,13 +1954,13 @@ export const zAssetWritable = z.object({
|
||||
})
|
||||
.max(BigInt('9223372036854775807'), {
|
||||
message: 'Invalid value: Expected int64 to be <= 9223372036854775807'
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
mime_type: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
user_metadata: z.record(z.unknown()).optional(),
|
||||
preview_url: z.string().url().optional(),
|
||||
preview_id: z.string().uuid().nullish(),
|
||||
prompt_id: z.string().uuid().nullish(),
|
||||
job_id: z.string().uuid().nullish(),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
@@ -1827,7 +1974,8 @@ export const zAssetWritable = z.object({
|
||||
export const zListAssetsResponseWritable = z.object({
|
||||
assets: z.array(zAssetWritable),
|
||||
total: z.number().int(),
|
||||
has_more: z.boolean()
|
||||
has_more: z.boolean(),
|
||||
next_cursor: z.string().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -1961,21 +2109,6 @@ export const zGetModelsInFolderData = z.object({
|
||||
*/
|
||||
export const zGetModelsInFolderResponse = z.array(zModelFile)
|
||||
|
||||
export const zGetModelPreviewData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.object({
|
||||
folder: z.string(),
|
||||
path_index: z.number().int(),
|
||||
filename: z.string()
|
||||
}),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Success - Model preview image
|
||||
*/
|
||||
export const zGetModelPreviewResponse = z.string()
|
||||
|
||||
export const zGetLegacyHistoryData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
@@ -2027,6 +2160,7 @@ export const zListJobsData = z.object({
|
||||
output_type: z.enum(['image', 'video', 'audio', '3d']).optional(),
|
||||
sort_by: z.enum(['create_time', 'execution_time']).optional(),
|
||||
sort_order: z.enum(['asc', 'desc']).optional(),
|
||||
after: z.string().optional(),
|
||||
offset: z.number().int().gte(0).optional().default(0),
|
||||
limit: z.number().int().gte(1).lte(1000).optional().default(100)
|
||||
})
|
||||
@@ -2132,9 +2266,9 @@ export const zListAssetsData = z.object({
|
||||
.enum(['name', 'created_at', 'updated_at', 'size', 'last_access_time'])
|
||||
.optional(),
|
||||
order: z.enum(['asc', 'desc']).optional(),
|
||||
job_ids: z.array(z.string().uuid()).optional(),
|
||||
include_public: z.boolean().optional().default(true),
|
||||
asset_hash: z.string().optional()
|
||||
hash: z.string().optional(),
|
||||
after: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -2144,22 +2278,28 @@ export const zListAssetsData = z.object({
|
||||
*/
|
||||
export const zListAssetsResponse2 = zListAssetsResponse
|
||||
|
||||
export const zUploadAssetData = z.object({
|
||||
export const zCreateAssetData = z.object({
|
||||
body: z.object({
|
||||
url: z.string().url(),
|
||||
name: z.string(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
user_metadata: z.record(z.unknown()).optional(),
|
||||
preview_id: z.string().uuid().optional()
|
||||
file: z.string(),
|
||||
hash: z
|
||||
.string()
|
||||
.regex(/^(blake3|sha256):[a-f0-9]{64}$/)
|
||||
.optional(),
|
||||
tags: z.string().optional(),
|
||||
id: z.string().uuid().optional(),
|
||||
preview_id: z.string().uuid().optional(),
|
||||
name: z.string().optional(),
|
||||
mime_type: z.string().optional(),
|
||||
user_metadata: z.string().optional()
|
||||
}),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Asset already exists (returned existing asset)
|
||||
* Asset created successfully
|
||||
*/
|
||||
export const zUploadAssetResponse = zAssetCreated
|
||||
export const zCreateAssetResponse = zAssetCreated
|
||||
|
||||
export const zCreateAssetFromHashData = z.object({
|
||||
body: z.object({
|
||||
@@ -2174,7 +2314,7 @@ export const zCreateAssetFromHashData = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* Asset reference already exists (returned existing)
|
||||
* Asset reference created successfully
|
||||
*/
|
||||
export const zCreateAssetFromHashResponse = zAssetCreated
|
||||
|
||||
@@ -2214,7 +2354,8 @@ export const zCreateAssetExportData = z.object({
|
||||
naming_strategy: z
|
||||
.enum(['group_by_job_id', 'preserve', 'asset_id', 'group_by_job_time'])
|
||||
.optional(),
|
||||
job_asset_name_filters: z.record(z.array(z.string()).min(1)).optional()
|
||||
job_asset_name_filters: z.record(z.array(z.string()).min(1)).optional(),
|
||||
include_previews: z.boolean().optional().default(false)
|
||||
}),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
@@ -2247,7 +2388,7 @@ export const zDeleteAssetData = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* Asset deleted successfully
|
||||
* Asset record deleted successfully
|
||||
*/
|
||||
export const zDeleteAssetResponse = z.void()
|
||||
|
||||
@@ -2312,22 +2453,6 @@ export const zAddAssetTagsData = z.object({
|
||||
*/
|
||||
export const zAddAssetTagsResponse = zTagsModificationResponse
|
||||
|
||||
export const zUpdateAssetTagsData = z.object({
|
||||
body: z.object({
|
||||
add: z.array(z.string()).optional(),
|
||||
remove: z.array(z.string()).optional()
|
||||
}),
|
||||
path: z.object({
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Tags updated successfully
|
||||
*/
|
||||
export const zUpdateAssetTagsResponse = zTagsModificationResponse
|
||||
|
||||
export const zListTagsData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
@@ -2509,10 +2634,10 @@ export const zUpdateMultipleSettingsData = z.object({
|
||||
*/
|
||||
export const zUpdateMultipleSettingsResponse = z.record(z.unknown())
|
||||
|
||||
export const zGetSettingByKeyData = z.object({
|
||||
export const zGetSettingByIdData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.object({
|
||||
key: z.string()
|
||||
id: z.string()
|
||||
}),
|
||||
query: z.never().optional()
|
||||
})
|
||||
@@ -2520,14 +2645,14 @@ export const zGetSettingByKeyData = z.object({
|
||||
/**
|
||||
* Setting value response
|
||||
*/
|
||||
export const zGetSettingByKeyResponse = z.object({
|
||||
export const zGetSettingByIdResponse = z.object({
|
||||
value: z.unknown().optional()
|
||||
})
|
||||
|
||||
export const zUpdateSettingByKeyData = z.object({
|
||||
export const zUpdateSettingByIdData = z.object({
|
||||
body: z.unknown(),
|
||||
path: z.object({
|
||||
key: z.string()
|
||||
id: z.string()
|
||||
}),
|
||||
query: z.never().optional()
|
||||
})
|
||||
@@ -2535,7 +2660,7 @@ export const zUpdateSettingByKeyData = z.object({
|
||||
/**
|
||||
* Updated setting value response
|
||||
*/
|
||||
export const zUpdateSettingByKeyResponse = z.object({
|
||||
export const zUpdateSettingByIdResponse = z.object({
|
||||
value: z.unknown().optional()
|
||||
})
|
||||
|
||||
@@ -2691,21 +2816,7 @@ export const zUploadMaskData = z.object({
|
||||
export const zUploadMaskResponse = z.object({
|
||||
name: z.string().optional(),
|
||||
subfolder: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
metadata: z
|
||||
.object({
|
||||
is_mask: z.boolean().optional(),
|
||||
original_hash: z.string().optional(),
|
||||
mask_type: z.string().optional(),
|
||||
related_files: z
|
||||
.object({
|
||||
mask: z.string().optional(),
|
||||
paint: z.string().optional(),
|
||||
painted: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional()
|
||||
type: z.string().optional()
|
||||
})
|
||||
|
||||
export const zGetLogsData = z.object({
|
||||
@@ -2774,6 +2885,115 @@ export const zGetJwksData = z.object({
|
||||
*/
|
||||
export const zGetJwksResponse = zJwksResponse
|
||||
|
||||
export const zGetOAuthAuthorizationServerData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Authorization-server metadata
|
||||
*/
|
||||
export const zGetOAuthAuthorizationServerResponse =
|
||||
zOAuthAuthorizationServerMetadata
|
||||
|
||||
export const zGetOAuthProtectedResourceData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Protected-resource metadata
|
||||
*/
|
||||
export const zGetOAuthProtectedResourceResponse =
|
||||
zOAuthProtectedResourceMetadata
|
||||
|
||||
export const zGetOAuthProtectedResourceByPathData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.object({
|
||||
resourcePath: z.string().regex(/^[a-zA-Z0-9._-]+$/)
|
||||
}),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Protected-resource metadata
|
||||
*/
|
||||
export const zGetOAuthProtectedResourceByPathResponse =
|
||||
zOAuthProtectedResourceMetadata
|
||||
|
||||
export const zGetOAuthAuthorizeData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z
|
||||
.object({
|
||||
response_type: z.string().optional(),
|
||||
client_id: z.string().optional(),
|
||||
redirect_uri: z.string().optional(),
|
||||
scope: z.string().optional(),
|
||||
state: z.string().optional(),
|
||||
code_challenge: z.string().optional(),
|
||||
code_challenge_method: z.string().optional(),
|
||||
resource: z.string().optional(),
|
||||
oauth_request_id: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Consent challenge payload (cookie present, email verified). Frontend renders the consent UI from this payload and POSTs back to /oauth/authorize.
|
||||
*
|
||||
*/
|
||||
export const zGetOAuthAuthorizeResponse = zOAuthConsentChallenge
|
||||
|
||||
export const zPostOAuthAuthorizeData = z.object({
|
||||
body: z.object({
|
||||
oauth_request_id: z.string().uuid(),
|
||||
csrf_token: z.string(),
|
||||
decision: z.enum(['allow', 'deny']),
|
||||
workspace_id: z.string()
|
||||
}),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Redirect URL for the frontend to navigate to (allow → with code+state; deny → with error+state)
|
||||
*/
|
||||
export const zPostOAuthAuthorizeResponse = zOAuthAuthorizeRedirectResponse
|
||||
|
||||
export const zPostOAuthTokenData = z.object({
|
||||
body: z.object({
|
||||
grant_type: z.enum(['authorization_code', 'refresh_token']),
|
||||
client_id: z.string(),
|
||||
code: z.string().optional(),
|
||||
redirect_uri: z.string().optional(),
|
||||
code_verifier: z.string().optional(),
|
||||
refresh_token: z.string().optional(),
|
||||
scope: z.string().optional(),
|
||||
client_secret: z.string().optional()
|
||||
}),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* New token pair
|
||||
*/
|
||||
export const zPostOAuthTokenResponse = zOAuthTokenResponse
|
||||
|
||||
export const zPostOAuthRegisterData = z.object({
|
||||
body: zOAuthRegisterRequest,
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Registered. Body echoes the metadata RFC 7591 §3.2.1 requires.
|
||||
*/
|
||||
export const zPostOAuthRegisterResponse = zOAuthRegisterResponse
|
||||
|
||||
export const zListWorkspacesData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
@@ -3078,6 +3298,28 @@ export const zUpdateSubscriptionCacheResponse = z.object({
|
||||
status: z.string().optional()
|
||||
})
|
||||
|
||||
export const zInsertDynamicConfigData = z.object({
|
||||
body: z.record(z.unknown()),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Config inserted successfully
|
||||
*/
|
||||
export const zInsertDynamicConfigResponse = z.object({
|
||||
id: z.coerce
|
||||
.bigint()
|
||||
.min(BigInt('-9223372036854775808'), {
|
||||
message: 'Invalid value: Expected int64 to be >= -9223372036854775808'
|
||||
})
|
||||
.max(BigInt('9223372036854775807'), {
|
||||
message: 'Invalid value: Expected int64 to be <= 9223372036854775807'
|
||||
})
|
||||
.optional(),
|
||||
message: z.string().optional()
|
||||
})
|
||||
|
||||
export const zSyncApiKeyData = z.object({
|
||||
body: zSyncApiKeyRequest,
|
||||
path: z.never().optional(),
|
||||
@@ -3671,12 +3913,6 @@ export const zGetHealthData = z.object({
|
||||
*/
|
||||
export const zGetHealthResponse = z.string()
|
||||
|
||||
export const zGetOpenapiSpecData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
export const zGetMonitoringTasksData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
@@ -3757,6 +3993,16 @@ export const zPostCustomNodeProxyData = z.object({
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
export const zGetModelPreviewData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.object({
|
||||
folder: z.string(),
|
||||
path_index: z.number().int(),
|
||||
filename: z.string()
|
||||
}),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
export const zGetLegacyPromptByIdData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.object({
|
||||
@@ -3832,3 +4078,150 @@ export const zGetLegacyViewMetadataData = z.object({
|
||||
}),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
export const zGetEmbeddingsData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Embedding names
|
||||
*/
|
||||
export const zGetEmbeddingsResponse = z.array(z.string())
|
||||
|
||||
export const zFreeMemoryData = z.object({
|
||||
body: z
|
||||
.object({
|
||||
unload_models: z.boolean().optional(),
|
||||
free_memory: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
export const zGetI18nData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Nested map of locale to translation key-value pairs
|
||||
*/
|
||||
export const zGetI18nResponse = z.record(z.unknown())
|
||||
|
||||
export const zGetInternalFolderPathsData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Map of folder type name to list of path entries
|
||||
*/
|
||||
export const zGetInternalFolderPathsResponse = z.record(
|
||||
z.array(z.array(z.string()))
|
||||
)
|
||||
|
||||
export const zGetInternalLogsData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Log text
|
||||
*/
|
||||
export const zGetInternalLogsResponse = z.string()
|
||||
|
||||
export const zGetInternalLogsRawData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Structured log data
|
||||
*/
|
||||
export const zGetInternalLogsRawResponse = z.object({
|
||||
entries: z
|
||||
.array(
|
||||
z.object({
|
||||
t: z.number().optional(),
|
||||
m: z.string().optional()
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
size: z
|
||||
.object({
|
||||
cols: z.number().int().optional(),
|
||||
rows: z.number().int().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
|
||||
export const zSubscribeToLogsData = z.object({
|
||||
body: z.object({
|
||||
clientId: z.string(),
|
||||
enabled: z.boolean()
|
||||
}),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
export const zPruneAssetsData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Prune result
|
||||
*/
|
||||
export const zPruneAssetsResponse = z.object({
|
||||
status: z.string().optional(),
|
||||
marked: z.number().int().optional()
|
||||
})
|
||||
|
||||
export const zSeedAssetsData = z.object({
|
||||
body: z
|
||||
.object({
|
||||
roots: z.array(z.string()).optional()
|
||||
})
|
||||
.optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Seed started
|
||||
*/
|
||||
export const zSeedAssetsResponse = z.object({
|
||||
status: z.string().optional()
|
||||
})
|
||||
|
||||
export const zGetAssetSeedStatusData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Scan progress details (files scanned, total, status, etc.)
|
||||
*/
|
||||
export const zGetAssetSeedStatusResponse = z.record(z.unknown())
|
||||
|
||||
export const zCancelAssetSeedData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Scan cancelled
|
||||
*/
|
||||
export const zCancelAssetSeedResponse = z.object({
|
||||
status: z.string().optional()
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
:ref="primeVueOverlay.overlayScopeRef"
|
||||
class="keybinding-panel flex flex-col gap-2"
|
||||
class="keybinding-panel flex min-w-0 flex-col gap-2 overflow-x-hidden"
|
||||
>
|
||||
<Teleport defer to="#keybinding-panel-header">
|
||||
<SearchInput
|
||||
@@ -46,7 +46,10 @@
|
||||
|
||||
<ContextMenuRoot>
|
||||
<ContextMenuTrigger as-child>
|
||||
<div @contextmenu.capture="clearContextMenuTarget">
|
||||
<div
|
||||
class="min-w-0 overflow-x-hidden"
|
||||
@contextmenu.capture="clearContextMenuTarget"
|
||||
>
|
||||
<DataTable
|
||||
v-model:selection="selectedCommandData"
|
||||
v-model:expanded-rows="expandedRows"
|
||||
@@ -60,6 +63,7 @@
|
||||
selection-mode="single"
|
||||
context-menu
|
||||
striped-rows
|
||||
:table-style="{ tableLayout: 'fixed', width: '100%' }"
|
||||
:pt="{
|
||||
header: 'px-0'
|
||||
}"
|
||||
@@ -71,12 +75,11 @@
|
||||
field="id"
|
||||
:header="$t('g.command')"
|
||||
sortable
|
||||
class="max-w-64 2xl:max-w-full"
|
||||
:pt="{ bodyCell: 'p-1 min-h-8' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<div
|
||||
class="flex items-center gap-1 truncate"
|
||||
class="flex min-w-0 items-center gap-1 truncate"
|
||||
:class="slotProps.data.keybindings.length < 2 && 'pl-5'"
|
||||
:title="slotProps.data.id"
|
||||
>
|
||||
@@ -103,53 +106,38 @@
|
||||
<Column
|
||||
field="keybindings"
|
||||
:header="$t('g.keybinding')"
|
||||
:style="{ width: '30%' }"
|
||||
:pt="{ bodyCell: 'p-1 min-h-8' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<div
|
||||
v-if="slotProps.data.keybindings.length > 0"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<template
|
||||
v-for="(binding, idx) in (
|
||||
slotProps.data as ICommandData
|
||||
).keybindings.slice(0, 2)"
|
||||
:key="binding.combo.serialize()"
|
||||
>
|
||||
<span v-if="idx > 0" class="text-muted-foreground">,</span>
|
||||
<KeyComboDisplay
|
||||
:key-combo="binding.combo"
|
||||
:is-modified="slotProps.data.isModified"
|
||||
/>
|
||||
</template>
|
||||
<span
|
||||
v-if="slotProps.data.keybindings.length > 2"
|
||||
class="rounded-sm px-1.5 py-0.5 text-xs text-muted-foreground"
|
||||
>
|
||||
{{
|
||||
$t('g.nMoreKeybindings', {
|
||||
count: slotProps.data.keybindings.length - 2
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
<KeybindingList
|
||||
:keybindings="slotProps.data.keybindings"
|
||||
:is-modified="slotProps.data.isModified"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
<Column
|
||||
field="source"
|
||||
:header="$t('g.source')"
|
||||
:style="{ width: '16%' }"
|
||||
:pt="{ bodyCell: 'p-1 min-h-8' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<span class="overflow-hidden text-ellipsis">{{
|
||||
<span class="block truncate" :title="slotProps.data.source">{{
|
||||
slotProps.data.source || '-'
|
||||
}}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="actions" header="" :pt="{ bodyCell: 'p-1 min-h-8' }">
|
||||
<Column
|
||||
field="actions"
|
||||
header=""
|
||||
:style="{ width: '9rem' }"
|
||||
:pt="{ bodyCell: 'p-1 min-h-8 whitespace-nowrap' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<div class="actions flex flex-row justify-end">
|
||||
<div
|
||||
class="actions flex flex-row justify-end whitespace-nowrap"
|
||||
>
|
||||
<Button
|
||||
v-if="slotProps.data.keybindings.length === 1"
|
||||
v-tooltip="$t('g.edit')"
|
||||
@@ -330,6 +318,7 @@ import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
import KeybindingList from './keybinding/KeybindingList.vue'
|
||||
import KeybindingPresetToolbar from './keybinding/KeybindingPresetToolbar.vue'
|
||||
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
|
||||
|
||||
import KeybindingList from './KeybindingList.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
g: {
|
||||
nMoreKeybindings: '+ {count} more',
|
||||
nMoreKeybindingsCompact: '+ {count}',
|
||||
keybindingListAriaLabel: 'Keybindings: {combos}'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function makeKeybinding(key: string, ctrl = false, shift = false) {
|
||||
return new KeybindingImpl({
|
||||
commandId: 'test.cmd',
|
||||
combo: { key, ctrl, shift }
|
||||
})
|
||||
}
|
||||
|
||||
function renderList(props: {
|
||||
keybindings: KeybindingImpl[]
|
||||
isModified?: boolean
|
||||
}) {
|
||||
return render(KeybindingList, {
|
||||
props,
|
||||
global: { plugins: [i18n] }
|
||||
})
|
||||
}
|
||||
|
||||
describe('KeybindingList', () => {
|
||||
it('renders "-" placeholder when there are no keybindings', () => {
|
||||
renderList({ keybindings: [] })
|
||||
expect(screen.getByText('-')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('keybinding-list')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders a single keybinding without any "more" badge', () => {
|
||||
renderList({ keybindings: [makeKeybinding('A', true)] })
|
||||
expect(screen.getByTestId('keybinding-list')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByTestId('keybinding-list-more-wide')
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByTestId('keybinding-list-more-medium')
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByTestId('keybinding-list-more-compact')
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('with 2 keybindings: omits wide-tier badge, shows medium/compact for narrow widths', () => {
|
||||
renderList({
|
||||
keybindings: [makeKeybinding('A', true), makeKeybinding('B', true)]
|
||||
})
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('keybinding-list-more-wide')
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
expect(screen.getByTestId('keybinding-list-more-medium')).toHaveTextContent(
|
||||
'+ 1 more'
|
||||
)
|
||||
expect(
|
||||
screen.getByTestId('keybinding-list-more-compact')
|
||||
).toHaveTextContent('+ 1')
|
||||
})
|
||||
|
||||
it('with 3 keybindings: wide-tier uses count-minus-two, narrower tiers use count-minus-one', () => {
|
||||
renderList({
|
||||
keybindings: [
|
||||
makeKeybinding('A', true),
|
||||
makeKeybinding('B', true),
|
||||
makeKeybinding('C', true)
|
||||
]
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('keybinding-list-more-wide')).toHaveTextContent(
|
||||
'+ 1 more'
|
||||
)
|
||||
expect(screen.getByTestId('keybinding-list-more-medium')).toHaveTextContent(
|
||||
'+ 2 more'
|
||||
)
|
||||
expect(
|
||||
screen.getByTestId('keybinding-list-more-compact')
|
||||
).toHaveTextContent('+ 2')
|
||||
})
|
||||
|
||||
it('uses a container query parent so the visible tier can adapt to width', () => {
|
||||
renderList({
|
||||
keybindings: [makeKeybinding('A', true), makeKeybinding('B', true)]
|
||||
})
|
||||
expect(screen.getByTestId('keybinding-list').className).toContain(
|
||||
'@container/keybindings'
|
||||
)
|
||||
})
|
||||
|
||||
it('emits an accessible label listing all combos', () => {
|
||||
renderList({
|
||||
keybindings: [makeKeybinding('A', true), makeKeybinding('B', true, true)]
|
||||
})
|
||||
const ariaText = screen.getByTestId('keybinding-list-aria').textContent
|
||||
expect(ariaText).toContain('Keybindings:')
|
||||
expect(ariaText).toContain('Ctrl')
|
||||
expect(ariaText).toContain('A')
|
||||
expect(ariaText).toContain('Shift')
|
||||
expect(ariaText).toContain('B')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<span
|
||||
v-if="keybindings.length > 0"
|
||||
class="@container/keybindings flex w-full min-w-0 items-center gap-1 overflow-hidden"
|
||||
data-testid="keybinding-list"
|
||||
>
|
||||
<KeyComboDisplay
|
||||
:key-combo="keybindings[0].combo"
|
||||
:is-modified="isModified"
|
||||
/>
|
||||
<template v-if="keybindings.length >= 2">
|
||||
<span
|
||||
class="hidden text-muted-foreground @[16rem]/keybindings:inline"
|
||||
aria-hidden="true"
|
||||
>
|
||||
,
|
||||
</span>
|
||||
<KeyComboDisplay
|
||||
class="hidden @[16rem]/keybindings:inline-flex"
|
||||
:key-combo="keybindings[1].combo"
|
||||
:is-modified="isModified"
|
||||
/>
|
||||
</template>
|
||||
<span
|
||||
v-if="keybindings.length > 2"
|
||||
class="hidden rounded-sm px-1.5 py-0.5 text-xs text-muted-foreground @[16rem]/keybindings:inline"
|
||||
data-testid="keybinding-list-more-wide"
|
||||
>
|
||||
{{ $t('g.nMoreKeybindings', { count: keybindings.length - 2 }) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="keybindings.length >= 2"
|
||||
class="hidden rounded-sm px-1.5 py-0.5 text-xs text-muted-foreground @[12rem]/keybindings:inline @[16rem]/keybindings:hidden"
|
||||
data-testid="keybinding-list-more-medium"
|
||||
>
|
||||
{{ $t('g.nMoreKeybindings', { count: keybindings.length - 1 }) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="keybindings.length >= 2"
|
||||
class="hidden rounded-sm px-1.5 py-0.5 text-xs text-muted-foreground @[8rem]/keybindings:inline @[12rem]/keybindings:hidden"
|
||||
data-testid="keybinding-list-more-compact"
|
||||
>
|
||||
{{ $t('g.nMoreKeybindingsCompact', { count: keybindings.length - 1 }) }}
|
||||
</span>
|
||||
<span class="sr-only" data-testid="keybinding-list-aria">
|
||||
{{ ariaLabel }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { KeybindingImpl } from '@/platform/keybindings/keybinding'
|
||||
|
||||
import KeyComboDisplay from './KeyComboDisplay.vue'
|
||||
|
||||
const { keybindings, isModified = false } = defineProps<{
|
||||
keybindings: KeybindingImpl[]
|
||||
isModified?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const ariaLabel = computed(() => {
|
||||
if (keybindings.length === 0) return ''
|
||||
const combos = keybindings
|
||||
.map((binding) => binding.combo.toString())
|
||||
.join(', ')
|
||||
return t('g.keybindingListAriaLabel', { combos })
|
||||
})
|
||||
</script>
|
||||
@@ -220,4 +220,30 @@ describe('useFeatureFlags', () => {
|
||||
expect(flags.teamWorkspacesEnabled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unifiedCloudAuthEnabled', () => {
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('reads the unified_cloud_auth server feature when set', () => {
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(path, defaultValue) => {
|
||||
if (path === ServerFeatureFlag.UNIFIED_CLOUD_AUTH) return true
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
expect(flags.unifiedCloudAuthEnabled).toBe(true)
|
||||
})
|
||||
|
||||
it('lets a dev override beat the server value', () => {
|
||||
vi.mocked(api.getServerFeature).mockReturnValue(false)
|
||||
localStorage.setItem('ff:unified_cloud_auth', 'true')
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
expect(flags.unifiedCloudAuthEnabled).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,7 +28,8 @@ export enum ServerFeatureFlag {
|
||||
WORKFLOW_SHARING_ENABLED = 'workflow_sharing_enabled',
|
||||
COMFYHUB_UPLOAD_ENABLED = 'comfyhub_upload_enabled',
|
||||
COMFYHUB_PROFILE_GATE_ENABLED = 'comfyhub_profile_gate_enabled',
|
||||
SHOW_SIGNIN_BUTTON = 'show_signin_button'
|
||||
SHOW_SIGNIN_BUTTON = 'show_signin_button',
|
||||
UNIFIED_CLOUD_AUTH = 'unified_cloud_auth'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,6 +166,13 @@ export function useFeatureFlags() {
|
||||
ServerFeatureFlag.SHOW_SIGNIN_BUTTON,
|
||||
undefined
|
||||
)
|
||||
},
|
||||
get unifiedCloudAuthEnabled() {
|
||||
return resolveFlag(
|
||||
ServerFeatureFlag.UNIFIED_CLOUD_AUTH,
|
||||
remoteConfig.value.unified_cloud_auth,
|
||||
false
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"resetToDefault": "Reset to default",
|
||||
"removeKeybinding": "Remove keybinding",
|
||||
"nMoreKeybindings": "+ {count} more",
|
||||
"nMoreKeybindingsCompact": "+ {count}",
|
||||
"keybindingListAriaLabel": "Keybindings: {combos}",
|
||||
"customizeFolder": "Customize Folder",
|
||||
"icon": "Icon",
|
||||
"color": "Color",
|
||||
|
||||
@@ -103,5 +103,6 @@ export type RemoteConfig = {
|
||||
workflow_sharing_enabled?: boolean
|
||||
comfyhub_upload_enabled?: boolean
|
||||
comfyhub_profile_gate_enabled?: boolean
|
||||
unified_cloud_auth?: boolean
|
||||
sentry_dsn?: string
|
||||
}
|
||||
|
||||