[backport cloud/1.38] feat: sort workspaces (#8776)

Backport of #8770 to `cloud/1.38`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8776-backport-cloud-1-38-feat-sort-workspaces-3036d73d36508173bb59f3b4ac8e463f)
by [Unito](https://www.unito.io)

Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
This commit is contained in:
Comfy Org PR Bot
2026-02-11 03:31:40 +09:00
committed by GitHub
parent c12aa37599
commit e2744868a4
4 changed files with 92 additions and 12 deletions

View File

@@ -53,7 +53,9 @@ describe('useWorkspaceSwitch', () => {
id: 'workspace-1',
name: 'Test Workspace',
type: 'personal',
role: 'owner'
role: 'owner',
created_at: '2026-01-01T00:00:00Z',
joined_at: '2026-01-01T00:00:00Z'
}
mockModifiedWorkflows.length = 0
})

View File

@@ -11,6 +11,8 @@ interface Workspace {
id: string
name: string
type: WorkspaceType
created_at: string
joined_at: string
}
export interface WorkspaceWithRole extends Workspace {

View File

@@ -1,7 +1,7 @@
import { createPinia, setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useTeamWorkspaceStore } from './teamWorkspaceStore'
import { sortWorkspaces, useTeamWorkspaceStore } from './teamWorkspaceStore'
// Mock workspaceAuthStore
const mockWorkspaceAuthStore = vi.hoisted(() => ({
@@ -92,21 +92,27 @@ const mockPersonalWorkspace = {
id: 'ws-personal-123',
name: 'Personal',
type: 'personal' as const,
role: 'owner' as const
role: 'owner' as const,
created_at: '2026-01-01T00:00:00Z',
joined_at: '2026-01-01T00:00:00Z'
}
const mockTeamWorkspace = {
id: 'ws-team-456',
name: 'Team Alpha',
type: 'team' as const,
role: 'owner' as const
role: 'owner' as const,
created_at: '2026-02-01T00:00:00Z',
joined_at: '2026-02-01T00:00:00Z'
}
const mockMemberWorkspace = {
id: 'ws-team-789',
name: 'Team Beta',
type: 'team' as const,
role: 'member' as const
role: 'member' as const,
created_at: '2026-03-01T00:00:00Z',
joined_at: '2026-03-01T00:00:00Z'
}
describe('useTeamWorkspaceStore', () => {
@@ -308,7 +314,9 @@ describe('useTeamWorkspaceStore', () => {
id: 'ws-new-999',
name: 'New Workspace',
type: 'team' as const,
role: 'member' as const
role: 'member' as const,
created_at: '2026-04-01T00:00:00Z',
joined_at: '2026-04-01T00:00:00Z'
}
mockWorkspaceApi.list
@@ -346,7 +354,9 @@ describe('useTeamWorkspaceStore', () => {
id: 'ws-new-created',
name: 'Created Workspace',
type: 'team' as const,
role: 'owner' as const
role: 'owner' as const,
created_at: '2026-05-01T00:00:00Z',
joined_at: '2026-05-01T00:00:00Z'
}
mockWorkspaceApi.create.mockResolvedValue(newWorkspace)
@@ -385,7 +395,9 @@ describe('useTeamWorkspaceStore', () => {
id: 'ws-new',
name: 'New Workspace',
type: 'team',
role: 'owner'
role: 'owner',
created_at: '2026-06-01T00:00:00Z',
joined_at: '2026-06-01T00:00:00Z'
})
await resultPromise
})
@@ -571,7 +583,9 @@ describe('useTeamWorkspaceStore', () => {
id: `ws-owned-${i}`,
name: `Owned ${i}`,
type: 'team' as const,
role: 'owner' as const
role: 'owner' as const,
created_at: `2026-${String(i + 1).padStart(2, '0')}-01T00:00:00Z`,
joined_at: `2026-${String(i + 1).padStart(2, '0')}-01T00:00:00Z`
}))
mockWorkspaceApi.list.mockResolvedValue({
@@ -915,3 +929,49 @@ describe('useTeamWorkspaceStore', () => {
})
})
})
describe('sortWorkspaces', () => {
it('places personal first, then sorts ascending by created_at for owners and joined_at for members', () => {
const input = [
{
created_at: '2026-06-01T00:00:00Z',
id: 'w-team-new-owner',
joined_at: '2026-01-01T00:00:00Z',
name: 'Newest Owner Team',
role: 'owner' as const,
type: 'team' as const
},
{
created_at: '2026-12-01T00:00:00Z',
id: 'w-personal',
joined_at: '2026-12-01T00:00:00Z',
name: 'Personal Workspace',
role: 'owner' as const,
type: 'personal' as const
},
{
created_at: '2026-01-01T00:00:00Z',
id: 'w-team-member',
joined_at: '2026-04-01T00:00:00Z',
name: 'Member Team',
role: 'member' as const,
type: 'team' as const
},
{
created_at: '2026-02-01T00:00:00Z',
id: 'w-team-old-owner',
joined_at: '2026-09-01T00:00:00Z',
name: 'Oldest Owner Team',
role: 'owner' as const,
type: 'team' as const
}
]
expect(sortWorkspaces(input).map((w) => w.id)).toEqual([
'w-personal',
'w-team-old-owner',
'w-team-member',
'w-team-new-owner'
])
})
})

View File

@@ -76,6 +76,16 @@ function createWorkspaceState(workspace: WorkspaceWithRole): WorkspaceState {
}
}
export function sortWorkspaces<T extends WorkspaceWithRole>(list: T[]): T[] {
return [...list].sort((a, b) => {
if (a.type === 'personal') return -1
if (b.type === 'personal') return 1
const dateA = a.role === 'owner' ? a.created_at : a.joined_at
const dateB = b.role === 'owner' ? b.created_at : b.joined_at
return dateA.localeCompare(dateB)
})
}
function getLastWorkspaceId(): string | null {
try {
return localStorage.getItem(WORKSPACE_STORAGE_KEYS.LAST_WORKSPACE_ID)
@@ -205,7 +215,9 @@ export const useTeamWorkspaceStore = defineStore('teamWorkspace', () => {
if (hasValidSession && workspaceAuthStore.currentWorkspace) {
// Valid session exists - fetch workspace list and verify access
const response = await workspaceApi.list()
workspaces.value = response.workspaces.map(createWorkspaceState)
workspaces.value = sortWorkspaces(
response.workspaces.map(createWorkspaceState)
)
if (workspaces.value.length === 0) {
throw new Error('No workspaces available')
@@ -247,7 +259,9 @@ export const useTeamWorkspaceStore = defineStore('teamWorkspace', () => {
// 2. No valid session - fetch workspaces and pick default
const response = await workspaceApi.list()
workspaces.value = response.workspaces.map(createWorkspaceState)
workspaces.value = sortWorkspaces(
response.workspaces.map(createWorkspaceState)
)
if (workspaces.value.length === 0) {
throw new Error('No workspaces available')
@@ -315,7 +329,9 @@ export const useTeamWorkspaceStore = defineStore('teamWorkspace', () => {
isFetchingWorkspaces.value = true
try {
const response = await workspaceApi.list()
workspaces.value = response.workspaces.map(createWorkspaceState)
workspaces.value = sortWorkspaces(
response.workspaces.map(createWorkspaceState)
)
} finally {
isFetchingWorkspaces.value = false
}