feat(dashboard): enhance App and PhotoSyncActions components with new hooks and functionality

- Integrated useRequireStorageProvider in AppLayer to manage storage provider requirements.
- Added auto-run functionality in PhotoSyncActions to trigger sync based on user settings.
- Updated useStorageProvidersQuery to accept options for enabling/disabling queries.
- Enhanced StorageProvidersManager to prompt users for immediate photo sync after configuration.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-11-17 21:59:03 +08:00
parent 53abfc598d
commit 739b1e48b0
6 changed files with 128 additions and 7 deletions

View File

@@ -1,10 +1,11 @@
import type {FC} from 'react';
import type { FC } from 'react'
import { useEffect } from 'react'
import { Outlet, useLocation, useNavigate } from 'react-router'
import { useAccessDeniedValue } from '~/atoms/access-denied'
import { ROUTE_PATHS } from '~/constants/routes'
import { usePageRedirect } from '~/hooks/usePageRedirect'
import { useRequireStorageProvider } from '~/hooks/useRequireStorageProvider'
import { useRoutePermission } from '~/hooks/useRoutePermission'
import { RootProviders } from './providers/root-providers'
@@ -23,6 +24,10 @@ function AppLayer() {
session: pageRedirect.sessionQuery.data ?? null,
isLoading: pageRedirect.sessionQuery.isPending,
})
useRequireStorageProvider({
session: pageRedirect.sessionQuery.data ?? null,
isLoading: pageRedirect.sessionQuery.isPending,
})
useAccessDeniedRedirect()
const appIsReady = true

View File

@@ -0,0 +1,16 @@
import { atom } from 'jotai'
import { createAtomHooks } from '~/lib/jotai'
export type PhotoSyncAutoRunMode = 'dry-run' | 'apply' | null
const basePhotoSyncAutoRunAtom = atom<PhotoSyncAutoRunMode>(null)
export const [
photoSyncAutoRunAtom,
usePhotoSyncAutoRun,
usePhotoSyncAutoRunValue,
useSetPhotoSyncAutoRun,
getPhotoSyncAutoRun,
setPhotoSyncAutoRun,
] = createAtomHooks(basePhotoSyncAutoRunAtom)

View File

@@ -0,0 +1,47 @@
import { useEffect } from 'react'
import { useLocation, useNavigate } from 'react-router'
import { PUBLIC_ROUTES } from '~/constants/routes'
import type { SessionResponse } from '~/modules/auth/api/session'
import { useStorageProvidersQuery } from '~/modules/storage-providers'
const STORAGE_SETUP_PATH = '/photos/storage'
type UseRequireStorageProviderArgs = {
session: SessionResponse | null
isLoading: boolean
}
export function useRequireStorageProvider({ session, isLoading }: UseRequireStorageProviderArgs) {
const location = useLocation()
const navigate = useNavigate()
const pathname = location.pathname || '/'
const shouldCheck =
!isLoading &&
!!session &&
session.user.role !== 'superadmin' &&
!PUBLIC_ROUTES.has(pathname) &&
!pathname.startsWith('/superadmin')
const storageProvidersQuery = useStorageProvidersQuery({
enabled: shouldCheck,
})
const needsSetup =
shouldCheck &&
storageProvidersQuery.isSuccess &&
(storageProvidersQuery.data?.providers.length ?? 0) === 0 &&
!storageProvidersQuery.isFetching
useEffect(() => {
if (!needsSetup) {
return
}
if (pathname === STORAGE_SETUP_PATH) {
return
}
navigate(STORAGE_SETUP_PATH, { replace: true })
}, [navigate, needsSetup, pathname])
}

View File

