import { useCallback, useEffect, useMemo, useState } from 'react' import FieldError from '@/components/common/FieldError' import FieldWarning from '@/components/common/FieldWarning' import PostCreationDataFields from '@/components/posts/PostCreationDataFields' import PostDurationField from '@/components/posts/PostDurationField' import PostTextField from '@/components/posts/PostTextField' import PostImportThumbnailPreview from '@/components/posts/import/PostImportThumbnailPreview' import { hasThumbnailBaseValue, hasVideoTag } from '@/lib/postImportRows' import type { FC } from 'react' import type { DialogueFormControls } from '@/lib/dialogues/useDialogue' import type { PostImportEditableDraft, PostImportRow } from '@/lib/postImportTypes' type Draft = PostImportEditableDraft type Props = { row: PostImportRow controls: DialogueFormControls onSave: (args: { draft: Draft resetRequested: boolean }) => Promise<{ saved: boolean row: PostImportRow | null }> } const THUMBNAIL_MISSING_WARNING = 'サムネールなし' const buildDraft = (row: PostImportRow): Draft => ({ url: row.url, title: String (row.attributes.title ?? ''), thumbnailBase: String (row.attributes.thumbnailBase ?? ''), originalCreatedFrom: String (row.attributes.originalCreatedFrom ?? ''), originalCreatedBefore: String (row.attributes.originalCreatedBefore ?? ''), tags: String (row.attributes.tags ?? ''), parentPostIds: String (row.attributes.parentPostIds ?? ''), duration: String (row.attributes.duration ?? ''), thumbnailFile: row.thumbnailFile }) const buildResetDraft = (row: PostImportRow): Draft => ({ url: row.resetSnapshot.url, title: String (row.resetSnapshot.attributes.title ?? ''), thumbnailBase: String (row.resetSnapshot.attributes.thumbnailBase ?? ''), originalCreatedFrom: String (row.resetSnapshot.attributes.originalCreatedFrom ?? ''), originalCreatedBefore: String (row.resetSnapshot.attributes.originalCreatedBefore ?? ''), tags: String (row.resetSnapshot.attributes.tags ?? ''), parentPostIds: String (row.resetSnapshot.attributes.parentPostIds ?? ''), duration: String (row.resetSnapshot.attributes.duration ?? ''), thumbnailFile: undefined }) const groupedMessages = (...values: (string[] | undefined)[]): string[] => [...new Set (values.flatMap (value => value ?? []))] const sameDraft = (left: Draft, right: Draft): boolean => left.url === right.url && left.title === right.title && left.thumbnailBase === right.thumbnailBase && left.originalCreatedFrom === right.originalCreatedFrom && left.originalCreatedBefore === right.originalCreatedBefore && left.tags === right.tags && left.parentPostIds === right.parentPostIds && left.duration === right.duration && left.thumbnailFile === right.thumbnailFile const sameProvenance = ( current: PostImportRow['provenance'], reset: PostImportRow['resetSnapshot']['provenance'], ): boolean => Object.keys (reset).every (field => current[field] === reset[field]) const sameTagSources = ( current: PostImportRow['tagSources'], reset: PostImportRow['resetSnapshot']['tagSources'], ): boolean => (current?.automatic ?? '') === reset.automatic && (current?.manual ?? '') === reset.manual const sameWarnings = ( current: PostImportRow, reset: PostImportRow['resetSnapshot'], ): boolean => JSON.stringify (current.fieldWarnings) === JSON.stringify (reset.fieldWarnings) && JSON.stringify (current.baseWarnings) === JSON.stringify (reset.baseWarnings) const thumbnailWarnings = ( messages: string[] | undefined, thumbnailBase: string, thumbnailFile: File | undefined, ): string[] => { const others = (messages ?? []).filter (message => message !== THUMBNAIL_MISSING_WARNING) return hasThumbnailBaseValue (thumbnailBase) || thumbnailFile != null ? others : [...new Set ([...others, THUMBNAIL_MISSING_WARNING])] } const PostImportRowForm: FC = ( { row, controls, onSave }, ) => { const [draft, setDraft] = useState (() => buildDraft (row)) const [messageRow, setMessageRow] = useState (null) const [saving, setSaving] = useState (false) const [resetRequested, setResetRequested] = useState (false) const [committedThumbnailBase, setCommittedThumbnailBase] = useState ( () => String (row.attributes.thumbnailBase ?? '')) useEffect (() => { const nextDraft = buildDraft (row) setDraft (nextDraft) setMessageRow (null) setResetRequested (false) setCommittedThumbnailBase (String (row.attributes.thumbnailBase ?? '')) }, [row]) const displayRow = messageRow ?? row const resetDraft = useMemo ( () => buildResetDraft (row), [row]) const durationVisible = hasVideoTag (draft.tags) const currentThumbnailWarnings = thumbnailWarnings ( displayRow.fieldWarnings.thumbnailBase, draft.thumbnailBase, draft.thumbnailFile) const resetDisabled = saving || (sameDraft (draft, resetDraft) && sameProvenance (row.provenance, row.resetSnapshot.provenance) && sameTagSources (row.tagSources, row.resetSnapshot.tagSources) && row.metadataUrl === row.resetSnapshot.metadataUrl && sameWarnings (displayRow, row.resetSnapshot)) const update = ( key: Key, value: Draft[Key], ) => { if (messageRow != null) setMessageRow (null) setDraft (current => ({ ...current, [key]: value })) } const reset = useCallback (async (): Promise => { if (resetDisabled) return false const confirmed = await controls.confirm ({ title: '変更をリセットしますか?', confirmText: 'リセット', cancelText: '取消', variant: 'danger' }) if (!(confirmed)) return false setDraft (resetDraft) setResetRequested (true) setMessageRow (null) setCommittedThumbnailBase (resetDraft.thumbnailBase) return false }, [controls, resetDisabled, resetDraft]) const save = useCallback (async (): Promise => { setSaving (true) try { const result = await onSave ({ draft, resetRequested }) if (result.saved) return true if (result.row != null) setMessageRow (result.row) return false } finally { setSaving (false) } }, [draft, onSave, resetRequested]) useEffect (() => { controls.setActions ([{ label: '変更をリセット', placement: 'start', variant: 'danger', disabled: resetDisabled, onSelect: reset }, { label: '編輯内容を保存', disabled: saving, onSelect: save }]) }, [controls, resetDisabled, reset, save, saving]) return ( <>
update ('url', value), disabled: saving, warnings: displayRow.fieldWarnings.url, errors: groupedMessages ( displayRow.validationErrors.url, displayRow.importErrors?.url) }} thumbnailField={ <> { if (draft.thumbnailBase.trim () !== committedThumbnailBase.trim ()) setCommittedThumbnailBase (draft.thumbnailBase) }} onChange={value => update ('thumbnailBase', value)}/> {!(hasThumbnailBaseValue (draft.thumbnailBase)) && ( { const file = event.target.files?.[0] update ('thumbnailFile', file) }}/>)} } core={{ title: { value: draft.title, onChange: value => update ('title', value), disabled: saving, warnings: displayRow.fieldWarnings.title, errors: groupedMessages ( displayRow.validationErrors.title, displayRow.importErrors?.title) }, originalCreated: { disabled: saving, originalCreatedFrom: draft.originalCreatedFrom || null, setOriginalCreatedFrom: value => update ('originalCreatedFrom', value ?? ''), originalCreatedBefore: draft.originalCreatedBefore || null, setOriginalCreatedBefore: value => update ('originalCreatedBefore', value ?? ''), errors: { originalCreatedAt: groupedMessages ( displayRow.validationErrors.originalCreatedAt, displayRow.importErrors?.originalCreatedAt), originalCreatedFrom: groupedMessages ( displayRow.validationErrors.originalCreatedFrom, displayRow.importErrors?.originalCreatedFrom), originalCreatedBefore: groupedMessages ( displayRow.validationErrors.originalCreatedBefore, displayRow.importErrors?.originalCreatedBefore) } }, tags: { value: draft.tags, onChange: value => update ('tags', value), disabled: saving, warnings: displayRow.fieldWarnings.tags, errors: groupedMessages ( displayRow.validationErrors.tags, displayRow.importErrors?.tags), rows: 4 }, parentPostIds: { value: draft.parentPostIds, onChange: value => update ('parentPostIds', value), disabled: saving, errors: groupedMessages ( displayRow.validationErrors.parentPostIds, displayRow.importErrors?.parentPostIds) } }} extraFields={ durationVisible ? ( update ('duration', value)} disabled={saving} errors={groupedMessages ( displayRow.validationErrors.videoMs, displayRow.importErrors?.videoMs)}/>) : null}/>
) } export default PostImportRowForm export { buildDraft } export type { Draft as PostImportRowDraft }