ファイル
btrc-hub/frontend/src/pages/materials/MaterialSyncSuppressionsPage.tsx
T
2026-10-17 16:47:08 +09:00

205 行
6.4 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocale } from '@/lib/useLocale'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { createMaterialSyncSuppression, fetchMaterialSyncSuppressions } from '@/lib/materials'
import { materialsKeys } from '@/lib/queryKeys'
import { dateString, inputClass } from '@/lib/utils'
import type { FC, FormEvent } from 'react'
import type { MaterialSyncSuppressionSourceKind } from '@/types'
const SOURCE_KIND_LABELS: Record<MaterialSyncSuppressionSourceKind, string> = {
uri: 'URI',
google_drive_path: 'Google Drive ファイル',
google_drive_path_prefix: 'Google Drive フォルダ',
google_drive_file: 'Google Drive ファイル Id.',
legacy_drive_path: '汎用ファイル',
legacy_drive_path_prefix: '汎用フォルダ' }
const REASONS = [
'copyright_high_risk',
'copyright_takedown',
'adult_or_sensitive',
'personal_information',
'malware_or_dangerous_file',
'duplicate_or_low_quality',
'source_owner_request',
'other'] as const
type MaterialSyncSuppressionReason = typeof REASONS[number]
const REASON_NAMES: Record<MaterialSyncSuppressionReason, string> = {
['copyright_high_risk']: '著作権への懸念',
['copyright_takedown']: '著作者からの申出',
['adult_or_sensitive']: '成人向け',
['personal_information']: '個人情報',
['malware_or_dangerous_file']: '危険なソフトウェア',
['duplicate_or_low_quality']: '重複',
['source_owner_request']: '同期元管理者からの申出',
['other']: 'その他' } as const
const reasonName = (reason: string): string =>
REASON_NAMES[reason as MaterialSyncSuppressionReason] ?? reason
const MaterialSyncSuppressionsPage: FC = () => {
const locale = useLocale ()
const qc = useQueryClient ()
const [sourceKind, setSourceKind] =
useState<MaterialSyncSuppressionSourceKind> ('uri')
const [sourceUri, setSourceUri] = useState ('')
const [drivePath, setDrivePath] = useState ('')
const [driveFileId, setDriveFileId] = useState ('')
const [reason, setReason] = useState<MaterialSyncSuppressionReason> (REASONS[0])
const { data, isError, isLoading } = useQuery ({
queryKey: materialsKeys.suppressions (locale),
queryFn: () => fetchMaterialSyncSuppressions (locale)})
const createMutation = useMutation ({
mutationFn: async () =>
await createMaterialSyncSuppression ({
sourceKind,
sourceUri,
drivePath,
driveFileId,
reason}, locale),
onSuccess: async () => {
await qc.invalidateQueries ({ queryKey: materialsKeys.suppressions (locale) })
await qc.invalidateQueries ({ queryKey: materialsKeys.root })
setSourceUri ('')
setDrivePath ('')
setDriveFileId ('')
toast ({ title: '同期元抑止を登録しました' })
},
onError: () => {
toast ({ title: '同期元抑止の登録に失敗しました' })
}})
const handleSubmit = (event: FormEvent) => {
event.preventDefault ()
createMutation.mutate ()
}
const suppressions = data?.suppressions ?? []
return (
<MainArea>
<Helmet>
<title>{`素材同期抑止 | ${ SITE_TITLE }`}</title>
</Helmet>
<div className="space-y-5">
<PageTitle>同期抑止</PageTitle>
<form
onSubmit={handleSubmit}
className="max-w-3xl rounded-lg border border-stone-200 bg-white p-4
text-stone-900 dark:border-stone-700 dark:bg-stone-900
dark:text-stone-100">
<div className="space-y-3">
<FormField label="同期元種別">
{({ invalid }) => (
<select
value={sourceKind}
onChange={e => setSourceKind (
e.target.value as MaterialSyncSuppressionSourceKind)}
className={inputClass (invalid)}>
{Object.entries (SOURCE_KIND_LABELS).map (([value, label]) => (
<option key={value} value={value}>
{label}
</option>))}
</select>)}
</FormField>
<FormField label="同期元 URI">
{({ invalid }) => (
<input
type="text"
value={sourceUri}
onChange={e => setSourceUri (e.target.value)}
className={inputClass (invalid)}/>)}
</FormField>
<FormField label="Google Drive パス">
{({ invalid }) => (
<input
type="text"
value={drivePath}
onChange={e => setDrivePath (e.target.value)}
className={inputClass (invalid)}/>)}
</FormField>
<FormField label="Google Drive ファイル Id.">
{({ invalid }) => (
<input
type="text"
value={driveFileId}
onChange={e => setDriveFileId (e.target.value)}
className={inputClass (invalid)}/>)}
</FormField>
<FormField label="事由">
{({ invalid }) => (
<select
value={reason}
onChange={e => setReason (e.target.value as MaterialSyncSuppressionReason)}
className={inputClass (invalid)}>
{REASONS.map (value => (
<option key={value} value={value}>
{REASON_NAMES[value]}
</option>))}
</select>)}
</FormField>
</div>
<Button
type="submit"
className="mt-4 rounded bg-blue-600 px-4 py-2 text-white
disabled:bg-gray-400"
disabled={createMutation.isPending}>
登録
</Button>
</form>
{isLoading && <p>Loading...</p>}
{isError && (
<p className="text-red-600 dark:text-red-300">
同期元抑止一覧の取得に失敗しました.
</p>)}
<div className="space-y-3">
{suppressions.map (suppression => (
<article
key={suppression.id}
className="rounded-lg border border-stone-200 bg-white p-3
text-stone-900 shadow-sm dark:border-stone-700
dark:bg-stone-900 dark:text-stone-100">
<div className="font-medium">
{SOURCE_KIND_LABELS[suppression.sourceKind]}
</div>
<div className="break-all text-sm text-stone-700 dark:text-stone-300">
{suppression.normalizedSourceKey}
</div>
<div className="mt-2 text-sm text-stone-600 dark:text-stone-400">
事由: {reasonName (suppression.reason)} /
登録: {dateString (suppression.createdAt)}
</div>
</article>))}
</div>
</div>
</MainArea>)
}
export default MaterialSyncSuppressionsPage