このコミットが含まれているのは:
+3
-1
@@ -16,6 +16,7 @@ import MaterialBasePage from '@/pages/materials/MaterialBasePage'
|
||||
import MaterialDetailPage from '@/pages/materials/MaterialDetailPage'
|
||||
import MaterialListPage from '@/pages/materials/MaterialListPage'
|
||||
import MaterialNewPage from '@/pages/materials/MaterialNewPage'
|
||||
import MaterialSyncSuppressionsPage from '@/pages/materials/MaterialSyncSuppressionsPage'
|
||||
// import MaterialSearchPage from '@/pages/materials/MaterialSearchPage'
|
||||
import MorePage from '@/pages/MorePage'
|
||||
import GekanatorPage from '@/pages/GekanatorPage'
|
||||
@@ -69,7 +70,8 @@ const RouteTransitionWrapper = ({ user, setUser }: {
|
||||
<Route path="/materials" element={<MaterialBasePage/>}>
|
||||
<Route index element={<MaterialListPage/>}/>
|
||||
<Route path="new" element={<MaterialNewPage/>}/>
|
||||
<Route path=":id" element ={<MaterialDetailPage user={user}/>}/>
|
||||
<Route path="suppressions" element={<MaterialSyncSuppressionsPage/>}/>
|
||||
<Route path=":id" element ={<MaterialDetailPage/>}/>
|
||||
</Route>
|
||||
{/* <Route path="/materials/search" element={<MaterialSearchPage/>}/> */}
|
||||
<Route path="/wiki" element={<WikiSearchPage/>}/>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
apiGet,
|
||||
isApiError,
|
||||
apiPatch,
|
||||
apiPost,
|
||||
apiPut,
|
||||
} from '@/lib/api'
|
||||
@@ -10,6 +9,7 @@ import type {
|
||||
Material,
|
||||
FetchMaterialsParams,
|
||||
MaterialFilter,
|
||||
MaterialSyncSuppression,
|
||||
MaterialSidebarTag,
|
||||
MaterialTagTree,
|
||||
} from '@/types'
|
||||
@@ -24,6 +24,10 @@ export type MaterialIndexResponse = {
|
||||
count: number
|
||||
}
|
||||
|
||||
export type MaterialSyncSuppressionResponse = {
|
||||
suppressions: MaterialSyncSuppression[]
|
||||
}
|
||||
|
||||
const MATERIAL_FILTERS: MaterialFilter[] = ['present', 'missing', 'any']
|
||||
|
||||
|
||||
@@ -36,13 +40,13 @@ export const parseMaterialFilter = (
|
||||
|
||||
|
||||
export const fetchMaterials = async (
|
||||
{ q, tagState, mediaKind, suppression, createdFrom, createdTo,
|
||||
updatedFrom, updatedTo, sort, direction, page, limit }: FetchMaterialsParams): Promise<MaterialIndexResponse> =>
|
||||
{ q, tagState, mediaKind, createdFrom, createdTo,
|
||||
updatedFrom, updatedTo, sort, direction, page,
|
||||
limit }: FetchMaterialsParams): Promise<MaterialIndexResponse> =>
|
||||
await apiGet ('/materials', { params: {
|
||||
...(q && { q }),
|
||||
tag_state: tagState,
|
||||
media_kind: mediaKind,
|
||||
suppression,
|
||||
...(createdFrom && { created_from: createdFrom }),
|
||||
...(createdTo && { created_to: createdTo }),
|
||||
...(updatedFrom && { updated_from: updatedFrom }),
|
||||
@@ -101,7 +105,22 @@ export const updateMaterial = async (
|
||||
await apiPut (`/materials/${ id }`, formData)
|
||||
|
||||
|
||||
export const suppressMaterialFile = async (
|
||||
id: string,
|
||||
payload: { reason: string; purge?: boolean }): Promise<Material> =>
|
||||
await apiPatch (`/materials/${ id }/suppress_file`, payload)
|
||||
export const fetchMaterialSyncSuppressions =
|
||||
async (): Promise<MaterialSyncSuppressionResponse> =>
|
||||
await apiGet ('/materials/suppressions')
|
||||
|
||||
|
||||
export const createMaterialSyncSuppression = async (
|
||||
payload: {
|
||||
sourceKind: string
|
||||
sourceUri?: string
|
||||
drivePath?: string
|
||||
driveFileId?: string
|
||||
reason: string
|
||||
}): Promise<MaterialSyncSuppression> =>
|
||||
await apiPost ('/materials/suppressions', {
|
||||
source_kind: payload.sourceKind,
|
||||
source_uri: payload.sourceUri,
|
||||
drive_path: payload.drivePath,
|
||||
drive_file_id: payload.driveFileId,
|
||||
reason: payload.reason})
|
||||
|
||||
@@ -37,6 +37,7 @@ export const materialsKeys = {
|
||||
byTagName: (name: string, materialFilter: MaterialFilter) =>
|
||||
['materials', 'tag', name, materialFilter] as const,
|
||||
show: (id: string) => ['materials', id] as const,
|
||||
suppressions: () => ['materials', 'suppressions'] as const,
|
||||
tree: (p: {
|
||||
parentId?: number | null
|
||||
materialFilter: MaterialFilter
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
@@ -76,6 +76,11 @@ export const buildMaterial = (overrides: Partial<Material> = {}): Material => ({
|
||||
tag: buildTag (),
|
||||
file: null,
|
||||
url: null,
|
||||
sourceKind: null,
|
||||
sourceUri: null,
|
||||
sourcePath: null,
|
||||
sourceFileId: null,
|
||||
normalizedSourceKey: null,
|
||||
wikiPageBody: null,
|
||||
thumbnail: null,
|
||||
thumbnailFallbackText: 'テストタグ',
|
||||
@@ -83,8 +88,6 @@ export const buildMaterial = (overrides: Partial<Material> = {}): Material => ({
|
||||
mediaKind: 'url_only',
|
||||
contentType: null,
|
||||
fileByteSize: null,
|
||||
fileSuppressedAt: null,
|
||||
fileSuppressionReason: null,
|
||||
exportPaths: {},
|
||||
exportItems: [],
|
||||
createdAt: '2026-01-02T03:04:05.000Z',
|
||||
|
||||
+24
-6
@@ -86,8 +86,6 @@ export type MaterialIndexSort =
|
||||
| 'version_no'
|
||||
| 'id'
|
||||
|
||||
export type MaterialIndexSuppression = 'active' | 'suppressed' | 'all'
|
||||
|
||||
export type MaterialIndexTagState = 'all' | 'tagged' | 'untagged'
|
||||
|
||||
export type MaterialIndexView = 'card' | 'list'
|
||||
@@ -96,7 +94,6 @@ export type FetchMaterialsParams = {
|
||||
q: string
|
||||
tagState: MaterialIndexTagState
|
||||
mediaKind: MaterialIndexMediaKind
|
||||
suppression: MaterialIndexSuppression
|
||||
createdFrom: string
|
||||
createdTo: string
|
||||
updatedFrom: string
|
||||
@@ -113,15 +110,18 @@ export type Material = {
|
||||
tag: Tag | null
|
||||
file: string | null
|
||||
url: string | null
|
||||
sourceKind: string | null
|
||||
sourceUri: string | null
|
||||
sourcePath: string | null
|
||||
sourceFileId: string | null
|
||||
normalizedSourceKey: string | null
|
||||
wikiPageBody?: string | null
|
||||
thumbnail: string | null
|
||||
thumbnailFallbackText: string | null
|
||||
thumbnailFallbackKind: 'tag_name' | 'created_at'
|
||||
mediaKind: 'image' | 'video' | 'audio' | 'file_other' | 'url_only' | 'suppressed'
|
||||
mediaKind: 'image' | 'video' | 'audio' | 'file_other' | 'url_only'
|
||||
contentType: string | null
|
||||
fileByteSize: number | null
|
||||
fileSuppressedAt: string | null
|
||||
fileSuppressionReason: string | null
|
||||
exportPaths: Record<string, string>
|
||||
exportItems: MaterialExportItem[]
|
||||
createdAt: string
|
||||
@@ -129,6 +129,24 @@ export type Material = {
|
||||
updatedAt: string
|
||||
updatedByUser: { id: number; name: string } }
|
||||
|
||||
export type MaterialSyncSuppressionSourceKind =
|
||||
| 'uri'
|
||||
| 'google_drive_path'
|
||||
| 'google_drive_file'
|
||||
| 'legacy_drive_path'
|
||||
|
||||
export type MaterialSyncSuppression = {
|
||||
id: number
|
||||
sourceKind: MaterialSyncSuppressionSourceKind
|
||||
sourceUri: string | null
|
||||
drivePath: string | null
|
||||
driveFileId: string | null
|
||||
normalizedSourceKey: string
|
||||
reason: string
|
||||
createdByUser: { id: number; name: string } | null
|
||||
createdAt: string
|
||||
updatedAt: string }
|
||||
|
||||
export type MaterialSidebarTag = {
|
||||
id: number
|
||||
name: string
|
||||
|
||||
新しい課題から参照
ユーザをブロックする