コミットを比較
2 コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| 8eb8fb355b | |||
| ffd28c0f9e |
@@ -107,11 +107,16 @@ npm run preview
|
|||||||
- Prefer single quotes for strings unless interpolation or escaping makes
|
- Prefer single quotes for strings unless interpolation or escaping makes
|
||||||
double quotes better.
|
double quotes better.
|
||||||
- Ruby: never put a space before method-call parentheses.
|
- Ruby: never put a space before method-call parentheses.
|
||||||
|
- Ruby: `render` 系メソッド呼び出しでは、keyword 引数付きでも括弧を書かない。
|
||||||
- Ruby: never put a line break immediately before `)`.
|
- Ruby: never put a line break immediately before `)`.
|
||||||
- Ruby: do not use `%w` or `%i`.
|
- Ruby: do not use `%w` or `%i`.
|
||||||
|
- In Ruby, when an `if` condition is split across multiple lines and combines
|
||||||
|
clauses with `&&` or `||`, wrap the whole condition in parentheses.
|
||||||
- Ruby hashes are not blocks; keep `}` on the same line as the final pair.
|
- Ruby hashes are not blocks; keep `}` on the same line as the final pair.
|
||||||
- Ruby hashes keep the first pair on the same line as `{` unless line length
|
- Ruby hashes keep the first pair on the same line as `{` unless line length
|
||||||
requires a break.
|
requires a break.
|
||||||
|
- Short Ruby hashes may stay visually compact across two lines with the first
|
||||||
|
pair kept on the opening line and aligned continuation pairs below it.
|
||||||
- Ruby blocks use separate `{ ... }` rules from hashes, with 2-space body
|
- Ruby blocks use separate `{ ... }` rules from hashes, with 2-space body
|
||||||
indentation.
|
indentation.
|
||||||
- For arrays, never put whitespace or a line break immediately before `]`.
|
- For arrays, never put whitespace or a line break immediately before `]`.
|
||||||
|
|||||||
@@ -72,17 +72,23 @@ service, representation, and spec.
|
|||||||
- Prefer precise, minimal changes.
|
- Prefer precise, minimal changes.
|
||||||
- Use single quotes unless interpolation or escaping makes double quotes better.
|
- Use single quotes unless interpolation or escaping makes double quotes better.
|
||||||
- Do not put a space before Ruby method-call parentheses.
|
- Do not put a space before Ruby method-call parentheses.
|
||||||
|
- For `render`-family method calls, omit parentheses even when passing
|
||||||
|
keyword arguments.
|
||||||
- Never put a line break immediately before `)` in Ruby.
|
- Never put a line break immediately before `)` in Ruby.
|
||||||
- Do not use `%w` or `%i` in new Ruby code.
|
- Do not use `%w` or `%i` in new Ruby code.
|
||||||
- Never write a Ruby line longer than 99 characters.
|
- Never write a Ruby line longer than 99 characters.
|
||||||
- Aim to keep Ruby lines within 79 characters where practical.
|
- Aim to keep Ruby lines within 79 characters where practical.
|
||||||
- For small Ruby method definitions that take keyword arguments, match the
|
- For small Ruby method definitions that take keyword arguments, match the
|
||||||
local no-parentheses style when nearby code uses it.
|
local no-parentheses style when nearby code uses it.
|
||||||
|
- When an `if` condition is split across multiple lines and combines clauses
|
||||||
|
with `&&` or `||`, wrap the whole condition in parentheses.
|
||||||
- Treat Ruby hash `{ ... }` style and Ruby block `{ ... }` style as separate
|
- Treat Ruby hash `{ ... }` style and Ruby block `{ ... }` style as separate
|
||||||
rules.
|
rules.
|
||||||
- Do not format Ruby hashes like Ruby blocks.
|
- Do not format Ruby hashes like Ruby blocks.
|
||||||
- For Ruby hashes, keep the closing `}` on the same line as the final pair.
|
- For Ruby hashes, keep the closing `}` on the same line as the final pair.
|
||||||
- Keep the first pair on the same line as `{` by default.
|
- Keep the first pair on the same line as `{` by default.
|
||||||
|
- Short Ruby hashes may stay visually compact across two lines with the first
|
||||||
|
pair kept on the opening line and aligned continuation pairs below it.
|
||||||
- If the hash would exceed the line limit, break after `{` and indent pairs
|
- If the hash would exceed the line limit, break after `{` and indent pairs
|
||||||
by 4 spaces.
|
by 4 spaces.
|
||||||
- Put one logical pair per line when the expression would otherwise become
|
- Put one logical pair per line when the expression would otherwise become
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class TagVersionsController < ApplicationController
|
|||||||
AND prev.version_no = tag_versions.version_no - 1
|
AND prev.version_no = tag_versions.version_no - 1
|
||||||
SQL
|
SQL
|
||||||
.select('tag_versions.*', 'prev.name AS prev_name', 'prev.category AS prev_category',
|
.select('tag_versions.*', 'prev.name AS prev_name', 'prev.category AS prev_category',
|
||||||
|
'prev.deprecated_at AS prev_deprecated_at',
|
||||||
'prev.aliases AS prev_aliases', 'prev.parent_tag_ids AS prev_parent_tag_ids')
|
'prev.aliases AS prev_aliases', 'prev.parent_tag_ids AS prev_parent_tag_ids')
|
||||||
q = q.where('tag_versions.tag_id = ?', tag_id) if tag_id
|
q = q.where('tag_versions.tag_id = ?', tag_id) if tag_id
|
||||||
|
|
||||||
@@ -62,6 +63,8 @@ class TagVersionsController < ApplicationController
|
|||||||
event_type: row.event_type,
|
event_type: row.event_type,
|
||||||
name: { current: row.name, prev: row.attributes['prev_name'] },
|
name: { current: row.name, prev: row.attributes['prev_name'] },
|
||||||
category: { current: row.category, prev: row.attributes['prev_category'] },
|
category: { current: row.category, prev: row.attributes['prev_category'] },
|
||||||
|
deprecated_at: { current: row.deprecated_at&.iso8601,
|
||||||
|
prev: row.attributes['prev_deprecated_at']&.iso8601 },
|
||||||
aliases: build_version_values(cur_aliases, prev_aliases, key: :name),
|
aliases: build_version_values(cur_aliases, prev_aliases, key: :name),
|
||||||
parent_tags:,
|
parent_tags:,
|
||||||
created_at: row.created_at.iso8601,
|
created_at: row.created_at.iso8601,
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class TagsController < ApplicationController
|
|||||||
post_count_between[1] = nil if post_count_between[1] < 0
|
post_count_between[1] = nil if post_count_between[1] < 0
|
||||||
created_between = params[:created_from].presence, params[:created_to].presence
|
created_between = params[:created_from].presence, params[:created_to].presence
|
||||||
updated_between = params[:updated_from].presence, params[:updated_to].presence
|
updated_between = params[:updated_from].presence, params[:updated_to].presence
|
||||||
|
deprecated_given = params.key?(:deprecated)
|
||||||
|
deprecated = bool?(:deprecated)
|
||||||
|
|
||||||
order = params[:order].to_s.split(':', 2).map(&:strip)
|
order = params[:order].to_s.split(':', 2).map(&:strip)
|
||||||
unless order[0].in?(['name', 'category', 'post_count', 'created_at', 'updated_at'])
|
unless order[0].in?(['name', 'category', 'post_count', 'created_at', 'updated_at'])
|
||||||
@@ -48,6 +50,9 @@ class TagsController < ApplicationController
|
|||||||
q = q.where('tags.created_at <= ?', created_between[1]) if created_between[1]
|
q = q.where('tags.created_at <= ?', created_between[1]) if created_between[1]
|
||||||
q = q.where('tags.updated_at >= ?', updated_between[0]) if updated_between[0]
|
q = q.where('tags.updated_at >= ?', updated_between[0]) if updated_between[0]
|
||||||
q = q.where('tags.updated_at <= ?', updated_between[1]) if updated_between[1]
|
q = q.where('tags.updated_at <= ?', updated_between[1]) if updated_between[1]
|
||||||
|
if deprecated_given
|
||||||
|
q = deprecated ? q.where.not(deprecated_at: nil) : q.where(deprecated_at: nil)
|
||||||
|
end
|
||||||
|
|
||||||
sort_sql =
|
sort_sql =
|
||||||
case order[0]
|
case order[0]
|
||||||
@@ -79,9 +84,21 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
tag_ids =
|
tag_ids =
|
||||||
if parent_tag_id
|
if parent_tag_id
|
||||||
TagImplication.where(parent_tag_id:).select(:tag_id)
|
TagImplication.joins(:tag)
|
||||||
|
.where(parent_tag_id:)
|
||||||
|
.where(tags: { deprecated_at: nil })
|
||||||
|
.select(:tag_id)
|
||||||
else
|
else
|
||||||
Tag.where.not(id: TagImplication.select(:tag_id)).select(:id)
|
Tag.where(deprecated_at: nil)
|
||||||
|
.where.not(id: TagImplication
|
||||||
|
.joins(<<~SQL.squish)
|
||||||
|
INNER JOIN
|
||||||
|
tags parent_tags
|
||||||
|
ON parent_tags.id = tag_implications.parent_tag_id
|
||||||
|
SQL
|
||||||
|
.where('parent_tags.deprecated_at IS NULL')
|
||||||
|
.select(:tag_id))
|
||||||
|
.select(:id)
|
||||||
end
|
end
|
||||||
|
|
||||||
tags =
|
tags =
|
||||||
@@ -89,6 +106,7 @@ class TagsController < ApplicationController
|
|||||||
.joins(:tag_name)
|
.joins(:tag_name)
|
||||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||||
.where(category: [:meme, :character, :material])
|
.where(category: [:meme, :character, :material])
|
||||||
|
.where(deprecated_at: nil)
|
||||||
.where(id: tag_ids)
|
.where(id: tag_ids)
|
||||||
.order('tag_names.name')
|
.order('tag_names.name')
|
||||||
.distinct
|
.distinct
|
||||||
@@ -101,7 +119,8 @@ class TagsController < ApplicationController
|
|||||||
TagImplication
|
TagImplication
|
||||||
.joins(:tag)
|
.joins(:tag)
|
||||||
.where(parent_tag_id: tags.map(&:id),
|
.where(parent_tag_id: tags.map(&:id),
|
||||||
tags: { category: [:meme, :character, :material] })
|
tags: { category: [:meme, :character, :material],
|
||||||
|
deprecated_at: nil })
|
||||||
.distinct
|
.distinct
|
||||||
.pluck(:parent_tag_id)
|
.pluck(:parent_tag_id)
|
||||||
end
|
end
|
||||||
@@ -133,6 +152,7 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
base = Tag.joins(:tag_name)
|
base = Tag.joins(:tag_name)
|
||||||
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
.includes(:tag_name, :materials, tag_name: :wiki_page)
|
||||||
|
.where(deprecated_at: nil)
|
||||||
base = base.where('tags.post_count > 0') if present_only
|
base = base.where('tags.post_count > 0') if present_only
|
||||||
|
|
||||||
canonical_hit =
|
canonical_hit =
|
||||||
@@ -252,18 +272,24 @@ class TagsController < ApplicationController
|
|||||||
category = params[:category].to_s.strip
|
category = params[:category].to_s.strip
|
||||||
return render_unprocessable_entity('名前は必須です.', field: :name) if name.blank?
|
return render_unprocessable_entity('名前は必須です.', field: :name) if name.blank?
|
||||||
return render_unprocessable_entity('カテゴリは必須です.', field: :category) if category.blank?
|
return render_unprocessable_entity('カテゴリは必須です.', field: :category) if category.blank?
|
||||||
|
return render_unprocessable_entity '廃止状態は必須です.', field: :deprecated unless params.key?(:deprecated)
|
||||||
|
|
||||||
if name != tag.name &&
|
if (name != tag.name &&
|
||||||
tag.in?([Tag.tagme, Tag.bot, Tag.no_deerjikist, Tag.video, Tag.niconico])
|
tag.in?([Tag.tagme, Tag.bot, Tag.no_deerjikist, Tag.video, Tag.niconico]))
|
||||||
return render_unprocessable_entity('システム・タグの名称は変更できません.', field: :name)
|
return render_unprocessable_entity 'システム・タグの名称は変更できません.', field: :name
|
||||||
end
|
|
||||||
|
|
||||||
if tag.nico? || category == 'nico'
|
|
||||||
return render_unprocessable_entity('ニコタグは変更できません.', field: :category)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
alias_names = params[:aliases].to_s.split.uniq
|
alias_names = params[:aliases].to_s.split.uniq
|
||||||
parent_names = params[:parent_tags].to_s.split.uniq
|
parent_names = params[:parent_tags].to_s.split.uniq
|
||||||
|
deprecated = bool?(:deprecated)
|
||||||
|
|
||||||
|
if tag.nico? && deprecated
|
||||||
|
return render_unprocessable_entity 'ニコタグは廃止できません.', field: :deprecated
|
||||||
|
end
|
||||||
|
|
||||||
|
if tag.nico? || category == 'nico'
|
||||||
|
return render_unprocessable_entity 'ニコタグは変更できません.', field: :category
|
||||||
|
end
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
||||||
@@ -272,7 +298,11 @@ class TagsController < ApplicationController
|
|||||||
name_changed = name != old_name
|
name_changed = name != old_name
|
||||||
wiki_page = tag.tag_name.wiki_page if name_changed
|
wiki_page = tag.tag_name.wiki_page if name_changed
|
||||||
|
|
||||||
tag.update!(category:)
|
if tag.deprecated? == deprecated
|
||||||
|
tag.update!(category:)
|
||||||
|
else
|
||||||
|
tag.update!(category:, deprecated_at: deprecated ? Time.current : nil)
|
||||||
|
end
|
||||||
tag.tag_name.update!(name:)
|
tag.tag_name.update!(name:)
|
||||||
|
|
||||||
alias_names << old_name if name_changed
|
alias_names << old_name if name_changed
|
||||||
@@ -300,11 +330,17 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
name = params[:name].presence
|
name = params[:name].presence
|
||||||
category = params[:category].presence
|
category = params[:category].presence
|
||||||
|
deprecated_given = params.key?(:deprecated)
|
||||||
|
deprecated = bool?(:deprecated)
|
||||||
|
|
||||||
tag = Tag.find(params[:id])
|
tag = Tag.find(params[:id])
|
||||||
|
|
||||||
|
if tag.nico? && deprecated_given && deprecated
|
||||||
|
return render_unprocessable_entity 'ニコタグは廃止できません.', field: :deprecated
|
||||||
|
end
|
||||||
|
|
||||||
if tag.nico? || (category.present? && category == 'nico')
|
if tag.nico? || (category.present? && category == 'nico')
|
||||||
return render_unprocessable_entity('ニコタグは変更できません.', field: :category)
|
return render_unprocessable_entity 'ニコタグは変更できません.', field: :category
|
||||||
end
|
end
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
@@ -316,6 +352,9 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
tag.tag_name.update!(name:) if name.present?
|
tag.tag_name.update!(name:) if name.present?
|
||||||
tag.update!(category:) if category.present?
|
tag.update!(category:) if category.present?
|
||||||
|
if deprecated_given && tag.deprecated? != deprecated
|
||||||
|
tag.update!(deprecated_at: deprecated ? Time.current : nil)
|
||||||
|
end
|
||||||
|
|
||||||
tag.reload
|
tag.reload
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class Tag < ApplicationRecord
|
|||||||
validate :nico_tag_name_must_start_with_nico
|
validate :nico_tag_name_must_start_with_nico
|
||||||
validate :tag_name_must_be_canonical
|
validate :tag_name_must_be_canonical
|
||||||
validate :category_must_be_deerjikist_with_deerjikists
|
validate :category_must_be_deerjikist_with_deerjikists
|
||||||
|
validate :nico_tags_cannot_be_deprecated
|
||||||
|
|
||||||
scope :nico_tags, -> { nico }
|
scope :nico_tags, -> { nico }
|
||||||
|
|
||||||
@@ -77,6 +78,8 @@ class Tag < ApplicationRecord
|
|||||||
(self.tag_name ||= build_tag_name).name = val
|
(self.tag_name ||= build_tag_name).name = val
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def deprecated? = deprecated_at?
|
||||||
|
|
||||||
def has_wiki = wiki_page.present?
|
def has_wiki = wiki_page.present?
|
||||||
|
|
||||||
def material_id = materials.first&.id
|
def material_id = materials.first&.id
|
||||||
@@ -228,4 +231,10 @@ class Tag < ApplicationRecord
|
|||||||
errors.add :category, 'ニジラーと紐づいてゐるタグはニジラー・カテゴリである必要があります.'
|
errors.add :category, 'ニジラーと紐づいてゐるタグはニジラー・カテゴリである必要があります.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def nico_tags_cannot_be_deprecated
|
||||||
|
if nico? && deprecated_at.present?
|
||||||
|
errors.add :deprecated_at, 'ニコタグは廃止できません.'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
|
|
||||||
module TagRepr
|
module TagRepr
|
||||||
BASE = { only: [:id, :category, :post_count, :created_at, :updated_at],
|
BASE = { only: [:id, :category, :post_count, :created_at, :updated_at, :deprecated_at],
|
||||||
methods: [:name, :has_wiki, :material_id, :has_deerjikists] }.freeze
|
methods: [:name, :has_wiki, :material_id, :has_deerjikists] }.freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class TagVersionRecorder < VersionRecorder
|
|||||||
def snapshot_attributes
|
def snapshot_attributes
|
||||||
{ name: @record.name,
|
{ name: @record.name,
|
||||||
category: @record.category,
|
category: @record.category,
|
||||||
|
deprecated_at: @record.deprecated_at,
|
||||||
aliases: @record.snapshot_aliases.join(' '),
|
aliases: @record.snapshot_aliases.join(' '),
|
||||||
parent_tag_ids: @record.snapshot_parent_tag_ids.join(' ') }
|
parent_tag_ids: @record.snapshot_parent_tag_ids.join(' ') }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
class AddDeprecatedAtToTags < ActiveRecord::Migration[8.0]
|
||||||
|
def up
|
||||||
|
add_column :tags, :deprecated_at, :datetime, after: :category
|
||||||
|
add_column :tag_versions, :deprecated_at, :datetime, after: :parent_tag_ids
|
||||||
|
|
||||||
|
add_index :tags, :deprecated_at
|
||||||
|
|
||||||
|
add_check_constraint :tags, "deprecated_at IS NULL OR category <> 'nico'",
|
||||||
|
name: 'chk_tags_deprecated_at_not_nico'
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_check_constraint :tags, name: 'chk_tags_deprecated_at_not_nico'
|
||||||
|
|
||||||
|
remove_index :tags, :deprecated_at
|
||||||
|
|
||||||
|
remove_column :tag_versions, :deprecated_at, :datetime
|
||||||
|
remove_column :tags, :deprecated_at
|
||||||
|
end
|
||||||
|
end
|
||||||
生成ファイル
+5
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2026_06_12_000000) do
|
ActiveRecord::Schema[8.0].define(version: 2026_06_21_000000) do
|
||||||
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "record_type", null: false
|
t.string "record_type", null: false
|
||||||
@@ -319,6 +319,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_12_000000) do
|
|||||||
t.string "event_type", null: false
|
t.string "event_type", null: false
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "category", null: false
|
t.string "category", null: false
|
||||||
|
t.datetime "deprecated_at"
|
||||||
t.text "aliases", null: false
|
t.text "aliases", null: false
|
||||||
t.text "parent_tag_ids", null: false
|
t.text "parent_tag_ids", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
@@ -336,10 +337,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_12_000000) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.integer "post_count", default: 0, null: false
|
t.integer "post_count", default: 0, null: false
|
||||||
|
t.datetime "deprecated_at"
|
||||||
t.datetime "discarded_at"
|
t.datetime "discarded_at"
|
||||||
t.integer "version_no", null: false
|
t.integer "version_no", null: false
|
||||||
|
t.index ["deprecated_at"], name: "index_tags_on_deprecated_at"
|
||||||
t.index ["discarded_at"], name: "index_tags_on_discarded_at"
|
t.index ["discarded_at"], name: "index_tags_on_discarded_at"
|
||||||
t.index ["tag_name_id"], name: "index_tags_on_tag_name_id", unique: true
|
t.index ["tag_name_id"], name: "index_tags_on_tag_name_id", unique: true
|
||||||
|
t.check_constraint "(`deprecated_at` is null) or (`category` <> _utf8mb4'nico')", name: "chk_tags_deprecated_at_not_nico"
|
||||||
t.check_constraint "`version_no` > 0", name: "chk_tags_version_no_positive"
|
t.check_constraint "`version_no` > 0", name: "chk_tags_version_no_positive"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ const mWiki = match<{ title: string }> ('/wiki/:title')
|
|||||||
const mTag = match<{ id: string }> ('/tags/:id')
|
const mTag = match<{ id: string }> ('/tags/:id')
|
||||||
|
|
||||||
|
|
||||||
|
const boolFromQuery = (value: string | null): boolean =>
|
||||||
|
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
|
||||||
|
|
||||||
|
|
||||||
const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => {
|
const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => {
|
||||||
const title = url.searchParams.get ('title') ?? ''
|
const title = url.searchParams.get ('title') ?? ''
|
||||||
|
|
||||||
@@ -156,13 +160,16 @@ const prefetchTagsIndex: Prefetcher = async (qc, url) => {
|
|||||||
const createdTo = url.searchParams.get ('created_to') ?? ''
|
const createdTo = url.searchParams.get ('created_to') ?? ''
|
||||||
const updatedFrom = url.searchParams.get ('updated_from') ?? ''
|
const updatedFrom = url.searchParams.get ('updated_from') ?? ''
|
||||||
const updatedTo = url.searchParams.get ('updated_to') ?? ''
|
const updatedTo = url.searchParams.get ('updated_to') ?? ''
|
||||||
|
const deprecated = url.searchParams.has ('deprecated')
|
||||||
|
? boolFromQuery (url.searchParams.get ('deprecated'))
|
||||||
|
: null
|
||||||
const page = Number (url.searchParams.get ('page') || 1)
|
const page = Number (url.searchParams.get ('page') || 1)
|
||||||
const limit = Number (url.searchParams.get ('limit') || 20)
|
const limit = Number (url.searchParams.get ('limit') || 20)
|
||||||
const order = (url.searchParams.get ('order') ?? 'post_count:desc') as FetchTagsOrder
|
const order = (url.searchParams.get ('order') ?? 'post_count:desc') as FetchTagsOrder
|
||||||
|
|
||||||
const keys = {
|
const keys = {
|
||||||
post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
|
post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
|
||||||
updatedFrom, updatedTo, page, limit, order }
|
updatedFrom, updatedTo, deprecated, page, limit, order }
|
||||||
|
|
||||||
await qc.prefetchQuery ({
|
await qc.prefetchQuery ({
|
||||||
queryKey: tagsKeys.index (keys),
|
queryKey: tagsKeys.index (keys),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const baseParams: FetchTagsParams = {
|
|||||||
createdTo: '',
|
createdTo: '',
|
||||||
updatedFrom: '',
|
updatedFrom: '',
|
||||||
updatedTo: '',
|
updatedTo: '',
|
||||||
|
deprecated: null,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
order: 'updated_at:desc',
|
order: 'updated_at:desc',
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import type { Deerjikist,
|
|||||||
|
|
||||||
export const fetchTags = async (
|
export const fetchTags = async (
|
||||||
{ post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
|
{ post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
|
||||||
updatedFrom, updatedTo, page, limit, order }: FetchTagsParams,
|
updatedFrom, updatedTo, deprecated,
|
||||||
|
page, limit, order }: FetchTagsParams,
|
||||||
): Promise<{ tags: Tag[]
|
): Promise<{ tags: Tag[]
|
||||||
count: number }> =>
|
count: number }> =>
|
||||||
await apiGet ('/tags', { params: {
|
await apiGet ('/tags', { params: {
|
||||||
@@ -23,6 +24,7 @@ export const fetchTags = async (
|
|||||||
...(createdTo && { created_to: createdTo }),
|
...(createdTo && { created_to: createdTo }),
|
||||||
...(updatedFrom && { updated_from: updatedFrom }),
|
...(updatedFrom && { updated_from: updatedFrom }),
|
||||||
...(updatedTo && { updated_to: updatedTo }),
|
...(updatedTo && { updated_to: updatedTo }),
|
||||||
|
...(deprecated != null && { deprecated: deprecated ? '1' : '0' }),
|
||||||
...(page && { page }),
|
...(page && { page }),
|
||||||
...(limit && { limit }),
|
...(limit && { limit }),
|
||||||
...(order && { order }) } })
|
...(order && { order }) } })
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ import type { FC, FormEvent } from 'react'
|
|||||||
|
|
||||||
import type { Category, Tag } from '@/types'
|
import type { Category, Tag } from '@/types'
|
||||||
|
|
||||||
type TagFormField = 'name' | 'category' | 'aliases' | 'parentTags'
|
type TagFormField =
|
||||||
|
| 'name'
|
||||||
|
| 'category'
|
||||||
|
| 'aliases'
|
||||||
|
| 'parentTags'
|
||||||
|
| 'deprecated'
|
||||||
|
|
||||||
|
|
||||||
const TagDetailPage: FC = () => {
|
const TagDetailPage: FC = () => {
|
||||||
@@ -35,6 +40,7 @@ const TagDetailPage: FC = () => {
|
|||||||
const [category, setCategory] = useState<Category> ('general')
|
const [category, setCategory] = useState<Category> ('general')
|
||||||
const [aliases, setAliases] = useState ('')
|
const [aliases, setAliases] = useState ('')
|
||||||
const [parentTags, setParentTags] = useState ('')
|
const [parentTags, setParentTags] = useState ('')
|
||||||
|
const [deprecated, setDeprecated] = useState (false)
|
||||||
const [disabled, setDisabled] = useState (true)
|
const [disabled, setDisabled] = useState (true)
|
||||||
const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
|
const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
|
||||||
useValidationErrors<TagFormField> ()
|
useValidationErrors<TagFormField> ()
|
||||||
@@ -50,6 +56,7 @@ const TagDetailPage: FC = () => {
|
|||||||
formData.append ('category', category)
|
formData.append ('category', category)
|
||||||
formData.append ('aliases', aliases)
|
formData.append ('aliases', aliases)
|
||||||
formData.append ('parent_tags', parentTags)
|
formData.append ('parent_tags', parentTags)
|
||||||
|
formData.append ('deprecated', deprecated ? '1' : '0')
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -59,6 +66,7 @@ const TagDetailPage: FC = () => {
|
|||||||
setCategory (data.category as Category)
|
setCategory (data.category as Category)
|
||||||
setAliases (data.aliases.join (' '))
|
setAliases (data.aliases.join (' '))
|
||||||
setParentTags (data.parents.map (t => t.name).join (' '))
|
setParentTags (data.parents.map (t => t.name).join (' '))
|
||||||
|
setDeprecated (Boolean (data.deprecatedAt))
|
||||||
|
|
||||||
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
||||||
qc.invalidateQueries ({ queryKey: tagsKeys.root })
|
qc.invalidateQueries ({ queryKey: tagsKeys.root })
|
||||||
@@ -82,6 +90,7 @@ const TagDetailPage: FC = () => {
|
|||||||
setCategory (tag.category as Category)
|
setCategory (tag.category as Category)
|
||||||
setAliases (tag.aliases.join (' '))
|
setAliases (tag.aliases.join (' '))
|
||||||
setParentTags (tag.parents.map (t => t.name).join (' '))
|
setParentTags (tag.parents.map (t => t.name).join (' '))
|
||||||
|
setDeprecated (Boolean (tag.deprecatedAt))
|
||||||
setDisabled (tag.category === 'nico')
|
setDisabled (tag.category === 'nico')
|
||||||
}, [tag])
|
}, [tag])
|
||||||
|
|
||||||
@@ -165,6 +174,17 @@ const TagDetailPage: FC = () => {
|
|||||||
</>)}
|
</>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="廃止済" messages={fieldErrors.deprecated}>
|
||||||
|
{({ describedBy, invalid }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
disabled={disabled}
|
||||||
|
checked={deprecated}
|
||||||
|
onChange={e => setDeprecated (e.target.checked)}
|
||||||
|
aria-describedby={describedBy}
|
||||||
|
aria-invalid={invalid}/>)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -20,17 +20,28 @@ import type { FC } from 'react'
|
|||||||
|
|
||||||
const renderDiff = (diff: { current: string | null; prev: string | null }) => (
|
const renderDiff = (diff: { current: string | null; prev: string | null }) => (
|
||||||
<>
|
<>
|
||||||
{(diff.prev && diff.prev !== diff.current) && (
|
{diff.prev !== diff.current
|
||||||
|
? (
|
||||||
<>
|
<>
|
||||||
<del className="text-red-600 dark:text-red-400">
|
<del className="text-red-600 dark:text-red-400">
|
||||||
{diff.prev}
|
{diff.prev && <>{diff.prev}<br/></>}
|
||||||
</del>
|
</del>
|
||||||
{diff.current && <br/>}
|
<ins className="text-green-600 dark:text-green-400">
|
||||||
</>)}
|
{diff.current}
|
||||||
{diff.current}
|
</ins>
|
||||||
|
</>)
|
||||||
|
: diff.current}
|
||||||
</>)
|
</>)
|
||||||
|
|
||||||
|
|
||||||
|
const tagStateLabel = (deprecatedAt: string | null) => deprecatedAt ? '廃止' : ''
|
||||||
|
|
||||||
|
|
||||||
|
const renderStateDiff = (diff: { current: string | null; prev: string | null }) =>
|
||||||
|
renderDiff ({ current: tagStateLabel (diff.current),
|
||||||
|
prev: tagStateLabel (diff.prev) })
|
||||||
|
|
||||||
|
|
||||||
const TagHistoryPage: FC = () => {
|
const TagHistoryPage: FC = () => {
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
const query = new URLSearchParams (location.search)
|
const query = new URLSearchParams (location.search)
|
||||||
@@ -72,6 +83,8 @@ const TagHistoryPage: FC = () => {
|
|||||||
<col className="w-96"/>
|
<col className="w-96"/>
|
||||||
{/* カテゴリ */}
|
{/* カテゴリ */}
|
||||||
<col className="w-96"/>
|
<col className="w-96"/>
|
||||||
|
{/* 状態 */}
|
||||||
|
<col className="w-32"/>
|
||||||
{/* 別名 */}
|
{/* 別名 */}
|
||||||
<col className="w-[48rem]"/>
|
<col className="w-[48rem]"/>
|
||||||
{/* 上位タグ */}
|
{/* 上位タグ */}
|
||||||
@@ -87,6 +100,7 @@ const TagHistoryPage: FC = () => {
|
|||||||
<th className="p-2 text-left">版</th>
|
<th className="p-2 text-left">版</th>
|
||||||
<th className="p-2 text-left">名称</th>
|
<th className="p-2 text-left">名称</th>
|
||||||
<th className="p-2 text-left">カテゴリ</th>
|
<th className="p-2 text-left">カテゴリ</th>
|
||||||
|
<th className="p-2 text-left">状態</th>
|
||||||
<th className="p-2 text-left">別名</th>
|
<th className="p-2 text-left">別名</th>
|
||||||
<th className="p-2 text-left">上位タグ</th>
|
<th className="p-2 text-left">上位タグ</th>
|
||||||
<th className="p-2 text-left">更新日時</th>
|
<th className="p-2 text-left">更新日時</th>
|
||||||
@@ -106,6 +120,9 @@ const TagHistoryPage: FC = () => {
|
|||||||
prev: (change.category.prev
|
prev: (change.category.prev
|
||||||
&& CATEGORY_NAMES[change.category.prev]) })}
|
&& CATEGORY_NAMES[change.category.prev]) })}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="p-2 break-all">
|
||||||
|
{renderStateDiff (change.deprecatedAt)}
|
||||||
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
{change.aliases.map ((tag, i) => (
|
{change.aliases.map ((tag, i) => (
|
||||||
tag.type === 'added'
|
tag.type === 'added'
|
||||||
@@ -178,6 +195,7 @@ const TagHistoryPage: FC = () => {
|
|||||||
`/tags/${ change.tagId }`,
|
`/tags/${ change.tagId }`,
|
||||||
{ name: change.name.current,
|
{ name: change.name.current,
|
||||||
category: change.category.current,
|
category: change.category.current,
|
||||||
|
deprecated: change.deprecatedAt.current ? '1' : '0',
|
||||||
aliases:
|
aliases:
|
||||||
change.aliases
|
change.aliases
|
||||||
.filter (t => t.type !== 'removed')
|
.filter (t => t.type !== 'removed')
|
||||||
@@ -211,4 +229,5 @@ const TagHistoryPage: FC = () => {
|
|||||||
</MainArea>)
|
</MainArea>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TagHistoryPage
|
|
||||||
|
export default TagHistoryPage
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ const setIf = (qs: URLSearchParams, k: string, v: string | null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const boolFromQuery = (value: string | null): boolean =>
|
||||||
|
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
|
||||||
|
|
||||||
|
|
||||||
|
const tagStateLabel = (deprecatedAt: string | null) => deprecatedAt ? '廃止' : ''
|
||||||
|
|
||||||
|
|
||||||
const TagListPage: FC = () => {
|
const TagListPage: FC = () => {
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
|
|
||||||
@@ -48,6 +55,9 @@ const TagListPage: FC = () => {
|
|||||||
const qCreatedTo = query.get ('created_to') ?? ''
|
const qCreatedTo = query.get ('created_to') ?? ''
|
||||||
const qUpdatedFrom = query.get ('updated_from') ?? ''
|
const qUpdatedFrom = query.get ('updated_from') ?? ''
|
||||||
const qUpdatedTo = query.get ('updated_to') ?? ''
|
const qUpdatedTo = query.get ('updated_to') ?? ''
|
||||||
|
const qDeprecated = query.has ('deprecated')
|
||||||
|
? boolFromQuery (query.get ('deprecated'))
|
||||||
|
: null
|
||||||
const order = (query.get ('order') || 'post_count:desc') as FetchTagsOrder
|
const order = (query.get ('order') || 'post_count:desc') as FetchTagsOrder
|
||||||
|
|
||||||
const [name, setName] = useState ('')
|
const [name, setName] = useState ('')
|
||||||
@@ -58,6 +68,7 @@ const TagListPage: FC = () => {
|
|||||||
const [createdTo, setCreatedTo] = useState<string | null> (null)
|
const [createdTo, setCreatedTo] = useState<string | null> (null)
|
||||||
const [updatedFrom, setUpdatedFrom] = useState<string | null> (null)
|
const [updatedFrom, setUpdatedFrom] = useState<string | null> (null)
|
||||||
const [updatedTo, setUpdatedTo] = useState<string | null> (null)
|
const [updatedTo, setUpdatedTo] = useState<string | null> (null)
|
||||||
|
const [deprecated, setDeprecated] = useState<boolean | null> (null)
|
||||||
|
|
||||||
const keys = {
|
const keys = {
|
||||||
page, limit, order,
|
page, limit, order,
|
||||||
@@ -69,7 +80,8 @@ const TagListPage: FC = () => {
|
|||||||
createdFrom: qCreatedFrom,
|
createdFrom: qCreatedFrom,
|
||||||
createdTo: qCreatedTo,
|
createdTo: qCreatedTo,
|
||||||
updatedFrom: qUpdatedFrom,
|
updatedFrom: qUpdatedFrom,
|
||||||
updatedTo: qUpdatedTo }
|
updatedTo: qUpdatedTo,
|
||||||
|
deprecated: qDeprecated }
|
||||||
const { data, isLoading: loading } = useQuery ({
|
const { data, isLoading: loading } = useQuery ({
|
||||||
queryKey: tagsKeys.index (keys),
|
queryKey: tagsKeys.index (keys),
|
||||||
queryFn: () => fetchTags (keys) })
|
queryFn: () => fetchTags (keys) })
|
||||||
@@ -85,10 +97,11 @@ const TagListPage: FC = () => {
|
|||||||
setCreatedTo (qCreatedTo)
|
setCreatedTo (qCreatedTo)
|
||||||
setUpdatedFrom (qUpdatedFrom)
|
setUpdatedFrom (qUpdatedFrom)
|
||||||
setUpdatedTo (qUpdatedTo)
|
setUpdatedTo (qUpdatedTo)
|
||||||
|
setDeprecated (qDeprecated)
|
||||||
|
|
||||||
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' })
|
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' })
|
||||||
}, [location.search, qCategory, qCreatedFrom, qCreatedTo, qName, qPostCountGTE,
|
}, [location.search, qCategory, qCreatedFrom, qCreatedTo, qName, qPostCountGTE,
|
||||||
qPostCountLTE, qUpdatedFrom, qUpdatedTo])
|
qPostCountLTE, qUpdatedFrom, qUpdatedTo, qDeprecated])
|
||||||
|
|
||||||
const handleSearch = (e: FormEvent) => {
|
const handleSearch = (e: FormEvent) => {
|
||||||
e.preventDefault ()
|
e.preventDefault ()
|
||||||
@@ -104,6 +117,8 @@ const TagListPage: FC = () => {
|
|||||||
setIf (qs, 'created_to', createdTo)
|
setIf (qs, 'created_to', createdTo)
|
||||||
setIf (qs, 'updated_from', updatedFrom)
|
setIf (qs, 'updated_from', updatedFrom)
|
||||||
setIf (qs, 'updated_to', updatedTo)
|
setIf (qs, 'updated_to', updatedTo)
|
||||||
|
if (deprecated != null)
|
||||||
|
qs.set ('deprecated', deprecated ? '1' : '0')
|
||||||
qs.set ('page', '1')
|
qs.set ('page', '1')
|
||||||
qs.set ('order', order)
|
qs.set ('order', order)
|
||||||
|
|
||||||
@@ -201,6 +216,21 @@ const TagListPage: FC = () => {
|
|||||||
</>)}
|
</>)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="状態">
|
||||||
|
{({ invalid }) => (
|
||||||
|
<select
|
||||||
|
value={deprecated == null ? '' : (deprecated ? '1' : '0')}
|
||||||
|
onChange={e => setDeprecated (
|
||||||
|
e.target.value === ''
|
||||||
|
? null
|
||||||
|
: e.target.value === '1')}
|
||||||
|
className={inputClass (invalid)}>
|
||||||
|
<option value=""> </option>
|
||||||
|
<option value="0">有効</option>
|
||||||
|
<option value="1">廃止</option>
|
||||||
|
</select>)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -219,6 +249,7 @@ const TagListPage: FC = () => {
|
|||||||
<col className="w-72"/>
|
<col className="w-72"/>
|
||||||
<col className="w-16"/>
|
<col className="w-16"/>
|
||||||
<col className="w-48"/>
|
<col className="w-48"/>
|
||||||
|
<col className="w-32"/>
|
||||||
<col className="w-72"/>
|
<col className="w-72"/>
|
||||||
<col className="w-48"/>
|
<col className="w-48"/>
|
||||||
<col className="w-56"/>
|
<col className="w-56"/>
|
||||||
@@ -249,6 +280,7 @@ const TagListPage: FC = () => {
|
|||||||
currentOrder={order}
|
currentOrder={order}
|
||||||
defaultDirection={defaultDirection}/>
|
defaultDirection={defaultDirection}/>
|
||||||
</th>
|
</th>
|
||||||
|
<th className="p-2 text-left whitespace-nowrap">状態</th>
|
||||||
<th className="p-2 text-left whitespace-nowrap">別名</th>
|
<th className="p-2 text-left whitespace-nowrap">別名</th>
|
||||||
<th className="p-2 text-left whitespace-nowrap">上位タグ</th>
|
<th className="p-2 text-left whitespace-nowrap">上位タグ</th>
|
||||||
<th className="p-2 text-left whitespace-nowrap">
|
<th className="p-2 text-left whitespace-nowrap">
|
||||||
@@ -280,6 +312,7 @@ const TagListPage: FC = () => {
|
|||||||
</td>
|
</td>
|
||||||
<td className="p-2 text-right">{row.postCount}</td>
|
<td className="p-2 text-right">{row.postCount}</td>
|
||||||
<td className="p-2">{CATEGORY_NAMES[row.category]}</td>
|
<td className="p-2">{CATEGORY_NAMES[row.category]}</td>
|
||||||
|
<td className="p-2">{tagStateLabel (row.deprecatedAt)}</td>
|
||||||
<td className="p-2">{row.aliases.join (' ')}</td>
|
<td className="p-2">{row.aliases.join (' ')}</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
{row.parents.map (t => (
|
{row.parents.map (t => (
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const buildTag = (overrides: Partial<Tag> = {}): Tag => ({
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'テストタグ',
|
name: 'テストタグ',
|
||||||
category: 'general',
|
category: 'general',
|
||||||
|
deprecatedAt: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
parents: [],
|
parents: [],
|
||||||
postCount: 12,
|
postCount: 12,
|
||||||
|
|||||||
+15
-12
@@ -39,18 +39,19 @@ export type FetchTagsOrderField =
|
|||||||
| 'updated_at'
|
| 'updated_at'
|
||||||
|
|
||||||
export type FetchTagsParams = {
|
export type FetchTagsParams = {
|
||||||
post: number | null
|
post: number | null
|
||||||
name: string
|
name: string
|
||||||
category: Category | null
|
category: Category | null
|
||||||
postCountGTE: number
|
postCountGTE: number
|
||||||
postCountLTE: number | null
|
postCountLTE: number | null
|
||||||
createdFrom: string
|
createdFrom: string
|
||||||
createdTo: string
|
createdTo: string
|
||||||
updatedFrom: string
|
updatedFrom: string
|
||||||
updatedTo: string
|
updatedTo: string
|
||||||
page: number
|
deprecated: boolean | null
|
||||||
limit: number
|
page: number
|
||||||
order: FetchTagsOrder }
|
limit: number
|
||||||
|
order: FetchTagsOrder }
|
||||||
|
|
||||||
export type FetchNicoTagsParams = {
|
export type FetchNicoTagsParams = {
|
||||||
name: string
|
name: string
|
||||||
@@ -196,6 +197,7 @@ export type Tag = {
|
|||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
category: Category
|
category: Category
|
||||||
|
deprecatedAt: string | null
|
||||||
aliases: string[]
|
aliases: string[]
|
||||||
parents: Tag[]
|
parents: Tag[]
|
||||||
postCount: number
|
postCount: number
|
||||||
@@ -213,6 +215,7 @@ export type TagVersion = {
|
|||||||
eventType: 'create' | 'update' | 'discard' | 'restore'
|
eventType: 'create' | 'update' | 'discard' | 'restore'
|
||||||
name: { current: string; prev: string | null }
|
name: { current: string; prev: string | null }
|
||||||
category: { current: Category; prev: Category | null }
|
category: { current: Category; prev: Category | null }
|
||||||
|
deprecatedAt: { current: string | null; prev: string | null }
|
||||||
aliases: { name: string; type: 'context' | 'added' | 'removed' }[]
|
aliases: { name: string; type: 'context' | 'added' | 'removed' }[]
|
||||||
parentTags: { tag: Tag; type: 'context' | 'added' | 'removed' }[]
|
parentTags: { tag: Tag; type: 'context' | 'added' | 'removed' }[]
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする