Reviewed-on: http://git.miteruzo.com/miteruzo/btrc-hub/pulls/413 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #413 でマージされました。
このコミットが含まれているのは:
@@ -0,0 +1,308 @@
|
||||
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<Props> = (
|
||||
{ row,
|
||||
controls,
|
||||
onSave },
|
||||
) => {
|
||||
const [draft, setDraft] = useState<Draft> (() => buildDraft (row))
|
||||
const [messageRow, setMessageRow] = useState<PostImportRow | null> (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 extends keyof Draft,> (
|
||||
key: Key,
|
||||
value: Draft[Key],
|
||||
) => {
|
||||
if (messageRow != null)
|
||||
setMessageRow (null)
|
||||
setDraft (current => ({ ...current, [key]: value }))
|
||||
}
|
||||
|
||||
const reset = useCallback (async (): Promise<boolean> => {
|
||||
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<boolean> => {
|
||||
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 (
|
||||
<>
|
||||
<div className="px-6 pb-6">
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-6 md:grid-cols-[7rem_minmax(0,1fr)]">
|
||||
<div className="space-y-3 md:sticky md:top-0 md:self-start">
|
||||
<PostImportThumbnailPreview
|
||||
url={committedThumbnailBase}
|
||||
file={
|
||||
hasThumbnailBaseValue (committedThumbnailBase)
|
||||
? undefined
|
||||
: draft.thumbnailFile}
|
||||
className="h-28 w-28"/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<PostCreationDataFields
|
||||
url={{
|
||||
value: draft.url,
|
||||
onChange: value => update ('url', value),
|
||||
disabled: saving,
|
||||
warnings: displayRow.fieldWarnings.url,
|
||||
errors: groupedMessages (
|
||||
displayRow.validationErrors.url,
|
||||
displayRow.importErrors?.url) }}
|
||||
thumbnailField={
|
||||
<>
|
||||
<PostTextField
|
||||
label="サムネール"
|
||||
value={draft.thumbnailBase}
|
||||
disabled={saving}
|
||||
warnings={currentThumbnailWarnings}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.thumbnailBase,
|
||||
displayRow.importErrors?.thumbnailBase)}
|
||||
onBlur={() => {
|
||||
if (draft.thumbnailBase.trim () !== committedThumbnailBase.trim ())
|
||||
setCommittedThumbnailBase (draft.thumbnailBase)
|
||||
}}
|
||||
onChange={value => update ('thumbnailBase', value)}/>
|
||||
{!(hasThumbnailBaseValue (draft.thumbnailBase)) && (
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
disabled={saving}
|
||||
onChange={event => {
|
||||
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
|
||||
? (
|
||||
<PostDurationField
|
||||
value={draft.duration}
|
||||
onChange={value => update ('duration', value)}
|
||||
disabled={saving}
|
||||
errors={groupedMessages (
|
||||
displayRow.validationErrors.videoMs,
|
||||
displayRow.importErrors?.videoMs)}/>)
|
||||
: null}/>
|
||||
<FieldWarning messages={displayRow.baseWarnings}/>
|
||||
<FieldError messages={displayRow.validationErrors.base}/>
|
||||
<FieldError messages={displayRow.importErrors?.base}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>)
|
||||
}
|
||||
|
||||
export default PostImportRowForm
|
||||
export { buildDraft }
|
||||
export type { Draft as PostImportRowDraft }
|
||||
新しいイシューから参照
ユーザーをブロックする