mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
Rework user selection (#1728)
* Move to new route * Convert to tailwind * Basic style * Add userStore * nit * nit * nit * Remove app.#setUser * Route to user-select view * Mock login * Use primevue UI components * handle create new user * Remove legacy user selection * Add logout button on side toolbar * Add username to logout button tooltip * Add playwright tests * hide logout button in single user server * nit
This commit is contained in:
41
browser_tests/fixtures/UserSelectPage.ts
Normal file
41
browser_tests/fixtures/UserSelectPage.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Page } from 'playwright'
|
||||||
|
import { test as base } from '@playwright/test'
|
||||||
|
|
||||||
|
export class UserSelectPage {
|
||||||
|
constructor(
|
||||||
|
public readonly url: string,
|
||||||
|
public readonly page: Page
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get selectionUrl() {
|
||||||
|
return this.url + '/user-select'
|
||||||
|
}
|
||||||
|
|
||||||
|
get container() {
|
||||||
|
return this.page.locator('#comfy-user-selection')
|
||||||
|
}
|
||||||
|
|
||||||
|
get newUserInput() {
|
||||||
|
return this.container.locator('#new-user-input')
|
||||||
|
}
|
||||||
|
|
||||||
|
get existingUserSelect() {
|
||||||
|
return this.container.locator('#existing-user-select')
|
||||||
|
}
|
||||||
|
|
||||||
|
get nextButton() {
|
||||||
|
return this.container.getByText('Next')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userSelectPageFixture = base.extend<{
|
||||||
|
userSelectPage: UserSelectPage
|
||||||
|
}>({
|
||||||
|
userSelectPage: async ({ page }, use) => {
|
||||||
|
const userSelectPage = new UserSelectPage(
|
||||||
|
process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188',
|
||||||
|
page
|
||||||
|
)
|
||||||
|
await use(userSelectPage)
|
||||||
|
}
|
||||||
|
})
|
||||||
42
browser_tests/userSelectView.spec.ts
Normal file
42
browser_tests/userSelectView.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { expect } from '@playwright/test'
|
||||||
|
import { userSelectPageFixture as test } from './fixtures/UserSelectPage'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects ComfyUI backend to be launched with `--multi-user` flag.
|
||||||
|
*/
|
||||||
|
test.describe('User Select View', () => {
|
||||||
|
test.beforeEach(async ({ userSelectPage, page }) => {
|
||||||
|
await page.goto(userSelectPage.url)
|
||||||
|
await page.evaluate(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
sessionStorage.clear()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Redirects to user select view if no user is logged in', async ({
|
||||||
|
userSelectPage,
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto(userSelectPage.url)
|
||||||
|
await expect(userSelectPage.container).toBeVisible()
|
||||||
|
expect(page.url()).toBe(userSelectPage.selectionUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Can create new user', async ({ userSelectPage, page }) => {
|
||||||
|
const randomUser = `test-user-${Math.random().toString(36).substring(2, 7)}`
|
||||||
|
await page.goto(userSelectPage.url)
|
||||||
|
await expect(page).toHaveURL(userSelectPage.selectionUrl)
|
||||||
|
await userSelectPage.newUserInput.fill(randomUser)
|
||||||
|
await userSelectPage.nextButton.click()
|
||||||
|
await expect(page).toHaveURL(userSelectPage.url)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Can choose existing user', async ({ userSelectPage, page }) => {
|
||||||
|
await page.goto(userSelectPage.url)
|
||||||
|
await expect(page).toHaveURL(userSelectPage.selectionUrl)
|
||||||
|
await userSelectPage.existingUserSelect.click()
|
||||||
|
await page.locator('.p-select-list .p-select-option').first().click()
|
||||||
|
await userSelectPage.nextButton.click()
|
||||||
|
await expect(page).toHaveURL(userSelectPage.url)
|
||||||
|
})
|
||||||
|
})
|
||||||
27
index.html
27
index.html
@@ -9,33 +9,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="litegraph grid">
|
<body class="litegraph grid">
|
||||||
<div id="vue-app"></div>
|
<div id="vue-app"></div>
|
||||||
<div id="comfy-user-selection" class="comfy-user-selection" style="display: none;">
|
|
||||||
<main class="comfy-user-selection-inner">
|
|
||||||
<h1>ComfyUI</h1>
|
|
||||||
<form>
|
|
||||||
<section>
|
|
||||||
<label>New user:
|
|
||||||
<input placeholder="Enter a username" />
|
|
||||||
</label>
|
|
||||||
</section>
|
|
||||||
<div class="comfy-user-existing">
|
|
||||||
<span class="or-separator">OR</span>
|
|
||||||
<section>
|
|
||||||
<label>
|
|
||||||
Existing user:
|
|
||||||
<select>
|
|
||||||
<option hidden disabled selected value> Select a user </option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
<span class="comfy-user-error"> </span>
|
|
||||||
<button class="comfy-btn comfy-user-button-next">Next</button>
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
<script type="module" src="src/main.ts"></script>
|
<script type="module" src="src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
@click="onTabClick(tab)"
|
@click="onTabClick(tab)"
|
||||||
/>
|
/>
|
||||||
<div class="side-tool-bar-end">
|
<div class="side-tool-bar-end">
|
||||||
|
<SidebarLogoutIcon v-if="userStore.isMultiUserServer" />
|
||||||
<SidebarThemeToggleIcon />
|
<SidebarThemeToggleIcon />
|
||||||
<SidebarSettingsToggleIcon />
|
<SidebarSettingsToggleIcon />
|
||||||
</div>
|
</div>
|
||||||
@@ -29,15 +30,18 @@
|
|||||||
import SidebarIcon from './SidebarIcon.vue'
|
import SidebarIcon from './SidebarIcon.vue'
|
||||||
import SidebarThemeToggleIcon from './SidebarThemeToggleIcon.vue'
|
import SidebarThemeToggleIcon from './SidebarThemeToggleIcon.vue'
|
||||||
import SidebarSettingsToggleIcon from './SidebarSettingsToggleIcon.vue'
|
import SidebarSettingsToggleIcon from './SidebarSettingsToggleIcon.vue'
|
||||||
|
import SidebarLogoutIcon from './SidebarLogoutIcon.vue'
|
||||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||||
|
import { useUserStore } from '@/stores/userStore'
|
||||||
|
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const teleportTarget = computed(() =>
|
const teleportTarget = computed(() =>
|
||||||
settingStore.get('Comfy.Sidebar.Location') === 'left'
|
settingStore.get('Comfy.Sidebar.Location') === 'left'
|
||||||
|
|||||||
21
src/components/sidebar/SidebarLogoutIcon.vue
Normal file
21
src/components/sidebar/SidebarLogoutIcon.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<SidebarIcon icon="pi pi-sign-out" :tooltip="tooltip" @click="logout" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SidebarIcon from './SidebarIcon.vue'
|
||||||
|
import { useUserStore } from '@/stores/userStore'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const tooltip = computed(
|
||||||
|
() => `${t('sideToolbar.logout')} (${userStore.currentUser?.username})`
|
||||||
|
)
|
||||||
|
const logout = () => {
|
||||||
|
userStore.logout()
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -3,6 +3,13 @@ export default {
|
|||||||
title: 'Welcome to ComfyUI',
|
title: 'Welcome to ComfyUI',
|
||||||
getStarted: 'Get Started'
|
getStarted: 'Get Started'
|
||||||
},
|
},
|
||||||
|
userSelect: {
|
||||||
|
newUser: 'New user',
|
||||||
|
enterUsername: 'Enter a username',
|
||||||
|
existingUser: 'Existing user',
|
||||||
|
selectUser: 'Select a user',
|
||||||
|
next: 'Next'
|
||||||
|
},
|
||||||
notSupported: {
|
notSupported: {
|
||||||
title: 'Your device is not supported',
|
title: 'Your device is not supported',
|
||||||
message: 'Only following devices are supported:',
|
message: 'Only following devices are supported:',
|
||||||
@@ -147,6 +154,7 @@ export default {
|
|||||||
newFolder: 'New Folder',
|
newFolder: 'New Folder',
|
||||||
sideToolbar: {
|
sideToolbar: {
|
||||||
themeToggle: 'Toggle Theme',
|
themeToggle: 'Toggle Theme',
|
||||||
|
logout: 'Logout',
|
||||||
queue: 'Queue',
|
queue: 'Queue',
|
||||||
nodeLibrary: 'Node Library',
|
nodeLibrary: 'Node Library',
|
||||||
workflows: 'Workflows',
|
workflows: 'Workflows',
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ export default {
|
|||||||
sideToolbar: {
|
sideToolbar: {
|
||||||
themeToggle: 'テーマの切り替え',
|
themeToggle: 'テーマの切り替え',
|
||||||
queue: 'キュー',
|
queue: 'キュー',
|
||||||
|
logout: 'ログアウト',
|
||||||
nodeLibrary: 'ノードライブラリ',
|
nodeLibrary: 'ノードライブラリ',
|
||||||
workflows: 'ワークフロー',
|
workflows: 'ワークフロー',
|
||||||
browseTemplates: 'サンプルテンプレートを表示',
|
browseTemplates: 'サンプルテンプレートを表示',
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default {
|
|||||||
sideToolbar: {
|
sideToolbar: {
|
||||||
themeToggle: 'Переключить тему',
|
themeToggle: 'Переключить тему',
|
||||||
queue: 'Очередь',
|
queue: 'Очередь',
|
||||||
|
logout: 'Выйти',
|
||||||
nodeLibrary: 'Библиотека узлов',
|
nodeLibrary: 'Библиотека узлов',
|
||||||
workflows: 'Рабочие процессы',
|
workflows: 'Рабочие процессы',
|
||||||
browseTemplates: 'Просмотреть примеры шаблонов',
|
browseTemplates: 'Просмотреть примеры шаблонов',
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default {
|
|||||||
newFolder: '新建文件夹',
|
newFolder: '新建文件夹',
|
||||||
sideToolbar: {
|
sideToolbar: {
|
||||||
themeToggle: '主题切换',
|
themeToggle: '主题切换',
|
||||||
|
logout: '登出',
|
||||||
queue: '队列',
|
queue: '队列',
|
||||||
nodeLibrary: '节点库',
|
nodeLibrary: '节点库',
|
||||||
workflows: '工作流',
|
workflows: '工作流',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from 'vue-router'
|
} from 'vue-router'
|
||||||
import LayoutDefault from '@/views/layouts/LayoutDefault.vue'
|
import LayoutDefault from '@/views/layouts/LayoutDefault.vue'
|
||||||
import { isElectron } from './utils/envUtil'
|
import { isElectron } from './utils/envUtil'
|
||||||
|
import { useUserStore } from './stores/userStore'
|
||||||
|
|
||||||
const isFileProtocol = window.location.protocol === 'file:'
|
const isFileProtocol = window.location.protocol === 'file:'
|
||||||
const basePath = isElectron() ? '/' : window.location.pathname
|
const basePath = isElectron() ? '/' : window.location.pathname
|
||||||
@@ -38,7 +39,21 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: 'GraphView',
|
name: 'GraphView',
|
||||||
component: () => import('@/views/GraphView.vue')
|
component: () => import('@/views/GraphView.vue'),
|
||||||
|
beforeEnter: async (to, from, next) => {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
await userStore.initialize()
|
||||||
|
if (userStore.needsLogin) {
|
||||||
|
next('/user-select')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'user-select',
|
||||||
|
name: 'UserSelectView',
|
||||||
|
component: () => import('@/views/UserSelectView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'server-start',
|
path: 'server-start',
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ export class ComfyApi extends EventTarget {
|
|||||||
* The current client id from websocket status updates.
|
* The current client id from websocket status updates.
|
||||||
*/
|
*/
|
||||||
clientId?: string
|
clientId?: string
|
||||||
|
/**
|
||||||
|
* The current user id.
|
||||||
|
*/
|
||||||
user: string
|
user: string
|
||||||
socket: WebSocket | null = null
|
socket: WebSocket | null = null
|
||||||
|
|
||||||
@@ -49,7 +52,6 @@ export class ComfyApi extends EventTarget {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
// api.user is set by ComfyApp.setup()
|
|
||||||
this.user = ''
|
this.user = ''
|
||||||
this.api_host = location.host
|
this.api_host = location.host
|
||||||
this.api_base = location.pathname.split('/').slice(0, -1).join('/')
|
this.api_base = location.pathname.split('/').slice(0, -1).join('/')
|
||||||
|
|||||||
@@ -1711,67 +1711,6 @@ export class ComfyApp {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async #setUser() {
|
|
||||||
const userConfig = await api.getUserConfig()
|
|
||||||
// Return in single user mode.
|
|
||||||
if (userConfig.users === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = localStorage['Comfy.userId']
|
|
||||||
const users = userConfig.users ?? {}
|
|
||||||
if (!user || !users[user]) {
|
|
||||||
// Lift spinner / BlockUI for user selection.
|
|
||||||
if (this.vueAppReady) useWorkspaceStore().spinner = false
|
|
||||||
|
|
||||||
// This will rarely be hit so move the loading to on demand
|
|
||||||
const { UserSelectionScreen } = await import('./ui/userSelection')
|
|
||||||
|
|
||||||
this.ui.menuContainer.style.display = 'none'
|
|
||||||
const { userId, username } = await new UserSelectionScreen().show(
|
|
||||||
users,
|
|
||||||
user
|
|
||||||
)
|
|
||||||
this.ui.menuContainer.style.display = ''
|
|
||||||
|
|
||||||
user = userId
|
|
||||||
localStorage['Comfy.userName'] = username
|
|
||||||
localStorage['Comfy.userId'] = user
|
|
||||||
}
|
|
||||||
|
|
||||||
api.user = user
|
|
||||||
|
|
||||||
this.ui.settings.addSetting({
|
|
||||||
id: 'Comfy.SwitchUser',
|
|
||||||
name: 'Switch User',
|
|
||||||
type: (name) => {
|
|
||||||
let currentUser = localStorage['Comfy.userName']
|
|
||||||
if (currentUser) {
|
|
||||||
currentUser = ` (${currentUser})`
|
|
||||||
}
|
|
||||||
return $el('tr', [
|
|
||||||
$el('td', [
|
|
||||||
$el('label', {
|
|
||||||
textContent: name
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
$el('td', [
|
|
||||||
$el('button', {
|
|
||||||
textContent: name + (currentUser ?? ''),
|
|
||||||
onclick: () => {
|
|
||||||
delete localStorage['Comfy.userId']
|
|
||||||
delete localStorage['Comfy.userName']
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
])
|
|
||||||
])
|
|
||||||
},
|
|
||||||
// TODO: Is that the correct default value?
|
|
||||||
defaultValue: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up the app on the page
|
* Set up the app on the page
|
||||||
*/
|
*/
|
||||||
@@ -1779,7 +1718,6 @@ export class ComfyApp {
|
|||||||
this.canvasEl = canvasEl
|
this.canvasEl = canvasEl
|
||||||
// Show menu container for GraphView.
|
// Show menu container for GraphView.
|
||||||
this.ui.menuContainer.style.display = 'block'
|
this.ui.menuContainer.style.display = 'block'
|
||||||
await this.#setUser()
|
|
||||||
|
|
||||||
this.resizeCanvas()
|
this.resizeCanvas()
|
||||||
|
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
.comfy-user-selection {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 999;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: sans-serif;
|
|
||||||
background: linear-gradient(var(--tr-even-bg-color), var(--tr-odd-bg-color));
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner {
|
|
||||||
background: var(--comfy-menu-bg);
|
|
||||||
margin-top: -30vh;
|
|
||||||
padding: 20px 40px;
|
|
||||||
border-radius: 10px;
|
|
||||||
min-width: 365px;
|
|
||||||
position: relative;
|
|
||||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner form {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner h1 {
|
|
||||||
margin: 10px 0 30px 0;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner label {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection input,
|
|
||||||
.comfy-user-selection select {
|
|
||||||
background-color: var(--comfy-input-bg);
|
|
||||||
color: var(--input-text);
|
|
||||||
border: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 5px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection input::placeholder {
|
|
||||||
color: var(--descrip-text);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-existing {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-users .comfy-user-existing {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner .or-separator {
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 10px;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
color: var(--descrip-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner .or-separator {
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: center;
|
|
||||||
margin-left: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner .or-separator::before,
|
|
||||||
.comfy-user-selection-inner .or-separator::after {
|
|
||||||
content: "";
|
|
||||||
background-color: var(--border-color);
|
|
||||||
position: relative;
|
|
||||||
height: 1px;
|
|
||||||
vertical-align: middle;
|
|
||||||
display: inline-block;
|
|
||||||
width: calc(50% - 20px);
|
|
||||||
top: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner .or-separator::before {
|
|
||||||
right: 10px;
|
|
||||||
margin-left: -50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner .or-separator::after {
|
|
||||||
left: 10px;
|
|
||||||
margin-right: -50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner section {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
margin: -10px;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner section.selected {
|
|
||||||
background: var(--border-color);
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner footer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-selection-inner .comfy-user-error {
|
|
||||||
color: var(--error-text);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-user-button-next {
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
width: 100px;
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
// @ts-strict-ignore
|
|
||||||
import { api } from '../api'
|
|
||||||
import { $el } from '../ui'
|
|
||||||
import { createSpinner } from './spinner'
|
|
||||||
import './userSelection.css'
|
|
||||||
|
|
||||||
interface SelectedUser {
|
|
||||||
username: string
|
|
||||||
userId: string
|
|
||||||
created: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserSelectionScreen {
|
|
||||||
async show(users, user): Promise<SelectedUser> {
|
|
||||||
const userSelection = document.getElementById('comfy-user-selection')
|
|
||||||
userSelection.style.display = ''
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const input = userSelection.getElementsByTagName('input')[0]
|
|
||||||
const select = userSelection.getElementsByTagName('select')[0]
|
|
||||||
const inputSection = input.closest('section')
|
|
||||||
const selectSection = select.closest('section')
|
|
||||||
const form = userSelection.getElementsByTagName('form')[0]
|
|
||||||
const error = userSelection.getElementsByClassName('comfy-user-error')[0]
|
|
||||||
const button = userSelection.getElementsByClassName(
|
|
||||||
'comfy-user-button-next'
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
let inputActive = null
|
|
||||||
input.addEventListener('focus', () => {
|
|
||||||
inputSection.classList.add('selected')
|
|
||||||
selectSection.classList.remove('selected')
|
|
||||||
inputActive = true
|
|
||||||
})
|
|
||||||
select.addEventListener('focus', () => {
|
|
||||||
inputSection.classList.remove('selected')
|
|
||||||
selectSection.classList.add('selected')
|
|
||||||
inputActive = false
|
|
||||||
select.style.color = ''
|
|
||||||
})
|
|
||||||
select.addEventListener('blur', () => {
|
|
||||||
if (!select.value) {
|
|
||||||
select.style.color = 'var(--descrip-text)'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
form.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (inputActive == null) {
|
|
||||||
error.textContent =
|
|
||||||
'Please enter a username or select an existing user.'
|
|
||||||
} else if (inputActive) {
|
|
||||||
const username = input.value.trim()
|
|
||||||
if (!username) {
|
|
||||||
error.textContent = 'Please enter a username.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new user
|
|
||||||
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
|
|
||||||
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
|
|
||||||
input.disabled =
|
|
||||||
select.disabled =
|
|
||||||
// @ts-expect-error
|
|
||||||
input.readonly =
|
|
||||||
// @ts-expect-error
|
|
||||||
select.readonly =
|
|
||||||
true
|
|
||||||
const spinner = createSpinner()
|
|
||||||
button.prepend(spinner)
|
|
||||||
try {
|
|
||||||
const resp = await api.createUser(username)
|
|
||||||
if (resp.status >= 300) {
|
|
||||||
let message =
|
|
||||||
'Error creating user: ' + resp.status + ' ' + resp.statusText
|
|
||||||
try {
|
|
||||||
const res = await resp.json()
|
|
||||||
if (res.error) {
|
|
||||||
message = res.error
|
|
||||||
}
|
|
||||||
} catch (error) {}
|
|
||||||
throw new Error(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve({ username, userId: await resp.json(), created: true })
|
|
||||||
} catch (err) {
|
|
||||||
spinner.remove()
|
|
||||||
error.textContent =
|
|
||||||
err.message ??
|
|
||||||
err.statusText ??
|
|
||||||
err ??
|
|
||||||
'An unknown error occurred.'
|
|
||||||
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
|
|
||||||
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
|
|
||||||
input.disabled =
|
|
||||||
select.disabled =
|
|
||||||
// @ts-expect-error
|
|
||||||
input.readonly =
|
|
||||||
// @ts-expect-error
|
|
||||||
select.readonly =
|
|
||||||
false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if (!select.value) {
|
|
||||||
error.textContent = 'Please select an existing user.'
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
resolve({
|
|
||||||
username: users[select.value],
|
|
||||||
userId: select.value,
|
|
||||||
created: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
const name = localStorage['Comfy.userName']
|
|
||||||
if (name) {
|
|
||||||
input.value = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (input.value) {
|
|
||||||
// Focus the input, do this separately as sometimes browsers like to fill in the value
|
|
||||||
input.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
const userIds = Object.keys(users ?? {})
|
|
||||||
if (userIds.length) {
|
|
||||||
for (const u of userIds) {
|
|
||||||
$el('option', { textContent: users[u], value: u, parent: select })
|
|
||||||
}
|
|
||||||
select.style.color = 'var(--descrip-text)'
|
|
||||||
|
|
||||||
if (select.value) {
|
|
||||||
// Focus the select, do this separately as sometimes browsers like to fill in the value
|
|
||||||
select.focus()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
userSelection.classList.add('no-users')
|
|
||||||
input.focus()
|
|
||||||
}
|
|
||||||
}).then((r: SelectedUser) => {
|
|
||||||
userSelection.remove()
|
|
||||||
return r
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
109
src/stores/userStore.ts
Normal file
109
src/stores/userStore.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { api } from '@/scripts/api'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, ref, watchEffect } from 'vue'
|
||||||
|
import type { User as UserConfig } from '@/types/apiTypes'
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
userId: string
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
/**
|
||||||
|
* The user config. null if not loaded.
|
||||||
|
*/
|
||||||
|
const userConfig = ref<UserConfig | null>(null)
|
||||||
|
/**
|
||||||
|
* The current user id. null if not logged in or in single user mode.
|
||||||
|
*/
|
||||||
|
const currentUserId = ref<string | null>(null)
|
||||||
|
const isMultiUserServer = computed(
|
||||||
|
() => userConfig.value && 'users' in userConfig.value
|
||||||
|
)
|
||||||
|
const needsLogin = computed(
|
||||||
|
() => !currentUserId.value && isMultiUserServer.value
|
||||||
|
)
|
||||||
|
const users = computed<User[]>(() =>
|
||||||
|
Object.entries(userConfig.value?.users ?? {}).map(([userId, username]) => ({
|
||||||
|
userId,
|
||||||
|
username
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const currentUser = computed<User | null>(
|
||||||
|
() =>
|
||||||
|
users.value.find((user) => user.userId === currentUserId.value) ?? null
|
||||||
|
)
|
||||||
|
const initialized = computed(() => userConfig.value !== null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the user store.
|
||||||
|
*/
|
||||||
|
async function initialize() {
|
||||||
|
userConfig.value = await api.getUserConfig()
|
||||||
|
currentUserId.value = localStorage['Comfy.userId']
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user.
|
||||||
|
*
|
||||||
|
* @param username - The username.
|
||||||
|
* @returns The new user.
|
||||||
|
*/
|
||||||
|
async function createUser(username: string): Promise<User> {
|
||||||
|
const resp = await api.createUser(username)
|
||||||
|
const data = await resp.json()
|
||||||
|
if (resp.status >= 300) {
|
||||||
|
throw new Error(
|
||||||
|
data.error ??
|
||||||
|
'Error creating user: ' + resp.status + ' ' + resp.statusText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
userId: data,
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login the current user.
|
||||||
|
*
|
||||||
|
* @param user - The user.
|
||||||
|
*/
|
||||||
|
async function login({
|
||||||
|
userId,
|
||||||
|
username
|
||||||
|
}: {
|
||||||
|
userId: string
|
||||||
|
username: string
|
||||||
|
}) {
|
||||||
|
currentUserId.value = userId
|
||||||
|
localStorage['Comfy.userId'] = userId
|
||||||
|
localStorage['Comfy.userName'] = username
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (isMultiUserServer.value && currentUserId.value) {
|
||||||
|
api.user = currentUserId.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout the current user.
|
||||||
|
*/
|
||||||
|
async function logout() {
|
||||||
|
delete localStorage['Comfy.userId']
|
||||||
|
delete localStorage['Comfy.userName']
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
users,
|
||||||
|
currentUser,
|
||||||
|
isMultiUserServer,
|
||||||
|
needsLogin,
|
||||||
|
initialized,
|
||||||
|
initialize,
|
||||||
|
createUser,
|
||||||
|
login,
|
||||||
|
logout
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -431,8 +431,10 @@ export const zSystemStats = z.object({
|
|||||||
})
|
})
|
||||||
const zUser = z.object({
|
const zUser = z.object({
|
||||||
storage: z.enum(['server']),
|
storage: z.enum(['server']),
|
||||||
migrated: z.boolean(),
|
// `migrated` is only available in single-user mode.
|
||||||
users: z.record(z.string(), z.unknown())
|
migrated: z.boolean().optional(),
|
||||||
|
// `users` is only available in multi-user server mode.
|
||||||
|
users: z.record(z.string(), z.string()).optional()
|
||||||
})
|
})
|
||||||
const zUserData = z.array(z.array(z.string(), z.string()))
|
const zUserData = z.array(z.array(z.string(), z.string()))
|
||||||
const zUserDataFullInfo = z.object({
|
const zUserDataFullInfo = z.object({
|
||||||
|
|||||||
90
src/views/UserSelectView.vue
Normal file
90
src/views/UserSelectView.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
id="comfy-user-selection"
|
||||||
|
class="font-sans flex flex-col items-center h-screen m-0 text-neutral-300 bg-neutral-900 dark-theme pointer-events-auto"
|
||||||
|
>
|
||||||
|
<main
|
||||||
|
class="mt-[5vh] 2xl:mt-[20vh] min-w-84 relative rounded-lg bg-[var(--comfy-menu-bg)] p-5 px-10 shadow-lg"
|
||||||
|
>
|
||||||
|
<h1 class="my-2.5 mb-7 font-normal">ComfyUI</h1>
|
||||||
|
<form class="flex w-full flex-col items-center">
|
||||||
|
<div class="flex w-full flex-col gap-2">
|
||||||
|
<label for="new-user-input">{{ $t('userSelect.newUser') }}:</label>
|
||||||
|
<InputText
|
||||||
|
id="new-user-input"
|
||||||
|
v-model="newUsername"
|
||||||
|
:placeholder="$t('userSelect.enterUsername')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<div class="flex w-full flex-col gap-2">
|
||||||
|
<label for="existing-user-select"
|
||||||
|
>{{ $t('userSelect.existingUser') }}:</label
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
v-model="selectedUser"
|
||||||
|
class="w-full"
|
||||||
|
inputId="existing-user-select"
|
||||||
|
:options="userStore.users"
|
||||||
|
option-label="username"
|
||||||
|
:placeholder="$t('userSelect.selectUser')"
|
||||||
|
:disabled="createNewUser"
|
||||||
|
/>
|
||||||
|
<Message v-if="error" severity="error">{{ error }}</Message>
|
||||||
|
</div>
|
||||||
|
<footer class="mt-5">
|
||||||
|
<Button :label="$t('userSelect.next')" @click="login" />
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import Divider from 'primevue/divider'
|
||||||
|
import InputText from 'primevue/inputtext'
|
||||||
|
import Select from 'primevue/select'
|
||||||
|
import Message from 'primevue/message'
|
||||||
|
import { User, useUserStore } from '@/stores/userStore'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const selectedUser = ref<User | null>(null)
|
||||||
|
const newUsername = ref('')
|
||||||
|
const loginError = ref('')
|
||||||
|
|
||||||
|
const createNewUser = computed(() => newUsername.value.trim() !== '')
|
||||||
|
const newUserExistsError = computed(() => {
|
||||||
|
return userStore.users.find((user) => user.username === newUsername.value)
|
||||||
|
? `User "${newUsername.value}" already exists`
|
||||||
|
: ''
|
||||||
|
})
|
||||||
|
const error = computed(() => newUserExistsError.value || loginError.value)
|
||||||
|
|
||||||
|
const login = async () => {
|
||||||
|
try {
|
||||||
|
const user = createNewUser.value
|
||||||
|
? await userStore.createUser(newUsername.value)
|
||||||
|
: selectedUser.value
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('No user selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
userStore.login(user)
|
||||||
|
router.push('/')
|
||||||
|
} catch (err) {
|
||||||
|
loginError.value = err.message ?? JSON.stringify(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!userStore.initialized) {
|
||||||
|
await userStore.initialize()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
// @ts-strict-ignore
|
|
||||||
import { start } from '../../utils'
|
|
||||||
import lg from '../../utils/litegraph'
|
|
||||||
|
|
||||||
describe('users', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
lg.setup(global)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
lg.teardown(global)
|
|
||||||
})
|
|
||||||
|
|
||||||
function expectNoUserScreen() {
|
|
||||||
// Ensure login isnt visible
|
|
||||||
const selection = document.querySelectorAll('#comfy-user-selection')?.[0]
|
|
||||||
expect(selection['style'].display).toBe('none')
|
|
||||||
const menu = document.querySelectorAll('.comfy-menu')?.[0]
|
|
||||||
expect(window.getComputedStyle(menu)?.display).not.toBe('none')
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('multi-user', () => {
|
|
||||||
async function mockAddStylesheet() {
|
|
||||||
const utils = await import('../../../src/scripts/utils')
|
|
||||||
utils.addStylesheet = jest.fn().mockReturnValue(Promise.resolve())
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForUserScreenShow() {
|
|
||||||
// Wait for "show" to be called
|
|
||||||
const { UserSelectionScreen } = await import(
|
|
||||||
'../../../src/scripts/ui/userSelection'
|
|
||||||
)
|
|
||||||
let resolve, reject
|
|
||||||
const fn = UserSelectionScreen.prototype.show
|
|
||||||
const p = new Promise((res, rej) => {
|
|
||||||
resolve = res
|
|
||||||
reject = rej
|
|
||||||
})
|
|
||||||
jest
|
|
||||||
.spyOn(UserSelectionScreen.prototype, 'show')
|
|
||||||
.mockImplementation(async (...args) => {
|
|
||||||
const res = fn(...args)
|
|
||||||
await new Promise(process.nextTick) // wait for promises to resolve
|
|
||||||
resolve()
|
|
||||||
return res
|
|
||||||
})
|
|
||||||
setTimeout(
|
|
||||||
() => reject('timeout waiting for UserSelectionScreen to be shown.'),
|
|
||||||
500
|
|
||||||
)
|
|
||||||
await p
|
|
||||||
await new Promise(process.nextTick) // wait for promises to resolve
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testUserScreen(onShown, users?) {
|
|
||||||
if (!users) {
|
|
||||||
users = {}
|
|
||||||
}
|
|
||||||
const starting = start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { storage: 'server', users },
|
|
||||||
preSetup: mockAddStylesheet
|
|
||||||
})
|
|
||||||
|
|
||||||
// Ensure no current user
|
|
||||||
expect(localStorage['Comfy.userId']).toBeFalsy()
|
|
||||||
expect(localStorage['Comfy.userName']).toBeFalsy()
|
|
||||||
|
|
||||||
await waitForUserScreenShow()
|
|
||||||
|
|
||||||
const selection = document.querySelectorAll('#comfy-user-selection')?.[0]
|
|
||||||
expect(selection).toBeTruthy()
|
|
||||||
|
|
||||||
// Ensure login is visible
|
|
||||||
expect(window.getComputedStyle(selection)?.display).not.toBe('none')
|
|
||||||
// Ensure menu is hidden
|
|
||||||
const menu = document.querySelectorAll('.comfy-menu')?.[0]
|
|
||||||
expect(window.getComputedStyle(menu)?.display).toBe('none')
|
|
||||||
|
|
||||||
const isCreate = await onShown(selection)
|
|
||||||
|
|
||||||
// Submit form
|
|
||||||
selection.querySelectorAll('form')[0].submit()
|
|
||||||
await new Promise(process.nextTick) // wait for promises to resolve
|
|
||||||
|
|
||||||
// Wait for start
|
|
||||||
const s = await starting
|
|
||||||
|
|
||||||
// Ensure login is removed
|
|
||||||
expect(document.querySelectorAll('#comfy-user-selection')).toHaveLength(0)
|
|
||||||
expect(window.getComputedStyle(menu)?.display).not.toBe('none')
|
|
||||||
|
|
||||||
// Ensure settings + templates are saved
|
|
||||||
const { api } = await import('../../../src/scripts/api')
|
|
||||||
expect(api.createUser).toHaveBeenCalledTimes(+isCreate)
|
|
||||||
expect(api.storeSettings).toHaveBeenCalledTimes(+isCreate)
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledTimes(+isCreate)
|
|
||||||
if (isCreate) {
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledWith(
|
|
||||||
'comfy.templates.json',
|
|
||||||
null,
|
|
||||||
{ stringify: false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return { users, selection, ...s }
|
|
||||||
}
|
|
||||||
|
|
||||||
it('allows user creation if no users', async () => {
|
|
||||||
const { users } = await testUserScreen((selection) => {
|
|
||||||
// Ensure we have no users flag added
|
|
||||||
expect(selection.classList.contains('no-users')).toBeTruthy()
|
|
||||||
|
|
||||||
// Enter a username
|
|
||||||
const input = selection.getElementsByTagName('input')[0]
|
|
||||||
input.focus()
|
|
||||||
input.value = 'Test User'
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(users).toStrictEqual({
|
|
||||||
'Test User!': 'Test User'
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(localStorage['Comfy.userId']).toBe('Test User!')
|
|
||||||
expect(localStorage['Comfy.userName']).toBe('Test User')
|
|
||||||
})
|
|
||||||
it('allows user creation if no current user but other users', async () => {
|
|
||||||
const users = {
|
|
||||||
'Test User 2!': 'Test User 2'
|
|
||||||
}
|
|
||||||
|
|
||||||
await testUserScreen((selection) => {
|
|
||||||
expect(selection.classList.contains('no-users')).toBeFalsy()
|
|
||||||
|
|
||||||
// Enter a username
|
|
||||||
const input = selection.getElementsByTagName('input')[0]
|
|
||||||
input.focus()
|
|
||||||
input.value = 'Test User 3'
|
|
||||||
return true
|
|
||||||
}, users)
|
|
||||||
|
|
||||||
expect(users).toStrictEqual({
|
|
||||||
'Test User 2!': 'Test User 2',
|
|
||||||
'Test User 3!': 'Test User 3'
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(localStorage['Comfy.userId']).toBe('Test User 3!')
|
|
||||||
expect(localStorage['Comfy.userName']).toBe('Test User 3')
|
|
||||||
})
|
|
||||||
it('allows user selection if no current user but other users', async () => {
|
|
||||||
const users = {
|
|
||||||
'A!': 'A',
|
|
||||||
'B!': 'B',
|
|
||||||
'C!': 'C'
|
|
||||||
}
|
|
||||||
|
|
||||||
await testUserScreen((selection) => {
|
|
||||||
expect(selection.classList.contains('no-users')).toBeFalsy()
|
|
||||||
|
|
||||||
// Check user list
|
|
||||||
const select = selection.getElementsByTagName('select')[0]
|
|
||||||
const options = select.getElementsByTagName('option')
|
|
||||||
expect(
|
|
||||||
[...options]
|
|
||||||
.filter((o) => !o.disabled)
|
|
||||||
.reduce((p, n) => {
|
|
||||||
p[n.getAttribute('value')] = n.textContent
|
|
||||||
return p
|
|
||||||
}, {})
|
|
||||||
).toStrictEqual(users)
|
|
||||||
|
|
||||||
// Select an option
|
|
||||||
select.focus()
|
|
||||||
select.value = options[2].value
|
|
||||||
|
|
||||||
return false
|
|
||||||
}, users)
|
|
||||||
|
|
||||||
expect(users).toStrictEqual(users)
|
|
||||||
|
|
||||||
expect(localStorage['Comfy.userId']).toBe('B!')
|
|
||||||
expect(localStorage['Comfy.userName']).toBe('B')
|
|
||||||
})
|
|
||||||
it('doesnt show user screen if current user', async () => {
|
|
||||||
const starting = start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: {
|
|
||||||
storage: 'server',
|
|
||||||
users: {
|
|
||||||
'User!': 'User'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
localStorage: {
|
|
||||||
'Comfy.userId': 'User!',
|
|
||||||
'Comfy.userName': 'User'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await new Promise(process.nextTick) // wait for promises to resolve
|
|
||||||
|
|
||||||
expectNoUserScreen()
|
|
||||||
|
|
||||||
await starting
|
|
||||||
})
|
|
||||||
it('allows user switching', async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: {
|
|
||||||
storage: 'server',
|
|
||||||
users: {
|
|
||||||
'User!': 'User'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
localStorage: {
|
|
||||||
'Comfy.userId': 'User!',
|
|
||||||
'Comfy.userName': 'User'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// cant actually test switching user easily but can check the setting is present
|
|
||||||
expect(app.ui.settings.settingsLookup['Comfy.SwitchUser']).toBeTruthy()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
describe('single-user', () => {
|
|
||||||
it('doesnt show user creation if no default user', async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { migrated: false, storage: 'server' }
|
|
||||||
})
|
|
||||||
expectNoUserScreen()
|
|
||||||
|
|
||||||
// It should store the settings
|
|
||||||
const { api } = await import('../../../src/scripts/api')
|
|
||||||
expect(api.storeSettings).toHaveBeenCalledTimes(1)
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledTimes(1)
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledWith(
|
|
||||||
'comfy.templates.json',
|
|
||||||
null,
|
|
||||||
{ stringify: false }
|
|
||||||
)
|
|
||||||
})
|
|
||||||
it('doesnt show user creation if default user', async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { migrated: true, storage: 'server' }
|
|
||||||
})
|
|
||||||
expectNoUserScreen()
|
|
||||||
|
|
||||||
// It should store the settings
|
|
||||||
const { api } = await import('../../../src/scripts/api')
|
|
||||||
expect(api.storeSettings).toHaveBeenCalledTimes(0)
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledTimes(0)
|
|
||||||
})
|
|
||||||
it('doesnt allow user switching', async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { migrated: true, storage: 'server' }
|
|
||||||
})
|
|
||||||
expectNoUserScreen()
|
|
||||||
|
|
||||||
expect(app.ui.settings.settingsLookup['Comfy.SwitchUser']).toBeFalsy()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
describe('browser-user', () => {
|
|
||||||
it('doesnt show user creation if no default user', async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { migrated: false, storage: 'browser' }
|
|
||||||
})
|
|
||||||
expectNoUserScreen()
|
|
||||||
|
|
||||||
// It should store the settings
|
|
||||||
const { api } = await import('../../../src/scripts/api')
|
|
||||||
expect(api.storeSettings).toHaveBeenCalledTimes(0)
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledTimes(0)
|
|
||||||
})
|
|
||||||
it('doesnt show user creation if default user', async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { migrated: true, storage: 'server' }
|
|
||||||
})
|
|
||||||
expectNoUserScreen()
|
|
||||||
|
|
||||||
// It should store the settings
|
|
||||||
const { api } = await import('../../../src/scripts/api')
|
|
||||||
expect(api.storeSettings).toHaveBeenCalledTimes(0)
|
|
||||||
expect(api.storeUserData).toHaveBeenCalledTimes(0)
|
|
||||||
})
|
|
||||||
it('doesnt allow user switching', async () => {
|
|
||||||
const { app } = await start({
|
|
||||||
resetEnv: true,
|
|
||||||
userConfig: { migrated: true, storage: 'browser' }
|
|
||||||
})
|
|
||||||
expectNoUserScreen()
|
|
||||||
|
|
||||||
expect(app.ui.settings.settingsLookup['Comfy.SwitchUser']).toBeFalsy()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user