@@ -3,6 +3,7 @@ import { useMutation } from '@tanstack/react-query'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { usePhotoSyncAutoRunValue, useSetPhotoSyncAutoRun } from '~/atoms/photo-sync'
import { useMainPageLayout } from '~/components/layouts/MainPageLayout'
import { getRequestErrorMessage } from '~/lib/errors'
@@ -15,6 +16,8 @@ export function PhotoSyncActions() {
const { setHeaderActionState } = useMainPageLayout()
const [pendingMode, setPendingMode] = useState<'dry-run' | 'apply' | null>(null)
const abortRef = useRef<AbortController | null>(null)
const autoRunMode = usePhotoSyncAutoRunValue()
const setAutoRunMode = useSetPhotoSyncAutoRun()
const mutation = useMutation({
mutationFn: async (variables: RunPhotoSyncPayload) => {
@@ -60,7 +63,7 @@ export function PhotoSyncActions() {
},
})
const { isPending } = mutation
const { isPending, mutate } = mutation
useEffect(() => {
return () => {
@@ -69,6 +72,17 @@ export function PhotoSyncActions() {
}
}, [setHeaderActionState])
useEffect(() => {
if (!autoRunMode) {
return
}
if (isPending) {
return
}
mutate({ dryRun: autoRunMode === 'dry-run' })
setAutoRunMode(null)
}, [autoRunMode, isPending, mutate, setAutoRunMode])
const handleSync = (dryRun: boolean) => {
mutation.mutate({ dryRun })
}

View File

@@ -1,9 +1,11 @@
import { Button, Modal } from '@afilmory/ui'
import { Button, Modal, Prompt } from '@afilmory/ui'
import { Spring } from '@afilmory/utils'
import { DynamicIcon } from 'lucide-react/dynamic'
import { m } from 'motion/react'
import { startTransition, useEffect, useState } from 'react'
import { startTransition, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router'
import { useSetPhotoSyncAutoRun } from '~/atoms/photo-sync'
import { LinearBorderPanel } from '~/components/common/GlassPanel'
import { MainPageLayout, useMainPageLayout } from '~/components/layouts/MainPageLayout'
import { useBlock } from '~/hooks/useBlock'
@@ -18,10 +20,14 @@ export function StorageProvidersManager() {
const { data, isLoading, isError, error } = useStorageProvidersQuery()
const updateMutation = useUpdateStorageProvidersMutation()
const { setHeaderActionState } = useMainPageLayout()
const navigate = useNavigate()
const setPhotoSyncAutoRun = useSetPhotoSyncAutoRun()
const [providers, setProviders] = useState<StorageProvider[]>([])
const [activeProviderId, setActiveProviderId] = useState<string | null>(null)
const [isDirty, setIsDirty] = useState(false)
const initialProviderStateRef = useRef<boolean | null>(null)
const hasShownSyncPromptRef = useRef(false)
useBlock({
when: isDirty,
@@ -46,6 +52,15 @@ export function StorageProvidersManager() {
})
}, [data])
useEffect(() => {
if (!data) {
return
}
if (initialProviderStateRef.current === null) {
initialProviderStateRef.current = data.providers.length > 0
}
}, [data])
const orderedProviders = reorderProvidersByActive(providers, activeProviderId)
const markDirty = () => setIsDirty(true)
@@ -104,6 +119,22 @@ export function StorageProvidersManager() {
{
onSuccess: () => {
setIsDirty(false)
const hadProvidersInitially =
initialProviderStateRef.current ?? ((data?.providers.length ?? 0) > 0 ? true : false)
if (!hadProvidersInitially && providers.length > 0 && !hasShownSyncPromptRef.current) {
initialProviderStateRef.current = true
hasShownSyncPromptRef.current = true
Prompt.prompt({
title: '配置完成,立即同步照片?',
description: '存储提供商配置已经保存,是否前往「数据同步」页面立即开始扫描存储中的照片并写入数据库?',
onConfirmText: '开始同步',
onCancelText: '稍后再说',
onConfirm: () => {
setPhotoSyncAutoRun('apply')
navigate('/photos/sync')
},
})
}
},
},
)

View File

@@ -12,7 +12,7 @@ import {
export const STORAGE_PROVIDERS_QUERY_KEY = ['settings', 'storage-providers'] as const
export function useStorageProvidersQuery() {
export function useStorageProvidersQuery(options?: { enabled?: boolean }) {
return useQuery({
queryKey: STORAGE_PROVIDERS_QUERY_KEY,
queryFn: async () => {
@@ -29,6 +29,7 @@ export function useStorageProvidersQuery() {
activeProviderId: ensureActiveProviderId(providers, activeProviderId),
}
},
enabled: options?.enabled ?? true,
})
}
@@ -44,6 +45,7 @@ export function useUpdateStorageProvidersMutation() {
}>(STORAGE_PROVIDERS_QUERY_KEY)?.providers
const resolvedProviders = restoreProviderSecrets(currentProviders, previousProviders ?? [])
const resolvedActiveId = ensureActiveProviderId(resolvedProviders, payload.activeProviderId ?? null)
await updateStorageSettings([
{
@@ -52,11 +54,17 @@ export function useUpdateStorageProvidersMutation() {
},
{
key: STORAGE_SETTING_KEYS.activeProvider,
value: payload.activeProviderId ?? '',
value: resolvedActiveId ?? '',
},
])
return {
providers: resolvedProviders,
activeProviderId: resolvedActiveId,
}
},
onSuccess: () => {
onSuccess: (data) => {
queryClient.setQueryData(STORAGE_PROVIDERS_QUERY_KEY, data)
void queryClient.invalidateQueries({
queryKey: STORAGE_PROVIDERS_QUERY_KEY,
})