このコミットが含まれているのは:
@@ -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|
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 ?? [])
|
||||
|
||||
@@ -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)
|
||||
? (
|
||||
|
||||
@@ -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 } })
|
||||
|
||||
|
||||
|
||||
@@ -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 }) })
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする