From c0f42ea0c0764c771f4aaf46580fcecacafeee93 Mon Sep 17 00:00:00 2001 From: Codex Night Worker Date: Fri, 13 Nov 2026 12:50:57 +0900 Subject: [PATCH] AI: address issue #170 --- frontend/src/components/PostEditForm.tsx | 76 +++++++++++++++- frontend/src/components/PostList.tsx | 108 +++++++++++++---------- frontend/src/locales/ain-Kana/posts.json | 2 + frontend/src/locales/ja/posts.json | 1 + 4 files changed, 138 insertions(+), 49 deletions(-) diff --git a/frontend/src/components/PostEditForm.tsx b/frontend/src/components/PostEditForm.tsx index 99039d9..f3c741a 100644 --- a/frontend/src/components/PostEditForm.tsx +++ b/frontend/src/components/PostEditForm.tsx @@ -1,6 +1,8 @@ +import { useQuery } from '@tanstack/react-query' import { useEffect, useMemo, useState } from 'react' import { useLocale } from '@/lib/useLocale' +import PostList from '@/components/PostList' import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField' import FieldError from '@/components/common/FieldError' import PostDurationField from '@/components/posts/PostDurationField' @@ -10,14 +12,15 @@ import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/use-toast' import { isApiError } from '@/lib/api' import useDialogue from '@/lib/dialogues/useDialogue' -import { updatePost } from '@/lib/posts' +import { fetchPosts, updatePost } from '@/lib/posts' +import { postsKeys } from '@/lib/queryKeys' import { msToTime } from '@/lib/utils' import { useValidationErrors } from '@/lib/useValidationErrors' import { useUITranslation } from '@/lib/useUITranslation' import type { FC, FormEvent } from 'react' -import type { Post, TagWithSections } from '@/types' +import type { FetchPostsParams, Post, TagWithSections } from '@/types' type PostFormField = 'parentPostIds' | 'tags' | 'videoMs' @@ -68,6 +71,33 @@ const PostEditForm: FC = ({ post, onSave }) => { const [tags, setTags] = useState (() => tagsToStr (post.tags)) const [title, setTitle] = useState (post.title) + const [importVisible, setImportVisible] = useState (false) + const [sourceTitle, setSourceTitle] = useState ('') + const [sourcePage, setSourcePage] = useState (1) + const sourceParams: FetchPostsParams = { + title: sourceTitle, page: sourcePage, limit: 20, + url: '', tags: '', match: 'all', order: 'original_created_at:desc', + originalCreatedFrom: '', originalCreatedTo: '', + createdFrom: '', createdTo: '', updatedFrom: '', updatedTo: '' } + const { data: sources, isError: sourceError } = useQuery ({ + queryKey: postsKeys.index (sourceParams, locale), + queryFn: () => fetchPosts (sourceParams, locale), + enabled: importVisible && !disabled }) + + const importTags = (source: Post) => { + if (disabled) + return + setTags (current => { + const existing = new Set (current.split (/\s+/)) + const additions = tagsToStr (source.tags).split (/\s+/) + .filter (tag => tag !== '' && !existing.has (tag)) + return additions.length > 0 + ? `${current}${current.trim () === '' ? '' : ' '}${additions.join (' ')}` + : current + }) + setImportVisible (false) + } + const videoFlg = useMemo (() => tags.split (/\s+/).some (tag => tag.replace (/\[.*\]$/, '') === '動画'), [tags]) @@ -192,6 +222,48 @@ const PostEditForm: FC = ({ post, onSave }) => { setTags={setTags} errors={fieldErrors.tags}/> + + {importVisible && ( +
{ + if (event.key === 'Enter' && event.target instanceof HTMLInputElement) + event.preventDefault () + }}> + { + setSourceTitle (value) + setSourcePage (1) + }}/> + + {!disabled && } +
+ + +
+
)} + {/* オリジナルの作成日時 */} void onClick?: (event: MouseEvent) => void } -const PostList: FC = ({ posts, onClick }) => { +const PostList: FC = ({ posts, onClick, onSelect }) => { const { label } = useUITranslation ('shared') const location = useLocation () const behaviourSettings = useClientBehaviourSettings () @@ -39,7 +41,63 @@ const PostList: FC = ({ posts, onClick }) => { const sharedId = `page-${ post.id }` const layoutId = sharedId - return ( + const card = ( + 0 && 'ring-4 ring-green-500', + (post.parentPosts ?? []).length > 0 && 'ring-4 ring-yellow-500')} + whileHover={{ scale: 1.02 }} + onLayoutAnimationStart={() => { + if (!(cardRef.current)) + return + + cardRef.current.style.position = 'relative' + cardRef.current.style.zIndex = '9999' + }} + onLayoutAnimationComplete={() => { + if (!(cardRef.current)) + return + + cardRef.current.style.zIndex = '' + cardRef.current.style.position = '' + }} + transition={{ layout: cardLayoutTransition }}> + {thumbnailMode === 'off' + ? ( +
+ {label ('labels.noThumbnail')} +
) + : ( + {post.title)} +
) + + return onSelect + ? ( + ) + : ( = ({ posts, onClick }) => { setForLocationKey (location.key, sharedId) onClick?.(e) }}> - 0 && 'ring-4 ring-green-500', - (post.parentPosts ?? []).length > 0 && 'ring-4 ring-yellow-500')} - whileHover={{ scale: 1.02 }} - onLayoutAnimationStart={() => { - if (!(cardRef.current)) - return - - cardRef.current.style.position = 'relative' - cardRef.current.style.zIndex = '9999' - }} - onLayoutAnimationComplete={() => { - if (!(cardRef.current)) - return - - cardRef.current.style.zIndex = '' - cardRef.current.style.position = '' - }} - transition={{ layout: cardLayoutTransition }}> - {thumbnailMode === 'off' - ? ( -
- {label ('labels.noThumbnail')} -
) - : ( - {post.title)} -
+ {card}
) })} ) diff --git a/frontend/src/locales/ain-Kana/posts.json b/frontend/src/locales/ain-Kana/posts.json index 0310a6b..436056b 100644 --- a/frontend/src/locales/ain-Kana/posts.json +++ b/frontend/src/locales/ain-Kana/posts.json @@ -1,4 +1,5 @@ { + "_untranslated": ["edit.import"], "list": { "tagline": "{{brand}}", "empty": "ミンタㇻ タ ネン カ イサㇺ", @@ -31,6 +32,7 @@ "showExternal": "ソイタㇰ ソㇱ アヌカレ" }, "edit": { + "import": "インポート", "validationFailed": "アシㇼ カㇻ カ エアイカㇷ゚……", "conflictTitle": "モㇱマ クㇽ アシㇼ カㇻ ワ アン!", "conflictDescription": "モㇱマ イカㇻ クㇽ エエトコ アシㇼ カㇻ ワ アン。", diff --git a/frontend/src/locales/ja/posts.json b/frontend/src/locales/ja/posts.json index b6c1308..74649d0 100644 --- a/frontend/src/locales/ja/posts.json +++ b/frontend/src/locales/ja/posts.json @@ -22,6 +22,7 @@ "question": "表示しますか?", "showExternal": "外部ページを表示" }, "edit": { + "import": "インポート", "validationFailed": "更新はできなかったよ……", "conflictTitle": "競合が発生しました.", "conflictDescription": "ほかの耕作員が先に更新してゐます.",