mirror of
https://github.com/Afilmory/afilmory
synced 2026-04-24 23:05:05 +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 { useEffect } from 'react'
|
||||||
import { Outlet, useLocation, useNavigate } from 'react-router'
|
import { Outlet, useLocation, useNavigate } from 'react-router'
|
||||||
|
|
||||||
import { useAccessDeniedValue } from '~/atoms/access-denied'
|
import { useAccessDeniedValue } from '~/atoms/access-denied'
|
||||||
import { ROUTE_PATHS } from '~/constants/routes'
|
import { ROUTE_PATHS } from '~/constants/routes'
|
||||||
import { usePageRedirect } from '~/hooks/usePageRedirect'
|
import { usePageRedirect } from '~/hooks/usePageRedirect'
|
||||||
|
import { useRequireStorageProvider } from '~/hooks/useRequireStorageProvider'
|
||||||
import { useRoutePermission } from '~/hooks/useRoutePermission'
|
import { useRoutePermission } from '~/hooks/useRoutePermission'
|
||||||
|
|
||||||
import { RootProviders } from './providers/root-providers'
|
import { RootProviders } from './providers/root-providers'
|
||||||
@@ -23,6 +24,10 @@ function AppLayer() {
|
|||||||
session: pageRedirect.sessionQuery.data ?? null,
|
session: pageRedirect.sessionQuery.data ?? null,
|
||||||
isLoading: pageRedirect.sessionQuery.isPending,
|
isLoading: pageRedirect.sessionQuery.isPending,
|
||||||
})
|
})
|
||||||
|
useRequireStorageProvider({
|
||||||
|
session: pageRedirect.sessionQuery.data ?? null,
|
||||||
|
isLoading: pageRedirect.sessionQuery.isPending,
|
||||||
|
})
|
||||||
useAccessDeniedRedirect()
|
useAccessDeniedRedirect()
|
||||||
|
|
||||||
const appIsReady = true
|
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 { useEffect, useRef, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
import { usePhotoSyncAutoRunValue, useSetPhotoSyncAutoRun } from '~/atoms/photo-sync'
|
||||||
import { useMainPageLayout } from '~/components/layouts/MainPageLayout'
|
import { useMainPageLayout } from '~/components/layouts/MainPageLayout'
|
||||||
import { getRequestErrorMessage } from '~/lib/errors'
|
import { getRequestErrorMessage } from '~/lib/errors'
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ export function PhotoSyncActions() {
|
|||||||
const { setHeaderActionState } = useMainPageLayout()
|
const { setHeaderActionState } = useMainPageLayout()
|
||||||
const [pendingMode, setPendingMode] = useState<'dry-run' | 'apply' | null>(null)
|
const [pendingMode, setPendingMode] = useState<'dry-run' | 'apply' | null>(null)
|
||||||
const abortRef = useRef<AbortController | null>(null)
|
const abortRef = useRef<AbortController | null>(null)
|
||||||
|
const autoRunMode = usePhotoSyncAutoRunValue()
|
||||||
|
const setAutoRunMode = useSetPhotoSyncAutoRun()
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (variables: RunPhotoSyncPayload) => {
|
mutationFn: async (variables: RunPhotoSyncPayload) => {
|
||||||
@@ -60,7 +63,7 @@ export function PhotoSyncActions() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { isPending } = mutation
|
const { isPending, mutate } = mutation
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -69,6 +72,17 @@ export function PhotoSyncActions() {
|
|||||||
}
|
}
|
||||||
}, [setHeaderActionState])
|
}, [setHeaderActionState])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!autoRunMode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isPending) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mutate({ dryRun: autoRunMode === 'dry-run' })
|
||||||
|
setAutoRunMode(null)
|
||||||
|
}, [autoRunMode, isPending, mutate, setAutoRunMode])
|
||||||
|
|
||||||
const handleSync = (dryRun: boolean) => {
|
const handleSync = (dryRun: boolean) => {
|
||||||
mutation.mutate({ dryRun })
|
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 { Spring } from '@afilmory/utils'
|
||||||
import { DynamicIcon } from 'lucide-react/dynamic'
|
import { DynamicIcon } from 'lucide-react/dynamic'
|
||||||
import { m } from 'motion/react'
|
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 { LinearBorderPanel } from '~/components/common/GlassPanel'
|
||||||
import { MainPageLayout, useMainPageLayout } from '~/components/layouts/MainPageLayout'
|
import { MainPageLayout, useMainPageLayout } from '~/components/layouts/MainPageLayout'
|
||||||
import { useBlock } from '~/hooks/useBlock'
|
import { useBlock } from '~/hooks/useBlock'
|
||||||
@@ -18,10 +20,14 @@ export function StorageProvidersManager() {
|
|||||||
const { data, isLoading, isError, error } = useStorageProvidersQuery()
|
const { data, isLoading, isError, error } = useStorageProvidersQuery()
|
||||||
const updateMutation = useUpdateStorageProvidersMutation()
|
const updateMutation = useUpdateStorageProvidersMutation()
|
||||||
const { setHeaderActionState } = useMainPageLayout()
|
const { setHeaderActionState } = useMainPageLayout()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const setPhotoSyncAutoRun = useSetPhotoSyncAutoRun()
|
||||||
|
|
||||||
const [providers, setProviders] = useState<StorageProvider[]>([])
|
const [providers, setProviders] = useState<StorageProvider[]>([])
|
||||||
const [activeProviderId, setActiveProviderId] = useState<string | null>(null)
|
const [activeProviderId, setActiveProviderId] = useState<string | null>(null)
|
||||||
const [isDirty, setIsDirty] = useState(false)
|
const [isDirty, setIsDirty] = useState(false)
|
||||||
|
const initialProviderStateRef = useRef<boolean | null>(null)
|
||||||
|
const hasShownSyncPromptRef = useRef(false)
|
||||||
|
|
||||||
useBlock({
|
useBlock({
|
||||||
when: isDirty,
|
when: isDirty,
|
||||||
@@ -46,6 +52,15 @@ export function StorageProvidersManager() {
|
|||||||
})
|
})
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (initialProviderStateRef.current === null) {
|
||||||
|
initialProviderStateRef.current = data.providers.length > 0
|
||||||
|
}
|
||||||
|
}, [data])
|
||||||
|
|
||||||
const orderedProviders = reorderProvidersByActive(providers, activeProviderId)
|
const orderedProviders = reorderProvidersByActive(providers, activeProviderId)
|
||||||
|
|
||||||
const markDirty = () => setIsDirty(true)
|
const markDirty = () => setIsDirty(true)
|
||||||
@@ -104,6 +119,22 @@ export function StorageProvidersManager() {
|
|||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsDirty(false)
|
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 const STORAGE_PROVIDERS_QUERY_KEY = ['settings', 'storage-providers'] as const
|
||||||
|
|
||||||
export function useStorageProvidersQuery() {
|
export function useStorageProvidersQuery(options?: { enabled?: boolean }) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: STORAGE_PROVIDERS_QUERY_KEY,
|
queryKey: STORAGE_PROVIDERS_QUERY_KEY,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -29,6 +29,7 @@ export function useStorageProvidersQuery() {
|
|||||||
activeProviderId: ensureActiveProviderId(providers, activeProviderId),
|
activeProviderId: ensureActiveProviderId(providers, activeProviderId),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
enabled: options?.enabled ?? true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ export function useUpdateStorageProvidersMutation() {
|
|||||||
}>(STORAGE_PROVIDERS_QUERY_KEY)?.providers
|
}>(STORAGE_PROVIDERS_QUERY_KEY)?.providers
|
||||||
|
|
||||||
const resolvedProviders = restoreProviderSecrets(currentProviders, previousProviders ?? [])
|
const resolvedProviders = restoreProviderSecrets(currentProviders, previousProviders ?? [])
|
||||||
|
const resolvedActiveId = ensureActiveProviderId(resolvedProviders, payload.activeProviderId ?? null)
|
||||||
|
|
||||||
await updateStorageSettings([
|
await updateStorageSettings([
|
||||||
{
|
{
|
||||||
@@ -52,11 +54,17 @@ export function useUpdateStorageProvidersMutation() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: STORAGE_SETTING_KEYS.activeProvider,
|
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({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: STORAGE_PROVIDERS_QUERY_KEY,
|
queryKey: STORAGE_PROVIDERS_QUERY_KEY,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user