mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
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:
@@ -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
|
||||
|
||||
16
be/apps/dashboard/src/atoms/photo-sync.ts
Normal file
16
be/apps/dashboard/src/atoms/photo-sync.ts
Normal 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)
|
||||
47
be/apps/dashboard/src/hooks/useRequireStorageProvider.ts
Normal file
47
be/apps/dashboard/src/hooks/useRequireStorageProvider.ts
Normal 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])
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user