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,196 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { uploadViaApiFallback } from '@/lib/uploads/client/api-fallback'
import { DirectUploadError, runUploadStrategy } from '@/lib/uploads/client/direct-upload'
const logger = createLogger('ProfilePictureUpload')
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml', 'image/webp']
interface UseProfilePictureUploadProps {
onUpload?: (url: string | null) => void
onError?: (error: string) => void
currentImage?: string | null
context?: 'profile-pictures' | 'workspace-logos'
workspaceId?: string
}
/**
* Hook for handling profile picture upload functionality.
* Manages file validation, preview generation, and server upload.
*/
export function useProfilePictureUpload({
onUpload,
onError,
currentImage,
context = 'profile-pictures',
workspaceId,
}: UseProfilePictureUploadProps = {}) {
const previewRef = useRef<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const onUploadRef = useRef(onUpload)
const onErrorRef = useRef(onError)
const currentImageRef = useRef(currentImage)
const [previewUrl, setPreviewUrl] = useState<string | null>(currentImage || null)
const [fileName, setFileName] = useState<string | null>(null)
const [isUploading, setIsUploading] = useState(false)
useEffect(() => {
onUploadRef.current = onUpload
onErrorRef.current = onError
currentImageRef.current = currentImage
}, [onUpload, onError, currentImage])
useEffect(() => {
if (previewRef.current && previewRef.current !== currentImage) {
URL.revokeObjectURL(previewRef.current)
previewRef.current = null
}
setPreviewUrl(currentImage || null)
}, [currentImage])
const validateFile = useCallback((file: File): string | null => {
if (file.size > MAX_FILE_SIZE) {
return `File "${file.name}" is too large. Maximum size is 5MB.`
}
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
return `File "${file.name}" is not a supported image format. Please use PNG, JPEG, SVG, or WebP.`
}
return null
}, [])
const handleThumbnailClick = useCallback(() => {
fileInputRef.current?.click()
}, [])
const uploadFileToServer = useCallback(
async (file: File): Promise<string> => {
const presignedEndpoint =
context === 'workspace-logos' && workspaceId
? `/api/files/presigned?type=workspace-logos&workspaceId=${encodeURIComponent(workspaceId)}`
: `/api/files/presigned?type=${context}`
try {
const result = await runUploadStrategy({
file,
workspaceId: workspaceId ?? '',
context,
presignedEndpoint,
})
logger.info(`${context} uploaded successfully: ${result.path}`)
return result.path
} catch (error) {
if (error instanceof DirectUploadError && error.code === 'FALLBACK_REQUIRED') {
const { path } = await uploadViaApiFallback(file, context, workspaceId)
logger.info(`${context} uploaded successfully via API fallback: ${path}`)
return path
}
throw error
}
},
[context, workspaceId]
)
const processFile = useCallback(
async (file: File) => {
const validationError = validateFile(file)
if (validationError) {
onErrorRef.current?.(validationError)
return
}
setFileName(file.name)
const newPreviewUrl = URL.createObjectURL(file)
if (previewRef.current) URL.revokeObjectURL(previewRef.current)
setPreviewUrl(newPreviewUrl)
previewRef.current = newPreviewUrl
setIsUploading(true)
try {
const serverUrl = await uploadFileToServer(file)
URL.revokeObjectURL(newPreviewUrl)
previewRef.current = null
setPreviewUrl(serverUrl)
onUploadRef.current?.(serverUrl)
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to upload profile picture')
onErrorRef.current?.(errorMessage)
URL.revokeObjectURL(newPreviewUrl)
previewRef.current = null
setPreviewUrl(currentImageRef.current || null)
} finally {
setIsUploading(false)
}
},
[uploadFileToServer, validateFile]
)
const handleFileChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) processFile(file)
},
[processFile]
)
const handleFileDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault()
const file = e.dataTransfer.files[0]
if (file) processFile(file)
},
[processFile]
)
const handleRemove = useCallback(() => {
if (previewRef.current) {
URL.revokeObjectURL(previewRef.current)
previewRef.current = null
}
setPreviewUrl(null)
setFileName(null)
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
onUploadRef.current?.(null)
}, [])
/**
* Restore the preview to the stored image (`currentImage`) — for a form discard
* that should revert an unsaved pick/removal without clearing the saved image.
* Unlike {@link handleRemove}, this does not emit `onUpload(null)`.
*/
const reset = useCallback(() => {
if (previewRef.current) {
URL.revokeObjectURL(previewRef.current)
previewRef.current = null
}
setPreviewUrl(currentImageRef.current || null)
setFileName(null)
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}, [])
useEffect(() => {
return () => {
if (previewRef.current) {
URL.revokeObjectURL(previewRef.current)
}
}
}, [])
return {
previewUrl,
fileName,
fileInputRef,
handleThumbnailClick,
handleFileChange,
handleFileDrop,
handleRemove,
reset,
isUploading,
}
}
@@ -0,0 +1,21 @@
import { useEffect } from 'react'
import { useSettingsDirtyStore } from '@/stores/settings/dirty/store'
/**
* Registers a single `beforeunload` guard for the whole settings surface,
* active only while some section reports unsaved changes via
* `useSettingsDirtyStore`. Mounted once in the settings shell so individual
* pages never register their own.
*/
export function useSettingsBeforeUnload() {
const isDirty = useSettingsDirtyStore((s) => s.isDirty)
useEffect(() => {
if (!isDirty) return
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault()
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
}, [isDirty])
}
@@ -0,0 +1,63 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useSettingsDirtyStore } from '@/stores/settings/dirty/store'
interface UseSettingsUnsavedGuardParams {
isDirty: boolean
}
interface SettingsUnsavedGuard {
showUnsavedModal: boolean
setShowUnsavedModal: (open: boolean) => void
/** Run `onLeave` immediately when clean; when dirty, open the confirm modal and defer it. */
guardBack: (onLeave: () => void) => void
/** Confirmed discard — close the modal and run the deferred leave action. */
confirmDiscard: () => void
}
/**
* Wires a settings surface's local dirty state into the shared
* `useSettingsDirtyStore`, so the sidebar's leave confirmation (section switch,
* Back, workspace switch) and the centralized `beforeunload` both apply without
* per-page wiring. Also provides an in-view back/close guard (`guardBack` + the
* shared `UnsavedChangesModal`) for detail sub-views whose "back" is an
* in-component state change rather than a route navigation.
*/
export function useSettingsUnsavedGuard({
isDirty,
}: UseSettingsUnsavedGuardParams): SettingsUnsavedGuard {
const setDirty = useSettingsDirtyStore((s) => s.setDirty)
const reset = useSettingsDirtyStore((s) => s.reset)
const isDirtyRef = useRef(isDirty)
const pendingLeaveRef = useRef<(() => void) | null>(null)
const [showUnsavedModal, setShowUnsavedModal] = useState(false)
useEffect(() => {
isDirtyRef.current = isDirty
setDirty(isDirty)
if (!isDirty) {
pendingLeaveRef.current = null
setShowUnsavedModal(false)
}
}, [isDirty, setDirty])
useEffect(() => {
return () => reset()
}, [reset])
const guardBack = useCallback((onLeave: () => void) => {
if (isDirtyRef.current) {
pendingLeaveRef.current = onLeave
setShowUnsavedModal(true)
} else {
onLeave()
}
}, [])
const confirmDiscard = useCallback(() => {
setShowUnsavedModal(false)
pendingLeaveRef.current?.()
pendingLeaveRef.current = null
}, [])
return { showUnsavedModal, setShowUnsavedModal, guardBack, confirmDiscard }
}