このコミットが含まれているのは:
2026-06-25 17:40:34 +09:00
コミット 377a09ed70
22個のファイルの変更679行の追加231行の削除
+2 -46
ファイルの表示
@@ -15,7 +15,6 @@ import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import {
suppressMaterialFile,
fetchMaterial,
updateMaterial,
} from '@/lib/materials'
@@ -25,12 +24,10 @@ import { useValidationErrors } from '@/lib/useValidationErrors'
import type { FC } from 'react'
import type { User } from '@/types'
type MaterialFormField = 'tag' | 'file' | 'url' | 'exportPaths'
const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
const MaterialDetailPage: FC = () => {
const { id } = useParams ()
const qc = useQueryClient ()
@@ -91,35 +88,11 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
toast ({ title: '更新失敗……', description: '入力を見直してください.' })
}})
const suppressMutation = useMutation ({
mutationFn: async (reason: string) =>
await suppressMaterialFile (id ?? '', { reason }),
onSuccess: async data => {
qc.setQueryData (materialsKeys.show (id ?? ''), data)
setFile (null)
setFilePreview ('')
await invalidateMaterialQueries ()
toast ({ title: '抑止しました' })
},
onError: () => {
toast ({ title: '抑止に失敗しました' })
}})
const handleSubmit = () => {
clearValidationErrors ()
updateMutation.mutate ()
}
const handleSuppress = () => {
const reason = window.prompt ('抑止理由を入力してください。')
if (reason == null || reason.trim () === '')
return
if (!window.confirm ('素材ファイルを抑止します。表示と ZIP export から除外されます。'))
return
suppressMutation.mutate (reason)
}
return (
<MainArea>
{material && (
@@ -145,16 +118,7 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
: materialTitle}
</PageTitle>
{material.fileSuppressedAt && (
<div className="mb-4 rounded border border-red-300 bg-red-50 p-3
text-red-900 dark:border-red-800 dark:bg-red-950
dark:text-red-100">
<span></span>
{material.fileSuppressionReason && (
<span> : {material.fileSuppressionReason}</span>)}
</div>)}
{(!material.fileSuppressedAt && material.file && material.contentType) && (
{(material.file && material.contentType) && (
(/image\/.*/.test (material.contentType) && (
<img src={material.file} alt={material.tag?.name || undefined}/>))
|| (/video\/.*/.test (material.contentType) && (
@@ -256,14 +220,6 @@ const MaterialDetailPage: FC<{ user: User | null }> = ({ user }) => {
disabled={updateMutation.isPending}>
</Button>
{user?.role === 'admin' && !material.fileSuppressedAt && (
<Button
type="button"
variant="destructive"
onClick={handleSuppress}
disabled={suppressMutation.isPending}>
</Button>)}
</div>
</div>
</Tab>
+14 -44
ファイルの表示
@@ -23,7 +23,6 @@ import type {
MaterialIndexDirection,
MaterialIndexMediaKind,
MaterialIndexSort,
MaterialIndexSuppression,
MaterialIndexTagState,
MaterialIndexView } from '@/types'
@@ -32,8 +31,7 @@ const MEDIA_KIND_LABELS: Record<Material['mediaKind'], string> = {
video: '動画',
audio: '音声',
file_other: 'その他ファイル',
url_only: 'URL のみ',
suppressed: '抑止済み'}
url_only: 'URL のみ'}
const MEDIA_FILTER_LABELS: Record<MaterialIndexMediaKind, string> = {
all: 'すべて',
@@ -84,14 +82,9 @@ const materialTitle = (material: Material): string =>
const MaterialThumb: FC<{ material: Material }> = ({ material }) => (
<div
className={`flex aspect-square h-[180px] w-[180px] items-center justify-center
overflow-hidden rounded-lg border text-center shadow-sm ${
material.fileSuppressedAt
? [
'border-red-300 bg-red-50 text-red-900 dark:border-red-800',
'dark:bg-red-950 dark:text-red-100'].join (' ')
: [
'border-stone-200 bg-white text-stone-900 dark:border-stone-700',
'dark:bg-stone-900 dark:text-stone-100'].join (' ') }`}>
overflow-hidden rounded-lg border border-stone-200 bg-white text-center
text-stone-900 shadow-sm dark:border-stone-700 dark:bg-stone-900
dark:text-stone-100`}>
{material.thumbnail
? <img src={material.thumbnail} alt="" className="h-full w-full object-contain"/>
: (
@@ -123,14 +116,8 @@ const MaterialCard: FC<{ material: Material }> = ({ material }) => (
const MaterialListItem: FC<{ material: Material }> = ({ material }) => (
<article
className={`rounded-lg border p-3 shadow-sm ${
material.fileSuppressedAt
? [
'border-red-200 bg-red-50 text-red-900 dark:border-red-900',
'dark:bg-red-950 dark:text-red-100'].join (' ')
: [
'border-stone-200 bg-white text-stone-900 dark:border-stone-700',
'dark:bg-stone-900 dark:text-stone-100'].join (' ')}`}>
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="flex gap-3">
<MaterialThumb material={material}/>
<div className="min-w-0 flex-1 space-y-2">
@@ -141,8 +128,6 @@ const MaterialListItem: FC<{ material: Material }> = ({ material }) => (
dark:text-sky-300">
{materialTitle (material)}
</PrefetchLink>
{material.fileSuppressedAt && (
<p className="mt-1 text-sm text-red-700 dark:text-red-200"></p>)}
</div>
<dl className="space-y-1 text-sm text-stone-600 dark:text-stone-300">
<div>
@@ -188,10 +173,6 @@ const MaterialListPage: FC = () => {
query.get ('media_kind'),
['all', 'image', 'video', 'audio', 'file_other', 'url_only'],
'all')
const suppression = parseOption<MaterialIndexSuppression> (
query.get ('suppression'),
['active', 'suppressed', 'all'],
'active')
const sort = parseOption<MaterialIndexSort> (
query.get ('sort'),
['created_at', 'updated_at', 'tag_name', 'media_kind', 'file_byte_size',
@@ -210,8 +191,6 @@ const MaterialListPage: FC = () => {
const [q, setQ] = useState ('')
const [tagStateInput, setTagStateInput] = useState<MaterialIndexTagState> ('all')
const [mediaKindInput, setMediaKindInput] = useState<MaterialIndexMediaKind> ('all')
const [suppressionInput, setSuppressionInput] =
useState<MaterialIndexSuppression> ('active')
const [createdFrom, setCreatedFrom] = useState<string | null> (null)
const [createdTo, setCreatedTo] = useState<string | null> (null)
const [updatedFrom, setUpdatedFrom] = useState<string | null> (null)
@@ -221,7 +200,6 @@ const MaterialListPage: FC = () => {
q: qQuery,
tagState,
mediaKind,
suppression,
createdFrom: createdFromQuery,
createdTo: createdToQuery,
updatedFrom: updatedFromQuery,
@@ -241,12 +219,11 @@ const MaterialListPage: FC = () => {
setQ (qQuery)
setTagStateInput (tagState)
setMediaKindInput (mediaKind)
setSuppressionInput (suppression)
setCreatedFrom (createdFromQuery)
setCreatedTo (createdToQuery)
setUpdatedFrom (updatedFromQuery)
setUpdatedTo (updatedToQuery)
}, [createdFromQuery, createdToQuery, mediaKind, qQuery, suppression, tagState,
}, [createdFromQuery, createdToQuery, mediaKind, qQuery, tagState,
updatedFromQuery, updatedToQuery])
const search = (e: FormEvent) => {
@@ -256,7 +233,6 @@ const MaterialListPage: FC = () => {
setIf (qs, 'q', q)
qs.set ('tag_state', tagStateInput)
qs.set ('media_kind', mediaKindInput)
qs.set ('suppression', suppressionInput)
setIf (qs, 'created_from', createdFrom)
setIf (qs, 'created_to', createdTo)
setIf (qs, 'updated_from', updatedFrom)
@@ -301,6 +277,13 @@ const MaterialListPage: FC = () => {
dark:bg-stone-900 dark:text-stone-100 dark:hover:bg-stone-800">
</PrefetchLink>
<PrefetchLink
to="/materials/suppressions"
className="rounded-full border border-stone-300 bg-white px-4 py-2 text-sm
text-stone-900 hover:bg-stone-100 dark:border-stone-700
dark:bg-stone-900 dark:text-stone-100 dark:hover:bg-stone-800">
</PrefetchLink>
<a
href={`${ API_BASE_URL }/materials/download.zip?profile=legacy_drive`}
target="_blank"
@@ -363,19 +346,6 @@ const MaterialListPage: FC = () => {
</select>)}
</FormField>
<FormField label="抑止状態">
{({ invalid }) => (
<select
value={suppressionInput}
onChange={e => setSuppressionInput (
e.target.value as MaterialIndexSuppression)}
className={inputClass (invalid)}>
<option value="active"></option>
<option value="suppressed"></option>
<option value="all"></option>
</select>)}
</FormField>
<FormField label="作成日時">
{() => (
<div className="flex flex-wrap items-center gap-2">
+194
ファイルの表示
@@ -0,0 +1,194 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { Helmet } from 'react-helmet-async'
import PrefetchLink from '@/components/PrefetchLink'
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 path',
google_drive_file: 'Google Drive file ID',
legacy_drive_path: 'Legacy Drive path'}
const REASONS = [
'copyright_high_risk',
'copyright_takedown',
'adult_or_sensitive',
'personal_information',
'malware_or_dangerous_file',
'duplicate_or_low_quality',
'source_owner_request',
'other']
const MaterialSyncSuppressionsPage: FC = () => {
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 (REASONS[0])
const { data, isError, isLoading } = useQuery ({
queryKey: materialsKeys.suppressions (),
queryFn: fetchMaterialSyncSuppressions})
const createMutation = useMutation ({
mutationFn: async () =>
await createMaterialSyncSuppression ({
sourceKind,
sourceUri,
drivePath,
driveFileId,
reason}),
onSuccess: async () => {
await qc.invalidateQueries ({ queryKey: materialsKeys.suppressions () })
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">
<div className="flex flex-wrap items-center justify-between gap-3">
<PageTitle></PageTitle>
<PrefetchLink
to="/materials"
className="text-sm text-sky-700 underline underline-offset-2 dark:text-sky-300">
</PrefetchLink>
</div>
<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="Source URI">
{({ invalid }) => (
<input
type="text"
value={sourceUri}
onChange={e => setSourceUri (e.target.value)}
className={inputClass (invalid)}/>)}
</FormField>
<FormField label="Drive path">
{({ invalid }) => (
<input
type="text"
value={drivePath}
onChange={e => setDrivePath (e.target.value)}
className={inputClass (invalid)}/>)}
</FormField>
<FormField label="Drive file 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)}
className={inputClass (invalid)}>
{REASONS.map (value => (
<option key={value} value={value}>
{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">
: {suppression.reason} / : {dateString (suppression.createdAt)}
</div>
</article>))}
</div>
</div>
</MainArea>)
}
export default MaterialSyncSuppressionsPage