chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,28 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { groupForkFilesIntoFolders } from '@/ee/workspace-forking/components/fork-file-tree/fork-file-tree'
describe('groupForkFilesIntoFolders', () => {
it('groups files under their folder and lifts un-foldered files to the root bucket', () => {
const { folders, rootFiles } = groupForkFilesIntoFolders([
{ id: 'f1', label: 'b.png', folderId: 'fld-1', folderName: 'Images' },
{ id: 'f2', label: 'a.png', folderId: 'fld-1', folderName: 'Images' },
{ id: 'f3', label: 'root.txt', folderId: null, folderName: null },
{ id: 'f4', label: 'doc.pdf', folderId: 'fld-2', folderName: 'Docs' },
])
// Folders are sorted by name; each folder's files are sorted by label.
expect(folders.map((folder) => folder.name)).toEqual(['Docs', 'Images'])
expect(folders[1].files.map((file) => file.label)).toEqual(['a.png', 'b.png'])
expect(rootFiles.map((file) => file.label)).toEqual(['root.txt'])
})
it('treats a file whose folder was deleted (id set, name null) as a root file', () => {
const { folders, rootFiles } = groupForkFilesIntoFolders([
{ id: 'f1', label: 'orphan.png', folderId: 'fld-deleted', folderName: null },
])
expect(folders).toEqual([])
expect(rootFiles.map((file) => file.id)).toEqual(['f1'])
})
})
@@ -0,0 +1,197 @@
'use client'
import { useId, useState } from 'react'
import { Checkbox, ChevronDown, cn } from '@sim/emcn'
export interface ForkFileTreeItem {
id: string
label: string
}
export interface ForkFileTreeFolder {
id: string
name: string
files: ForkFileTreeItem[]
}
/** A flat copyable file with its folder grouping, before {@link groupForkFilesIntoFolders}. */
export interface ForkFlatFile {
id: string
label: string
folderId: string | null
folderName: string | null
}
/**
* Group flat files into folders (sorted by name, each file sorted by label) plus a root bucket
* for files with no folder. A file whose folder id is set but whose name is null (its folder was
* deleted) falls into the root bucket, so it stays selectable rather than hiding under a phantom.
*/
export function groupForkFilesIntoFolders(files: ForkFlatFile[]): {
folders: ForkFileTreeFolder[]
rootFiles: ForkFileTreeItem[]
} {
const folderById = new Map<string, ForkFileTreeFolder>()
const rootFiles: ForkFileTreeItem[] = []
for (const file of files) {
const item: ForkFileTreeItem = { id: file.id, label: file.label }
if (file.folderId && file.folderName) {
let folder = folderById.get(file.folderId)
if (!folder) {
folder = { id: file.folderId, name: file.folderName, files: [] }
folderById.set(file.folderId, folder)
}
folder.files.push(item)
} else {
rootFiles.push(item)
}
}
const folders = Array.from(folderById.values()).sort((a, b) => a.name.localeCompare(b.name))
for (const folder of folders) folder.files.sort((a, b) => a.label.localeCompare(b.label))
rootFiles.sort((a, b) => a.label.localeCompare(b.label))
return { folders, rootFiles }
}
interface ForkFileTreeProps {
folders: ForkFileTreeFolder[]
rootFiles: ForkFileTreeItem[]
isSelected: (fileId: string) => boolean
onToggleFile: (fileId: string, checked: boolean) => void
/** Toggle every file in a folder at once (the folder-level select-all). */
onToggleMany: (fileIds: string[], checked: boolean) => void
disabled?: boolean
}
/**
* Folder ▸ file collapsible tree shared by the fork picker and the sync copy selector, so both
* surfaces group files identically. Each folder is a tri-state select-all header that expands to
* its files; files with no folder render at the top level. Files are the only copyable kind that
* nests - tables, knowledge bases, custom tools, and skills stay flat at the top level.
*/
export function ForkFileTree({
folders,
rootFiles,
isSelected,
onToggleFile,
onToggleMany,
disabled = false,
}: ForkFileTreeProps) {
return (
<div className='flex flex-col gap-1'>
{folders.map((folder) => (
<ForkFileFolderRow
key={folder.id}
folder={folder}
isSelected={isSelected}
onToggleFile={onToggleFile}
onToggleMany={onToggleMany}
disabled={disabled}
/>
))}
{rootFiles.map((file) => (
<ForkFileRow
key={file.id}
file={file}
checked={isSelected(file.id)}
onToggle={onToggleFile}
disabled={disabled}
/>
))}
</div>
)
}
interface ForkFileFolderRowProps {
folder: ForkFileTreeFolder
isSelected: (fileId: string) => boolean
onToggleFile: (fileId: string, checked: boolean) => void
onToggleMany: (fileIds: string[], checked: boolean) => void
disabled: boolean
}
function ForkFileFolderRow({
folder,
isSelected,
onToggleFile,
onToggleMany,
disabled,
}: ForkFileFolderRowProps) {
const [expanded, setExpanded] = useState(false)
const fileIds = folder.files.map((file) => file.id)
const total = fileIds.length
const selectedCount = fileIds.filter(isSelected).length
const headerState = selectedCount === 0 ? false : selectedCount === total ? true : 'indeterminate'
return (
<div className='flex flex-col gap-1'>
<div className='flex items-center gap-2 text-[var(--text-body)] text-sm'>
<Checkbox
size='sm'
aria-label={`Copy all in ${folder.name}`}
checked={headerState}
onCheckedChange={() => onToggleMany(fileIds, headerState !== true)}
disabled={disabled}
/>
<button
type='button'
className='flex min-w-0 flex-1 items-center gap-1 text-left hover:text-[var(--text-primary)]'
onClick={() => setExpanded((value) => !value)}
>
<span className='min-w-0 flex-1 truncate'>
{folder.name} ({selectedCount > 0 ? `${selectedCount}/${total}` : total})
</span>
<ChevronDown
className={cn(
'h-[6px] w-[10px] flex-shrink-0 text-[var(--text-icon)] transition-transform',
expanded && 'rotate-180'
)}
/>
</button>
</div>
{expanded ? (
<div className='ml-6 flex flex-col gap-0.5'>
{folder.files.map((file) => (
<ForkFileRow
key={file.id}
file={file}
checked={isSelected(file.id)}
onToggle={onToggleFile}
disabled={disabled}
/>
))}
</div>
) : null}
</div>
)
}
interface ForkFileRowProps {
file: ForkFileTreeItem
checked: boolean
onToggle: (fileId: string, checked: boolean) => void
disabled: boolean
}
function ForkFileRow({ file, checked, onToggle, disabled }: ForkFileRowProps) {
const itemId = useId()
return (
<label
htmlFor={itemId}
className={cn(
'flex min-w-0 items-center gap-2 rounded-md py-0.5 text-[var(--text-body)] text-sm',
disabled
? 'cursor-not-allowed opacity-60'
: 'cursor-pointer hover:text-[var(--text-primary)]'
)}
>
<Checkbox
id={itemId}
size='sm'
checked={checked}
onCheckedChange={(value) => onToggle(file.id, value === true)}
disabled={disabled}
/>
<span className='min-w-0 flex-1 truncate'>{file.label}</span>
</label>
)
}