このコミットが含まれているのは:
2026-09-24 07:01:36 +09:00
コミット 1de00191ea
11個のファイルの変更108行の追加22行の削除
+3 -2
ファイルの表示
@@ -10,8 +10,9 @@ class TagNameSanitisationRule < ApplicationRecord
validate :source_pattern_must_be_regexp
class << self
def sanitise(name) =
rules.reduce(name.dup) { |name, (pattern, replacement)| name.gsub(pattern, replacement) }
def sanitise(name)
rules.reduce(name.dup) { |name, (pattern, replacement)| name.gsub(pattern, replacement) }
end
def apply!
TagName.find_each do |tn|
+2 -1
ファイルの表示
@@ -143,7 +143,8 @@ namespace :nico do
desired_tag_ids = kept_tag_ids.to_a
datum['tags'].each do |raw|
tag = ExternalTag.find_or_create_by!(platform: :nico, name: raw)
name = TagNameSanitisationRule.sanitise(raw)
tag = ExternalTag.find_or_create_by!(platform: :nico, name:)
unless tag.nico_tag_versions.exists?
NicoTagVersionRecorder.record!(external_tag: tag,
+62 -3
ファイルの表示
@@ -15,6 +15,11 @@ RSpec.describe 'nico:sync' do
NicoTagRelation.create!(nico_tag_id: nico_tag.id, tag_id: tag.id)
end
def create_nico_sanitisation_rules!
TagNameSanitisationRule.create!(priority: 20, source_pattern: '\\?', replacement: '_')
TagNameSanitisationRule.create!(priority: 40, source_pattren: '_$', replacement: '')
end
it '既存 post を見つけて、nico tag と linked tag を追加し、差分が出たら bot を付ける' do
# 既存 post(正規表現で拾われるURL)
post = Post.create!(
@@ -435,17 +440,24 @@ RSpec.describe 'nico:sync' do
expect(post.tags.map(&:name)).not_to include('bot操作')
end
it '外部タグが新規記載されたとき,その時点の連携タグを記載する' do
it 'サニタイズ後の既存外部タグを再利用し、その連携タグを記載する' do
post = create_nico_sync_post!
create_nico_sanitisation_rules!
external_tag = create_external_tag!('AAA')
linked_tag = create_tag!('spec_linked', category: :general)
link_nico_to_tag!(external_tag, linked_tag)
run_nico_sync_with_tags!(['AAA'])
expect {
run_nico_sync_with_tags!(['AAA?'])
}.not_to change(ExternalTag, :count)
expect(post.reload.external_tags).to include(external_tag)
post.reload
expect(post.external_tags).to contain_exactly(external_tag)
expect(post.tags).to include(linked_tag)
expect(ExternalTag.exists?(platform: :nico, name: 'AAA?')).to be(false)
end
it '外部タグに差分がない場合,内外マッピングが変はっても連携タグを再評価しない' do
@@ -520,4 +532,51 @@ RSpec.describe 'nico:sync' do
# 再記載時点の内外マッピングが新たに適用される.
expect(post.tags).to include(new_linked_tag)
end
it '外部タグだけの変更では bot を付けず、投稿履歴を記録する' do
post = create_nico_sync_post!
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: nil)
create_nico_sanitisation_rules!
expect {
run_nico_sync_with_tags!(['AAA?', 'AAA?'])
}.to change(PostVersion, :count).by(1)
.and change(ExternalTag, :count).by(1)
.and change(PostExternalTag, :count).by(1)
.and change(NicoTagVersion, :count).by(1)
.and change(TagName, :count).by(0)
external = post.reload.external_tags.sole
expect(external).to have_attributes(
platform: 'nico',
name: 'AAA')
expect(ExternalTag.exists?(platform: :nico, name: 'AAA?')).to be(false)
expect(post.tags.map(&:name)).not_to include('bot操作')
expect(post.post_versions.order(:version_no).last.tags_json)
.to include('external_tag_id' => external.id)
expect {
run_nico_sync_with_tags!(['AAA?'])
}.to change(PostVersion, :count).by(0)
.and change(NicoTagVersion, :count).by(0)
expect {
run_nico_sync_with_tags!([])
}.to change(PostVersion, :count).by(1)
expect(post.reload.external_tags).to be_empty
expect(post.tags.map(&:name)).not_to include('bot操作')
end
it 'nico: prefix を含む従来の規則で外部タグ名をサニタイズする' do
post = create_nico_sync_post!
run_nico_sync_with_tags!(['foo:_bar'])
expect(post.reload.external_tags.sole)
.to have_attributes(platform: 'nico', name: 'foo:_bar')
end
end
+6 -3
ファイルの表示
@@ -38,6 +38,7 @@ const DraggableDroppableTagRow: FC<Props> = ({
{ normal: { duration: .2, ease: 'easeOut' as const } },
)
const dndId = `tag-node:${ pathKey }`
const dndDisabled = tag.category === 'nico'
const downPosRef = useRef<{ x: number; y: number } | null> (null)
const armedRef = useRef (false)
@@ -62,7 +63,8 @@ const DraggableDroppableTagRow: FC<Props> = ({
const { attributes,
listeners,
setNodeRef: setDragRef,
transform } = useDraggable ({ id: dndId,
transform } = useDraggable ({ id: dndId,
disabled: dndDisabled,
data: { kind: 'tag',
dndId,
tagId: tag.id,
@@ -70,8 +72,9 @@ const DraggableDroppableTagRow: FC<Props> = ({
nestLevel } })
const { setNodeRef: setDropRef, isOver: over } = useDroppable ({
id: dndId,
data: { kind: 'tag', tagId: tag.id } })
id: dndId,
disabled: dndDisabled,
data: { kind: 'tag', tagId: tag.id } })
const activeDragging = activeDndId === dndId
const style: CSSProperties = { transform: CSS.Translate.toString (transform),
+4 -3
ファイルの表示
@@ -150,16 +150,17 @@ const buildFlatTagByCategory = (
byCategory: TagByCategory,
): TagByCategory => {
const tagsTmp = { } as TagByCategory
const seen = new Set<number> ()
const seen = new Set<string> ()
for (const category of CATEGORIES)
tagsTmp[category] = []
const visit = (tag: TagWithSections) => {
if (seen.has (tag.id))
const key = `${ tag.category }:${ tag.id }`
if (seen.has (key))
return
seen.add (tag.id)
seen.add (key)
tagsTmp[tag.category].push ({ ...tag, children: [] })
for (const child of tag.children ?? [])
+1 -1
ファイルの表示
@@ -86,7 +86,7 @@ const TagLink: FC<Props> = ({ tag,
className={cn (
'inline-flex min-w-0 max-w-full flex-nowrap items-stretch align-baseline',
'gap-x-1 md:items-baseline')}>
{(linkFlg && withWiki && isFullTag (tag)) && (
{(linkFlg && withWiki && isFullTag (tag) && tag.category !== 'nico') && (
<span className={markerWrapClass}>
{(tag.materialId != null || tag.hasWiki || tag.hasDeerjikists)
? (
+8 -5
ファイルの表示
@@ -28,15 +28,18 @@ export const fetchPost = async (id: string): Promise<Post> => await apiGet (`/po
export const fetchPostChanges = async (
{ post, tag, page, limit }: {
post?: string
tag?: string
page: number
limit: number }): Promise<{
{ post, tag, externalTag, page, limit }: {
post?: string
tag?: string
externalTag?: string
page: number
limit: number }): Promise<{
versions: PostVersion[]
count: number }> =>
await apiGet ('/posts/versions', { params: { ...(post && { post }),
...(tag && { tag }),
...(externalTag && {
external_tag: externalTag }),
page, limit } })
+3
ファイルの表示
@@ -128,6 +128,7 @@ const prefetchPostShow: Prefetcher = async (qc, url) => {
const prefetchPostChanges: Prefetcher = async (qc, url) => {
const id = url.searchParams.get ('id')
const tag = url.searchParams.get ('tag')
const externalTag = url.searchParams.get ('external_tag')
const page = Number (url.searchParams.get ('page') || 1)
const limit = Number (url.searchParams.get ('limit') || 20)
@@ -141,9 +142,11 @@ const prefetchPostChanges: Prefetcher = async (qc, url) => {
await qc.prefetchQuery ({
queryKey: postsKeys.changes ({ ...(id && { id }),
...(tag && { tag }),
...(externalTag && { externalTag }),
page, limit }),
queryFn: () => fetchPostChanges ({ ...(id && { id }),
...(tag && { tag }),
...(externalTag && { externalTag }),
page, limit }) })
}
+5 -1
ファイルの表示
@@ -11,7 +11,11 @@ export const postsKeys = {
index: (p: FetchPostsParams) => ['posts', 'index', p] as const,
show: (id: string) => ['posts', id] as const,
related: (id: string) => ['related', id] as const,
changes: (p: { post?: string; tag?: string; page: number; limit: number }) =>
changes: (p: { post?: string
tag?: string
externalTag?: string
page: number
limit: number }) =>
['posts', 'changes', p] as const }
export const gekanatorKeys = {
+4
ファイルの表示
@@ -51,6 +51,7 @@ const PostHistoryPage: FC = () => {
const query = new URLSearchParams (location.search)
const id = query.get ('id')
const tagId = query.get ('tag')
const externalTagId = query.get ('external_tag')
const page = Number (query.get ('page') ?? 1)
const limit = Number (query.get ('limit') ?? 20)
@@ -66,9 +67,12 @@ const PostHistoryPage: FC = () => {
const { data, isLoading: loading } = useQuery ({
queryKey: postsKeys.changes ({ ...(id && { post: id }),
...(tagId && { tag: tagId }),
...(externalTagId && { externalTag: externalTagId }),
page, limit }),
queryFn: () => fetchPostChanges ({ ...(id && { post: id }),
...(tagId && { tag: tagId }),
...(externalTagId && {
externalTag: externalTagId }),
page, limit }) })
const changes = data?.versions ?? []
const totalPages = data ? Math.ceil (data.count / limit) : 0
+10 -3
ファイルの表示
@@ -406,11 +406,15 @@ const TagListPage: FC = () => {
<tbody>
{results.map (row => (
<tr key={row.id} className="even:bg-gray-100 dark:even:bg-gray-700">
<tr
key={`${ row.category }:${ row.id }`}
className="even:bg-gray-100 dark:even:bg-gray-700">
<td className="p-2">
<TagLink
tag={row}
to={`/tags/${ encodeURIComponent (row.id) }`}
to={row.category === 'nico'
? `/tags/nico?name=${ encodeURIComponent (row.name) }`
: `/tags/${ encodeURIComponent (row.id) }`}
withCount={false}/>
</td>
<td className="p-2 text-right">{row.postCount}</td>
@@ -429,7 +433,10 @@ const TagListPage: FC = () => {
<td className="p-2">{dateString (row.createdAt)}</td>
<td className="p-2">{dateString (row.updatedAt)}</td>
<td className="p-2">
<PrefetchLink to={`/posts/changes?tag=${ row.id }`}>
<PrefetchLink
to={row.category === 'nico'
? `/posts/changes?external_tag=${ row.id }`
: `/posts/changes?tag=${ row.id }`}>
</PrefetchLink>
</td>