mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 14:44:48 +00:00
feat: enhance storage provider management and settings
- Added functionality to manage storage providers, including adding, editing, and toggling active status. - Introduced utility functions for normalizing and serializing storage provider configurations. - Implemented masking of sensitive fields in storage provider settings. - Updated the dashboard UI components to reflect the new storage provider management features. - Added new constants and utility functions for better handling of storage provider data. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
22
.vscode/settings.json
vendored
22
.vscode/settings.json
vendored
@@ -11,8 +11,28 @@
|
||||
},
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
[
|
||||
"([\"'`][^\"'`]*.*?[\"'`])",
|
||||
"cn\\(([^)]*)\\)",
|
||||
"[\"'`]([^\"'`]*).*?[\"'`]"
|
||||
],
|
||||
[
|
||||
"clsxm\\(([^)]*)\\)",
|
||||
"[\"'`]([^\"'`]*).*?[\"'`]"
|
||||
],
|
||||
[
|
||||
"tv\\(([^)]*)\\)",
|
||||
"(?:'|\"|`)([^']*)(?:'|\"|`)"
|
||||
],
|
||||
[
|
||||
"tw`([^`]*)`",
|
||||
"([^`]*)"
|
||||
],
|
||||
[
|
||||
"[a-zA-Z]+[cC]lass[nN]ame[\"'`]?:\\s*[\"'`]([^\"'`]*)[\"'`]",
|
||||
"([^\"'`]*)"
|
||||
],
|
||||
[
|
||||
"[a-zA-Z]+[cC]lass[nN]ame\\s*=\\s*[\"'`]([^\"'`]*)[\"'`]",
|
||||
"([^\"'`]*)"
|
||||
]
|
||||
],
|
||||
// If you do not want to autofix some rules on save
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"@afilmory/framework": "workspace:*",
|
||||
"@afilmory/redis": "workspace:*",
|
||||
"@afilmory/task-queue": "workspace:*",
|
||||
"@afilmory/utils": "workspace:*",
|
||||
"@aws-sdk/client-s3": "3.916.0",
|
||||
"@hono/node-server": "^1.19.5",
|
||||
"better-auth": "1.3.29",
|
||||
|
||||
@@ -18,12 +18,14 @@ export class SettingController {
|
||||
}
|
||||
|
||||
@Get('/:key')
|
||||
@BypassResponseTransform()
|
||||
async get(@Param() { key }: GetSettingDto) {
|
||||
const value = await this.settingService.get(key, {})
|
||||
return { key, value }
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
@BypassResponseTransform()
|
||||
async getMany(@Query() query: GetSettingsQueryDto) {
|
||||
const keys = query?.keys ?? []
|
||||
const targetKeys = keys.length > 0 ? keys : Array.from(SettingKeys)
|
||||
|
||||
@@ -12,6 +12,8 @@ import { TenantService } from '../tenant/tenant.service'
|
||||
import { AES_ALGORITHM, AUTH_TAG_LENGTH, DEFAULT_SETTING_METADATA, IV_LENGTH } from './setting.constant'
|
||||
import type { SettingKeyType, SettingRecord, SettingUiSchemaResponse, SettingValueMap } from './setting.type'
|
||||
import { SETTING_UI_SCHEMA, SETTING_UI_SCHEMA_KEYS } from './setting.ui-schema'
|
||||
import { STORAGE_PROVIDERS_SETTING_KEY } from './storage-provider.constants'
|
||||
import { prepareStorageProvidersForPersist, sanitizeStorageProviders } from './storage-provider.utils'
|
||||
|
||||
export type SettingOption = {
|
||||
tenantId?: string
|
||||
@@ -55,8 +57,13 @@ export class SettingService {
|
||||
if (!record) {
|
||||
return null
|
||||
}
|
||||
const value = record.isSensitive ? this.decrypt(record.value) : record.value
|
||||
return value
|
||||
const rawValue = record.isSensitive ? this.decrypt(record.value) : record.value
|
||||
|
||||
if (key === STORAGE_PROVIDERS_SETTING_KEY) {
|
||||
return sanitizeStorageProviders(rawValue)
|
||||
}
|
||||
|
||||
return rawValue
|
||||
}
|
||||
|
||||
async getMany<K extends readonly SettingKeyType[]>(
|
||||
@@ -85,7 +92,14 @@ export class SettingService {
|
||||
acc[key] = null
|
||||
return acc
|
||||
}
|
||||
acc[key] = record.isSensitive ? this.decrypt(record.value) : record.value
|
||||
const rawValue = record.isSensitive ? this.decrypt(record.value) : record.value
|
||||
|
||||
if (key === STORAGE_PROVIDERS_SETTING_KEY) {
|
||||
acc[key] = sanitizeStorageProviders(rawValue)
|
||||
return acc
|
||||
}
|
||||
|
||||
acc[key] = rawValue
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
@@ -96,8 +110,19 @@ export class SettingService {
|
||||
const tenantId = await this.resolveTenantId(options)
|
||||
const existing = await this.findSettingRecord(key, tenantId)
|
||||
const defaultMetadata = isSettingKey(key) ? DEFAULT_SETTING_METADATA[key] : undefined
|
||||
const isSensitive = options.isSensitive ?? defaultMetadata?.isSensitive ?? existing?.isSensitive ?? false
|
||||
const payload = isSensitive ? this.encrypt(value) : value
|
||||
let isSensitive = options.isSensitive ?? defaultMetadata?.isSensitive ?? existing?.isSensitive ?? false
|
||||
let persistedValue = value
|
||||
let maskedValue = value
|
||||
|
||||
if (key === STORAGE_PROVIDERS_SETTING_KEY) {
|
||||
const existingRaw = existing ? (existing.isSensitive ? this.decrypt(existing.value) : existing.value) : null
|
||||
const result = prepareStorageProvidersForPersist(value, existingRaw)
|
||||
persistedValue = result.stored
|
||||
maskedValue = result.masked
|
||||
isSensitive = true
|
||||
}
|
||||
|
||||
const payload = isSensitive ? this.encrypt(persistedValue) : persistedValue
|
||||
const db = this.dbAccessor.get()
|
||||
|
||||
const insertPayload: typeof settings.$inferInsert = {
|
||||
@@ -121,7 +146,7 @@ export class SettingService {
|
||||
set: updatePayload,
|
||||
})
|
||||
|
||||
await this.eventEmitter.emit('setting.updated', { tenantId, key, value })
|
||||
await this.eventEmitter.emit('setting.updated', { tenantId, key, value: maskedValue })
|
||||
}
|
||||
|
||||
async setMany(entries: readonly SettingEntryInput[]): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export const STORAGE_PROVIDERS_SETTING_KEY = 'builder.storage.providers'
|
||||
|
||||
export const STORAGE_PROVIDER_SENSITIVE_FIELDS: Record<string, readonly string[]> = {
|
||||
s3: ['secretAccessKey'],
|
||||
github: ['token'],
|
||||
}
|
||||
134
be/apps/core/src/modules/setting/storage-provider.utils.ts
Normal file
134
be/apps/core/src/modules/setting/storage-provider.utils.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { STORAGE_PROVIDER_SECRET_PLACEHOLDER } from '@afilmory/utils'
|
||||
|
||||
import { STORAGE_PROVIDER_SENSITIVE_FIELDS } from './storage-provider.constants'
|
||||
|
||||
export interface BuilderStorageProvider {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
config: Record<string, string>
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
function normalizeConfig (config: unknown): Record<string, string> {
|
||||
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const result: Record<string, string> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
result[key] = typeof value === 'string' ? value : value == null ? '' : String(value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function normalizeProvider (input: unknown): BuilderStorageProvider | null {
|
||||
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const record = input as Record<string, unknown>
|
||||
const now = new Date().toISOString()
|
||||
|
||||
return {
|
||||
id: typeof record.id === 'string' && record.id.trim().length > 0 ? record.id.trim() : randomUUID(),
|
||||
name: typeof record.name === 'string' && record.name.trim().length > 0 ? record.name.trim() : '未命名存储',
|
||||
type: typeof record.type === 'string' ? record.type : 'local',
|
||||
config: normalizeConfig(record.config),
|
||||
createdAt: typeof record.createdAt === 'string' ? record.createdAt : now,
|
||||
updatedAt: typeof record.updatedAt === 'string' ? record.updatedAt : now,
|
||||
}
|
||||
}
|
||||
|
||||
export function parseStorageProviders (raw: string | null): BuilderStorageProvider[] {
|
||||
if (!raw) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!Array.isArray(parsed)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return parsed.map((item) => normalizeProvider(item)).filter((item): item is BuilderStorageProvider => item !== null)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeStorageProviders (providers: BuilderStorageProvider[]): string {
|
||||
return JSON.stringify(providers)
|
||||
}
|
||||
|
||||
export function maskStorageProviderSecrets (providers: BuilderStorageProvider[]): BuilderStorageProvider[] {
|
||||
return providers.map((provider) => {
|
||||
const sensitiveKeys = STORAGE_PROVIDER_SENSITIVE_FIELDS[provider.type] ?? []
|
||||
const maskedConfig: Record<string, string> = { ...provider.config }
|
||||
|
||||
for (const key of sensitiveKeys) {
|
||||
if (maskedConfig[key] && maskedConfig[key].length > 0) {
|
||||
maskedConfig[key] = STORAGE_PROVIDER_SECRET_PLACEHOLDER
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...provider,
|
||||
config: maskedConfig,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function mergeStorageProviderSecrets (incoming: BuilderStorageProvider[],
|
||||
existing: BuilderStorageProvider[]): BuilderStorageProvider[] {
|
||||
const existingMap = new Map(existing.map((provider) => [provider.id, provider]))
|
||||
const now = new Date().toISOString()
|
||||
|
||||
return incoming.map((provider) => {
|
||||
const previous = existingMap.get(provider.id)
|
||||
const sensitiveKeys = STORAGE_PROVIDER_SENSITIVE_FIELDS[provider.type] ?? []
|
||||
const mergedConfig: Record<string, string> = { ...provider.config }
|
||||
|
||||
for (const key of sensitiveKeys) {
|
||||
const value = mergedConfig[key] ?? ''
|
||||
if (value === STORAGE_PROVIDER_SECRET_PLACEHOLDER) {
|
||||
mergedConfig[key] = previous?.config[key] ?? ''
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...provider,
|
||||
config: mergedConfig,
|
||||
createdAt: previous?.createdAt ?? provider.createdAt ?? now,
|
||||
updatedAt: now,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function sanitizeStorageProviders (raw: string | null): string {
|
||||
const normalized = typeof raw === 'string' ? raw.trim() : ''
|
||||
if (!normalized) {
|
||||
return '[]'
|
||||
}
|
||||
|
||||
const providers = parseStorageProviders(normalized)
|
||||
const masked = maskStorageProviderSecrets(providers)
|
||||
return serializeStorageProviders(masked)
|
||||
}
|
||||
|
||||
export function prepareStorageProvidersForPersist (incomingRaw: string,
|
||||
existingRaw: string | null): { stored: string; masked: string } {
|
||||
const incomingProviders = parseStorageProviders(incomingRaw)
|
||||
const existingProviders = parseStorageProviders(existingRaw)
|
||||
const merged = mergeStorageProviderSecrets(incomingProviders, existingProviders)
|
||||
return {
|
||||
stored: serializeStorageProviders(merged),
|
||||
masked: serializeStorageProviders(maskStorageProviderSecrets(merged)),
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Body, Controller, Get, Patch } from '@afilmory/framework'
|
||||
import { Roles } from 'core/guards/roles.decorator'
|
||||
import { BypassResponseTransform } from 'core/interceptors/response-transform.decorator'
|
||||
|
||||
import { SuperAdminSettingService } from '../system-setting/super-admin-setting.service'
|
||||
import { UpdateSuperAdminSettingsDto } from './super-admin.dto'
|
||||
@@ -10,11 +11,13 @@ export class SuperAdminSettingController {
|
||||
constructor(private readonly superAdminSettings: SuperAdminSettingService) {}
|
||||
|
||||
@Get('/')
|
||||
@BypassResponseTransform()
|
||||
async getOverview() {
|
||||
return await this.superAdminSettings.getOverview()
|
||||
}
|
||||
|
||||
@Patch('/')
|
||||
@BypassResponseTransform()
|
||||
async update(@Body() dto: UpdateSuperAdminSettingsDto) {
|
||||
await this.superAdminSettings.updateSettings(dto)
|
||||
return await this.superAdminSettings.getOverview()
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"jotai": "2.15.0",
|
||||
"lucide-react": "0.547.0",
|
||||
"motion": "12.23.24",
|
||||
"nanoid": "5.1.6",
|
||||
"ofetch": "1.4.1",
|
||||
"radix-ui": "1.4.3",
|
||||
"react": "19.2.0",
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { clsxm, Spring } from '@afilmory/utils'
|
||||
import { DynamicIcon } from 'lucide-react/dynamic'
|
||||
import { m } from 'motion/react'
|
||||
import type { FC } from 'react'
|
||||
|
||||
type AddProviderCardProps = {
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export const AddProviderCard: FC<AddProviderCardProps> = ({ onClick }) => {
|
||||
return (
|
||||
<m.button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={Spring.presets.smooth}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className={clsxm(
|
||||
'group relative flex flex-col items-center justify-center gap-3 overflow-hidden bg-background-tertiary p-5 transition-all duration-200',
|
||||
'hover:shadow-lg',
|
||||
'min-h-[180px]',
|
||||
)}
|
||||
>
|
||||
{/* Linear gradient borders with accent color on hover */}
|
||||
<div className="via-text/20 group-hover:via-accent/60 absolute top-0 right-0 left-0 h-[0.5px] bg-linear-to-r from-transparent to-transparent transition-opacity" />
|
||||
<div className="via-text/20 group-hover:via-accent/60 absolute top-0 right-0 bottom-0 w-[0.5px] bg-linear-to-b from-transparent to-transparent transition-opacity" />
|
||||
<div className="via-text/20 group-hover:via-accent/60 absolute right-0 bottom-0 left-0 h-[0.5px] bg-linear-to-r from-transparent to-transparent transition-opacity" />
|
||||
<div className="via-text/20 group-hover:via-accent/60 absolute top-0 bottom-0 left-0 w-[0.5px] bg-linear-to-b from-transparent to-transparent transition-opacity" />
|
||||
|
||||
{/* Icon */}
|
||||
<div className="relative">
|
||||
<div className="border-accent/30 bg-accent/5 group-hover:border-accent/60 group-hover:bg-accent/10 flex h-12 w-12 items-center justify-center rounded-lg border-2 border-dashed transition-all duration-200">
|
||||
<DynamicIcon
|
||||
name="plus"
|
||||
className="text-accent h-6 w-6 transition-transform duration-200 group-hover:scale-110"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div className="relative text-center">
|
||||
<p className="text-text text-sm font-semibold">Add Provider</p>
|
||||
<p className="text-text-tertiary text-xs">Configure a new storage</p>
|
||||
</div>
|
||||
</m.button>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { clsxm, Spring } from '@afilmory/utils'
|
||||
import { Button } from '@afilmory/ui'
|
||||
import { clsxm } from '@afilmory/utils'
|
||||
import { DynamicIcon } from 'lucide-react/dynamic'
|
||||
import { m } from 'motion/react'
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { StorageProvider } from '../types'
|
||||
@@ -41,13 +41,15 @@ const providerTypeConfig = {
|
||||
type ProviderCardProps = {
|
||||
provider: StorageProvider
|
||||
isActive: boolean
|
||||
onClick: () => void
|
||||
onEdit: () => void
|
||||
onToggleActive: () => void
|
||||
}
|
||||
|
||||
export const ProviderCard: FC<ProviderCardProps> = ({
|
||||
provider,
|
||||
isActive,
|
||||
onClick,
|
||||
onEdit,
|
||||
onToggleActive,
|
||||
}) => {
|
||||
const config =
|
||||
providerTypeConfig[provider.type as keyof typeof providerTypeConfig] ||
|
||||
@@ -76,16 +78,9 @@ export const ProviderCard: FC<ProviderCardProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<m.button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={Spring.presets.smooth}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
<div
|
||||
className={clsxm(
|
||||
'group relative flex flex-col gap-3 overflow-hidden bg-background-tertiary p-5 text-left transition-all duration-200',
|
||||
'group relative flex size-full flex-col gap-3 overflow-hidden bg-background-tertiary p-5 text-left transition-all duration-200',
|
||||
'hover:shadow-lg',
|
||||
)}
|
||||
>
|
||||
@@ -131,13 +126,36 @@ export const ProviderCard: FC<ProviderCardProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Hover Edit Indicator */}
|
||||
<div className="bg-accent/0 group-hover:bg-accent/5 absolute inset-0 flex items-center justify-center opacity-0 transition-all duration-200 group-hover:opacity-100">
|
||||
<span className="bg-accent flex items-center gap-1.5 rounded px-3 py-1.5 text-xs font-medium text-white shadow-lg">
|
||||
<DynamicIcon name="pencil" className="h-3.5 w-3.5" />
|
||||
Edit
|
||||
</span>
|
||||
{/* Actions - bottom right */}
|
||||
<div className="absolute bottom-3 right-3 flex items-center">
|
||||
{isActive ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-text-secondary hover:text-text"
|
||||
onClick={onToggleActive}
|
||||
>
|
||||
<DynamicIcon name="x-circle" className="h-3.5 w-3.5 mr-1" />
|
||||
<span>Make Inactive</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="border-accent/30 bg-accent/10 text-accent hover:bg-accent/20 border"
|
||||
onClick={onToggleActive}
|
||||
>
|
||||
<DynamicIcon name="check" className="h-3.5 w-3.5" />
|
||||
<span>Make Active</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" variant="ghost" size="sm" onClick={onEdit}>
|
||||
<DynamicIcon name="pencil" className="h-3.5 w-3.5 mr-1" />
|
||||
<span>Edit</span>
|
||||
</Button>
|
||||
</div>
|
||||
</m.button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { clsxm, Spring } from '@afilmory/utils'
|
||||
import { DynamicIcon } from 'lucide-react/dynamic'
|
||||
import { m } from 'motion/react'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import {
|
||||
@@ -28,15 +29,13 @@ type ProviderEditModalProps = ModalComponentProps & {
|
||||
activeProviderId: string | null
|
||||
onSave: (provider: StorageProvider) => void
|
||||
onSetActive: (id: string) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
export const ProviderEditModal = ({
|
||||
provider,
|
||||
activeProviderId,
|
||||
|
||||
onSave,
|
||||
onSetActive,
|
||||
onDelete,
|
||||
|
||||
dismiss,
|
||||
}: ProviderEditModalProps) => {
|
||||
const [formData, setFormData] = useState<StorageProvider | null>(provider)
|
||||
@@ -50,7 +49,6 @@ export const ProviderEditModal = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [providerKey])
|
||||
|
||||
const isActive = provider?.id === activeProviderId
|
||||
const isNewProvider = !provider?.id
|
||||
|
||||
const selectedFields = useMemo(() => {
|
||||
@@ -88,23 +86,11 @@ export const ProviderEditModal = ({
|
||||
|
||||
const handleSave = () => {
|
||||
if (!formData) return
|
||||
formData.id = formData.id ?? nanoid()
|
||||
onSave(formData)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!formData?.id) return
|
||||
if (window.confirm('确定要删除这个存储提供商吗?此操作无法撤销。')) {
|
||||
onDelete(formData.id)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSetActive = () => {
|
||||
if (!formData?.id) return
|
||||
onSetActive(formData.id)
|
||||
}
|
||||
|
||||
if (!formData) return null
|
||||
|
||||
return (
|
||||
@@ -261,10 +247,7 @@ export const ProviderEditModal = ({
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="shrink-0 px-6 pt-4 pb-6">
|
||||
{/* Horizontal divider */}
|
||||
<div className="via-text/20 mb-4 h-[0.5px] bg-linear-to-r from-transparent to-transparent" />
|
||||
|
||||
<div className="shrink-0 px-6 pt-4 pb-6 border-t">
|
||||
{isNewProvider ? (
|
||||
// Add mode: Simple cancel + create actions
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
@@ -283,64 +266,23 @@ export const ProviderEditModal = ({
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
<DynamicIcon name="plus" className="h-3.5 w-3.5" />
|
||||
<DynamicIcon name="plus" className="h-3.5 w-3.5 mr-2" />
|
||||
<span>Create Provider</span>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
// Edit mode: Delete + cancel + set active + save
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
variant="ghost"
|
||||
onClick={handleSave}
|
||||
disabled={!isDirty}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="text-red hover:bg-red/10"
|
||||
>
|
||||
<DynamicIcon name="trash-2" className="h-3.5 w-3.5" />
|
||||
<span>Delete</span>
|
||||
<DynamicIcon name="save" className="h-3.5 w-3.5 mr-2" />
|
||||
<span>Save Changes</span>
|
||||
</Button>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={dismiss}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-text-secondary hover:text-text"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
{isActive ? (
|
||||
<span className="bg-accent inline-flex items-center gap-1.5 rounded px-4 py-2 text-xs font-semibold text-white uppercase">
|
||||
<DynamicIcon name="check-circle" className="h-3.5 w-3.5" />
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSetActive}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="border-accent/30 bg-accent/10 text-accent hover:bg-accent/20 border"
|
||||
>
|
||||
<DynamicIcon name="check" className="h-3.5 w-3.5" />
|
||||
<span>Set Active</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={!isDirty}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
<DynamicIcon name="save" className="h-3.5 w-3.5" />
|
||||
<span>Save Changes</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from '../hooks'
|
||||
import type { StorageProvider } from '../types'
|
||||
import { createEmptyProvider, reorderProvidersByActive } from '../utils'
|
||||
import { AddProviderCard } from './AddProviderCard'
|
||||
import { ProviderCard } from './ProviderCard'
|
||||
import { ProviderEditModal } from './ProviderEditModal'
|
||||
|
||||
@@ -54,7 +53,6 @@ export const StorageProvidersManager = () => {
|
||||
activeProviderId,
|
||||
onSave: handleSaveProvider,
|
||||
onSetActive: handleSetActive,
|
||||
onDelete: handleDeleteProvider,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,20 +80,6 @@ export const StorageProvidersManager = () => {
|
||||
markDirty()
|
||||
}
|
||||
|
||||
const handleDeleteProvider = (providerId: string) => {
|
||||
setProviders((prev) => {
|
||||
const next = prev.filter((provider) => provider.id !== providerId)
|
||||
const nextActive = next.some(
|
||||
(provider) => provider.id === activeProviderId,
|
||||
)
|
||||
? activeProviderId
|
||||
: (next[0]?.id ?? null)
|
||||
setActiveProviderId(nextActive)
|
||||
return next
|
||||
})
|
||||
markDirty()
|
||||
}
|
||||
|
||||
const handleSetActive = (providerId: string) => {
|
||||
setActiveProviderId(providerId)
|
||||
markDirty()
|
||||
@@ -106,7 +90,7 @@ export const StorageProvidersManager = () => {
|
||||
activeProviderId &&
|
||||
providers.some((provider) => provider.id === activeProviderId)
|
||||
? activeProviderId
|
||||
: (providers[0]?.id ?? null)
|
||||
: null
|
||||
|
||||
updateMutation.mutate(
|
||||
{
|
||||
@@ -146,6 +130,14 @@ export const StorageProvidersManager = () => {
|
||||
|
||||
const headerActionPortal = (
|
||||
<MainPageLayout.Actions>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleAddProvider}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
新增提供商
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
@@ -216,20 +208,16 @@ export const StorageProvidersManager = () => {
|
||||
<ProviderCard
|
||||
provider={provider}
|
||||
isActive={provider.id === activeProviderId}
|
||||
onClick={() => handleEditProvider(provider)}
|
||||
onEdit={() => handleEditProvider(provider)}
|
||||
onToggleActive={() => {
|
||||
setActiveProviderId((prev) =>
|
||||
prev === provider.id ? null : provider.id,
|
||||
)
|
||||
markDirty()
|
||||
}}
|
||||
/>
|
||||
</m.div>
|
||||
))}
|
||||
<m.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
...Spring.presets.smooth,
|
||||
delay: orderedProviders.length * 0.05,
|
||||
}}
|
||||
>
|
||||
<AddProviderCard onClick={handleAddProvider} />
|
||||
</m.div>
|
||||
</m.div>
|
||||
|
||||
{/* Status Message */}
|
||||
|
||||
@@ -3,14 +3,18 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { getSettings, updateSettings } from '~/modules/settings'
|
||||
|
||||
import { STORAGE_SETTING_KEYS } from './constants'
|
||||
import type { StorageProvidersPayload } from './types'
|
||||
import type { StorageProvider, StorageProvidersPayload } from './types'
|
||||
import {
|
||||
ensureActiveProviderId,
|
||||
normalizeStorageProviderConfig,
|
||||
parseStorageProviders,
|
||||
serializeStorageProviders,
|
||||
} from './utils'
|
||||
|
||||
export const STORAGE_PROVIDERS_QUERY_KEY = ['settings', 'storage-providers'] as const
|
||||
export const STORAGE_PROVIDERS_QUERY_KEY = [
|
||||
'settings',
|
||||
'storage-providers',
|
||||
] as const
|
||||
|
||||
export const useStorageProvidersQuery = () => {
|
||||
return useQuery({
|
||||
@@ -21,12 +25,16 @@ export const useStorageProvidersQuery = () => {
|
||||
STORAGE_SETTING_KEYS.activeProvider,
|
||||
])
|
||||
|
||||
const rawProviders = response.values[STORAGE_SETTING_KEYS.providers] ?? '[]'
|
||||
const providers = parseStorageProviders(rawProviders)
|
||||
const rawProviders =
|
||||
response.values[STORAGE_SETTING_KEYS.providers] ?? '[]'
|
||||
const providers = parseStorageProviders(rawProviders).map((provider) =>
|
||||
normalizeStorageProviderConfig(provider),
|
||||
)
|
||||
const activeProviderRaw =
|
||||
response.values[STORAGE_SETTING_KEYS.activeProvider] ?? ''
|
||||
const activeProviderId =
|
||||
typeof activeProviderRaw === 'string' && activeProviderRaw.trim().length > 0
|
||||
typeof activeProviderRaw === 'string' &&
|
||||
activeProviderRaw.trim().length > 0
|
||||
? activeProviderRaw.trim()
|
||||
: null
|
||||
|
||||
@@ -43,10 +51,23 @@ export const useUpdateStorageProvidersMutation = () => {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (payload: StorageProvidersPayload) => {
|
||||
const currentProviders = payload.providers.map((provider) =>
|
||||
normalizeStorageProviderConfig(provider),
|
||||
)
|
||||
const previousProviders = queryClient.getQueryData<{
|
||||
providers: StorageProvider[]
|
||||
activeProviderId: string | null
|
||||
}>(STORAGE_PROVIDERS_QUERY_KEY)?.providers
|
||||
|
||||
const resolvedProviders = restoreProviderSecrets(
|
||||
currentProviders,
|
||||
previousProviders ?? [],
|
||||
)
|
||||
|
||||
await updateSettings([
|
||||
{
|
||||
key: STORAGE_SETTING_KEYS.providers,
|
||||
value: serializeStorageProviders(payload.providers),
|
||||
value: serializeStorageProviders(resolvedProviders),
|
||||
},
|
||||
{
|
||||
key: STORAGE_SETTING_KEYS.activeProvider,
|
||||
@@ -61,3 +82,25 @@ export const useUpdateStorageProvidersMutation = () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const restoreProviderSecrets = (
|
||||
nextProviders: StorageProvider[],
|
||||
previousProviders: StorageProvider[],
|
||||
): StorageProvider[] => {
|
||||
const previousMap = new Map(
|
||||
previousProviders.map((provider) => [provider.id, provider]),
|
||||
)
|
||||
|
||||
return nextProviders.map((provider) => {
|
||||
const previous = previousMap.get(provider.id)
|
||||
const config: Record<string, string> = { ...provider.config }
|
||||
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
if (value.trim().length === 0 && previous) {
|
||||
config[key] = previous.config[key] ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
return { ...provider, config }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,6 +104,15 @@ export const serializeStorageProviders = (
|
||||
)
|
||||
}
|
||||
|
||||
export const normalizeStorageProviderConfig = (
|
||||
provider: StorageProvider,
|
||||
): StorageProvider => {
|
||||
return {
|
||||
...provider,
|
||||
config: normaliseConfigForType(provider.type, provider.config),
|
||||
}
|
||||
}
|
||||
|
||||
export const getDefaultConfigForType = (
|
||||
type: StorageProviderType,
|
||||
): Record<string, string> => {
|
||||
|
||||
@@ -17,19 +17,18 @@ const buttonVariants = tv({
|
||||
variant: {
|
||||
primary: [
|
||||
'border-transparent',
|
||||
'text-white dark:text-white',
|
||||
'bg-accent dark:bg-accent',
|
||||
'hover:bg-accent/90 dark:hover:bg-accent/90',
|
||||
'disabled:bg-accent/50 disabled:text-white/70',
|
||||
'disabled:dark:bg-accent/30 disabled:dark:text-white/50',
|
||||
'text-text',
|
||||
'bg-accent',
|
||||
'hover:bg-accent/90',
|
||||
'disabled:bg-accent/50 disabled:text-text/70',
|
||||
],
|
||||
secondary: [
|
||||
'border border-gray-200 dark:border-gray-700',
|
||||
'text-gray-700 dark:text-gray-200',
|
||||
'bg-gray-50 dark:bg-gray-800',
|
||||
'hover:bg-gray-100 dark:hover:bg-gray-750',
|
||||
'disabled:bg-gray-50 disabled:text-gray-400',
|
||||
'disabled:dark:bg-gray-800 disabled:dark:text-gray-500',
|
||||
'border border-fill-tertiary dark:border-fill-tertiary',
|
||||
'text-text',
|
||||
'bg-fill-tertiary',
|
||||
'hover:bg-fill-tertiary/10',
|
||||
'disabled:bg-fill-tertiary/10',
|
||||
'disabled:dark:bg-fill-tertiary/10',
|
||||
],
|
||||
light: [
|
||||
'shadow-none',
|
||||
|
||||
@@ -162,8 +162,8 @@ const SelectItem = ({
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'cursor-menu focus:bg-theme-selection-active focus:text-theme-selection-foreground relative flex items-center rounded px-2.5 py-1 outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'data-[highlighted]:bg-theme-selection-hover focus-within:outline-transparent',
|
||||
'cursor-menu data-[highlighted]:text-accent-foreground relative flex items-center rounded px-2.5 py-1 outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'data-[highlighted]:bg-accent/20 focus-within:outline-transparent',
|
||||
'h-[28px] w-full',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
|
||||
@@ -2,4 +2,5 @@ export * from './backoff'
|
||||
export * from './cn'
|
||||
export * from './semaphore'
|
||||
export * from './spring'
|
||||
export * from './storage-provider'
|
||||
export * from './u8array'
|
||||
|
||||
2
packages/utils/src/storage-provider.ts
Normal file
2
packages/utils/src/storage-provider.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const STORAGE_PROVIDER_SECRET_PLACEHOLDER =
|
||||
'__afilmory_secret__preserved__'
|
||||
83
pnpm-lock.yaml
generated
83
pnpm-lock.yaml
generated
@@ -98,7 +98,7 @@ importers:
|
||||
version: 9.38.0(jiti@2.6.1)
|
||||
eslint-config-hyoban:
|
||||
specifier: 4.0.10
|
||||
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1))(tailwindcss@4.1.16)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1))(tailwindcss@4.1.16)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
fast-glob:
|
||||
specifier: 3.3.3
|
||||
version: 3.3.3
|
||||
@@ -592,6 +592,9 @@ importers:
|
||||
'@afilmory/task-queue':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/task-queue
|
||||
'@afilmory/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../../packages/utils
|
||||
'@aws-sdk/client-s3':
|
||||
specifier: 3.916.0
|
||||
version: 3.916.0
|
||||
@@ -600,7 +603,7 @@ importers:
|
||||
version: 1.19.5(hono@4.10.2)
|
||||
better-auth:
|
||||
specifier: 1.3.29
|
||||
version: 1.3.29(next@16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
version: 1.3.29(next@16.0.0(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
drizzle-orm:
|
||||
specifier: ^0.44.7
|
||||
version: 0.44.7(@types/pg@8.15.5)(@vercel/postgres@0.10.0)(kysely@0.28.8)(pg@8.16.3)(postgres@3.4.7)
|
||||
@@ -703,7 +706,7 @@ importers:
|
||||
version: 5.90.5(react@19.2.0)
|
||||
better-auth:
|
||||
specifier: 1.3.29
|
||||
version: 1.3.29(next@16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
version: 1.3.29(next@16.0.0(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
class-variance-authority:
|
||||
specifier: 0.7.1
|
||||
version: 0.7.1
|
||||
@@ -721,13 +724,16 @@ importers:
|
||||
version: 10.1.3
|
||||
jotai:
|
||||
specifier: 2.15.0
|
||||
version: 2.15.0(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0)
|
||||
version: 2.15.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0)
|
||||
lucide-react:
|
||||
specifier: 0.547.0
|
||||
version: 0.547.0(react@19.2.0)
|
||||
motion:
|
||||
specifier: 12.23.24
|
||||
version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
nanoid:
|
||||
specifier: 5.1.6
|
||||
version: 5.1.6
|
||||
ofetch:
|
||||
specifier: 1.4.1
|
||||
version: 1.4.1
|
||||
@@ -745,7 +751,7 @@ importers:
|
||||
version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-scan:
|
||||
specifier: 0.4.3
|
||||
version: 0.4.3(@types/react@19.2.2)(next@16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.52.5)
|
||||
version: 0.4.3(@types/react@19.2.2)(next@16.0.0(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.52.5)
|
||||
sonner:
|
||||
specifier: 2.0.7
|
||||
version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -800,7 +806,7 @@ importers:
|
||||
version: 9.38.0(jiti@2.6.1)
|
||||
eslint-config-hyoban:
|
||||
specifier: 4.0.10
|
||||
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1))(tailwindcss@4.1.16)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.38.0(jiti@2.6.1))(tailwindcss@4.1.16)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
lint-staged:
|
||||
specifier: 16.2.6
|
||||
version: 16.2.6
|
||||
@@ -1317,7 +1323,7 @@ importers:
|
||||
version: 0.15.9(typescript@5.9.3)
|
||||
unplugin-dts:
|
||||
specifier: 1.0.0-beta.6
|
||||
version: 1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.9.1))(esbuild@0.25.11)(rollup@4.52.5)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))
|
||||
version: 1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.9.1))(esbuild@0.25.11)(rolldown@1.0.0-beta.44)(rollup@4.52.5)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))
|
||||
vite:
|
||||
specifier: 7.1.12
|
||||
version: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
|
||||
@@ -8815,6 +8821,11 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@5.1.6:
|
||||
resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
|
||||
nanostores@1.0.1:
|
||||
resolution: {integrity: sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==}
|
||||
engines: {node: ^20.0.0 || >=22.0.0}
|
||||
@@ -17344,7 +17355,7 @@ snapshots:
|
||||
|
||||
batch-cluster@15.0.1: {}
|
||||
|
||||
better-auth@1.3.29(next@16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
better-auth@1.3.29(next@16.0.0(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@better-auth/core': 1.3.29(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)
|
||||
'@better-auth/telemetry': 1.3.29(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)
|
||||
@@ -17361,7 +17372,7 @@ snapshots:
|
||||
nanostores: 1.0.1
|
||||
zod: 4.1.12
|
||||
optionalDependencies:
|
||||
next: 16.0.0(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next: 16.0.0(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
@@ -19830,6 +19841,13 @@ snapshots:
|
||||
|
||||
jose@6.1.0: {}
|
||||
|
||||
jotai@2.15.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0):
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/template': 7.27.2
|
||||
'@types/react': 19.2.2
|
||||
react: 19.2.0
|
||||
|
||||
jotai@2.15.0(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0):
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.5
|
||||
@@ -20844,6 +20862,8 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@5.1.6: {}
|
||||
|
||||
nanostores@1.0.1: {}
|
||||
|
||||
napi-postinstall@0.3.3: {}
|
||||
@@ -20876,6 +20896,30 @@ snapshots:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
next@16.0.0(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@next/env': 16.0.0
|
||||
'@swc/helpers': 0.5.15
|
||||
caniuse-lite: 1.0.30001751
|
||||
postcss: 8.4.31
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
styled-jsx: 5.1.6(@babel/core@7.28.4)(react@19.2.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.0.0
|
||||
'@next/swc-darwin-x64': 16.0.0
|
||||
'@next/swc-linux-arm64-gnu': 16.0.0
|
||||
'@next/swc-linux-arm64-musl': 16.0.0
|
||||
'@next/swc-linux-x64-gnu': 16.0.0
|
||||
'@next/swc-linux-x64-musl': 16.0.0
|
||||
'@next/swc-win32-arm64-msvc': 16.0.0
|
||||
'@next/swc-win32-x64-msvc': 16.0.0
|
||||
sharp: 0.34.4
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
optional: true
|
||||
|
||||
next@16.0.0(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@next/env': 16.0.0
|
||||
@@ -22001,7 +22045,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
react-scan@0.4.3(@types/react@19.2.2)(next@16.0.0(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2):
|
||||
react-scan@0.4.3(@types/react@19.2.2)(next@16.0.0(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.52.5):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/generator': 7.28.3
|
||||
@@ -22010,7 +22054,7 @@ snapshots:
|
||||
'@clack/prompts': 0.8.2
|
||||
'@pivanov/utils': 0.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@preact/signals': 1.3.2(preact@10.27.2)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@2.79.2)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
|
||||
'@types/node': 20.19.23
|
||||
bippy: 0.3.27(@types/react@19.2.2)(react@19.2.0)
|
||||
esbuild: 0.25.11
|
||||
@@ -22023,7 +22067,7 @@ snapshots:
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
tsx: 4.20.6
|
||||
optionalDependencies:
|
||||
next: 16.0.0(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next: 16.0.0(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-router: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-router-dom: 6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
unplugin: 2.1.0
|
||||
@@ -22032,7 +22076,7 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
react-scan@0.4.3(@types/react@19.2.2)(next@16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.52.5):
|
||||
react-scan@0.4.3(@types/react@19.2.2)(next@16.0.0(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/generator': 7.28.3
|
||||
@@ -22041,7 +22085,7 @@ snapshots:
|
||||
'@clack/prompts': 0.8.2
|
||||
'@pivanov/utils': 0.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@preact/signals': 1.3.2(preact@10.27.2)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@2.79.2)
|
||||
'@types/node': 20.19.23
|
||||
bippy: 0.3.27(@types/react@19.2.2)(react@19.2.0)
|
||||
esbuild: 0.25.11
|
||||
@@ -22900,6 +22944,14 @@ snapshots:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.4
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.28.4)(react@19.2.0):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.0
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.4
|
||||
optional: true
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.0):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
@@ -23338,7 +23390,7 @@ snapshots:
|
||||
magic-string-ast: 1.0.3
|
||||
unplugin: 2.3.10
|
||||
|
||||
unplugin-dts@1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.9.1))(esbuild@0.25.11)(rollup@4.52.5)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)):
|
||||
unplugin-dts@1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.9.1))(esbuild@0.25.11)(rolldown@1.0.0-beta.44)(rollup@4.52.5)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
|
||||
'@volar/typescript': 2.4.23
|
||||
@@ -23352,6 +23404,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@microsoft/api-extractor': 7.52.13(@types/node@24.9.1)
|
||||
esbuild: 0.25.11
|
||||
rolldown: 1.0.0-beta.44
|
||||
rollup: 4.52.5
|
||||
vite: 7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
|
||||
transitivePeerDependencies:
|
||||
|
||||
Reference in New Issue
Block a user