| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
772c66aa64 | #171 | 1 day ago |
|
|
d54e66a114 | #171 | 2 days ago |
| @@ -173,15 +173,41 @@ class PostsController < ApplicationController | |||||
| return head :unauthorized unless current_user | return head :unauthorized unless current_user | ||||
| return head :forbidden unless current_user.gte_member? | return head :forbidden unless current_user.gte_member? | ||||
| base_version_no = parse_base_version_no | |||||
| force = truthy_param?(params[:force]) | |||||
| title = params[:title].presence | title = params[:title].presence | ||||
| tag_names = params[:tags].to_s.split | tag_names = params[:tags].to_s.split | ||||
| original_created_from = params[:original_created_from] | original_created_from = params[:original_created_from] | ||||
| original_created_before = params[:original_created_before] | original_created_before = params[:original_created_before] | ||||
| parent_post_ids = parse_parent_post_ids | parent_post_ids = parse_parent_post_ids | ||||
| post = Post.find(params[:id].to_i) | |||||
| post = nil | |||||
| conflict_json = nil | |||||
| ApplicationRecord.transaction do | ApplicationRecord.transaction do | ||||
| post = Post.find(params[:id].to_i) | |||||
| base_version = post.post_versions.find_by!(version_no: base_version_no) | |||||
| base_snapshot = post_snapshot_from_version(base_version) | |||||
| current_snapshot = post_snapshot_from_record(post) | |||||
| incoming_snapshot = post_incoming_snapshot(post, | |||||
| title:, | |||||
| original_created_from:, | |||||
| original_created_before:, | |||||
| tag_names:, | |||||
| parent_post_ids:) | |||||
| if !(force) && post.version_no != base_version_no | |||||
| conflict_json = post_conflict_json(post:, | |||||
| base_version_no:, | |||||
| base_snapshot:, | |||||
| current_snapshot:, | |||||
| incoming_snapshot:) | |||||
| raise ActiveRecord::Rollback | |||||
| end | |||||
| PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user) | PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user) | ||||
| post.update!(title:, original_created_from:, original_created_before:) | post.update!(title:, original_created_from:, original_created_before:) | ||||
| @@ -198,8 +224,10 @@ class PostsController < ApplicationController | |||||
| PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user) | PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user) | ||||
| end | end | ||||
| return render json: conflict_json, status: :conflict if conflict_json | |||||
| post.reload | post.reload | ||||
| json = post.as_json | |||||
| json = PostRepr.base(post, current_user) | |||||
| json['tags'] = build_tag_tree_for(post.tags) | json['tags'] = build_tag_tree_for(post.tags) | ||||
| render json:, status: :ok | render json:, status: :ok | ||||
| rescue Tag::NicoTagNormalisationError | rescue Tag::NicoTagNormalisationError | ||||
| @@ -404,4 +432,178 @@ class PostsController < ApplicationController | |||||
| PostImplication.create_or_find_by!(post_id: post.id, parent_post_id:) | PostImplication.create_or_find_by!(post_id: post.id, parent_post_id:) | ||||
| end | end | ||||
| end | end | ||||
| def parse_base_version_no | |||||
| version_no = Integer(params[:base_version_no], exception: false) | |||||
| raise ArgumentError, 'base_version_no は必須です.' unless version_no&.positive? | |||||
| version_no | |||||
| end | |||||
| def truthy_param?(value) = ActiveModel::Type::Boolean.new.cast(value) | |||||
| def post_snapshot_from_version version | |||||
| { title: version.title, | |||||
| original_created_from: snapshot_time(version.original_created_from), | |||||
| original_created_before: snapshot_time(version.original_created_before), | |||||
| tag_names: version.tags.to_s.split.sort, | |||||
| parent_post_ids: snapshot_parent_post_ids_from_version(version) } | |||||
| end | |||||
| def post_snapshot_form_record post | |||||
| { title: post.title, | |||||
| original_created_from: snapshot_time(post.original_created_from), | |||||
| original_created_before: snapshot_time(post.original_created_before), | |||||
| tag_names: post.tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name'), | |||||
| parent_post_ids: post.parent_posts.order(:id).pluck(:id) } | |||||
| end | |||||
| def post_incoming_snapshot post, title:, original_created_from:, original_created_before:, | |||||
| tag_names:, parent_post_ids: | |||||
| { title: | |||||
| original_created_from: snapshot_time(original_created_from), | |||||
| original_created_before: snapshot_time(original_created_before), | |||||
| tag_names: incoming_tag_names_for_snapshot(post, tag_names), | |||||
| parent_post_ids: parent_post_ids.sort } | |||||
| end | |||||
| def snapshot_parent_post_ids_from_version version | |||||
| if version.respond_to?(:parent_post_ids) | |||||
| version.parent_post_ids.to_s.split.map { |id| id.to_i }.sort | |||||
| elsif version.respond_to?(:parent_id) && version.parent_id | |||||
| [version.parent_id] | |||||
| else | |||||
| [] | |||||
| end | |||||
| end | |||||
| def snapshot_time value | |||||
| return nil if value.blank? | |||||
| value = Time.zone.parse(value.to_s) if value in String | |||||
| value&.in_time_zone&.iso8601(6) | |||||
| rescue ArgumentError, TypeError | |||||
| value.to_s | |||||
| end | |||||
| def incoming_tag_names_for_snapshot post, raw_tag_names | |||||
| manual_names = normalised_manual_tag_names_for_snapshot(raw_tag_names) | |||||
| nico_names = post.tags.nico.joins(:tag_name).pluck('tag_names.name') | |||||
| existing_tags = | |||||
| Tag | |||||
| .joins(:tag_name) | |||||
| .where(tag_names: { name: manual_names + nico_names }) | |||||
| .to_a | |||||
| expanded_names = Tag.expand_parent_tags(existing_tags).map(&:name) | |||||
| (manual_names + nico_names + expanded_names).uniq.sort | |||||
| end | |||||
| def normalised_manual_tag_names_for_snapshot raw_tag_names | |||||
| if raw_tag_names.any? { |name| name.downcase.start_with?('nico:') } | |||||
| raise Tag::NicoTagNormalisationError | |||||
| end | |||||
| pairs = raw_tag_names.map do |raw_name| | |||||
| prefix, category = | |||||
| Tag::CATEGORY_PREFIXES.find { |p, _| raw_name.downcase.start_with?(p) } || ['', nil] | |||||
| name = TagName.canonicalise(raw_name.sub(/\A#{ Regexp.escape(prefix) }/i, '')).first | |||||
| [name, category] | |||||
| end | |||||
| names = pairs.map(&:first) | |||||
| has_deerjikist = pairs.any? do |name, category| | |||||
| category == :deerjikist || | |||||
| Tag.joins(:tag_name).where(category: :deerjikist, tag_names: { name: }).exists? | |||||
| end | |||||
| names << Tag.no_deerjikist.name unless has_deerjikist | |||||
| names.uniq.sort | |||||
| end | |||||
| def post_conflict_json post:, base_version_no:, base_snapshot:, | |||||
| current_snapshot:, incoming_snapshot: | |||||
| changes = post_snapshot_changes(base_snapshot, current_snapshot, incoming_snapshot) | |||||
| conflicts = changes.select { |change| change[:conflict] } | |||||
| { error: 'conflict', | |||||
| message: '競合が発生しました.', | |||||
| post_id: post.id, | |||||
| base_version_no:, | |||||
| current_version_no: post.version_no, | |||||
| base: base_snapshot, | |||||
| current: current_snapshot, | |||||
| mine: incoming_snapshot, | |||||
| changes:, | |||||
| conflicts:, | |||||
| mergeable: conflicts.empty? } | |||||
| end | |||||
| def post_snapshot_changes base_snapshot, current_snapshot, incoming_snapshot | |||||
| [scalar_snapshot_change(:title, 'タイトル', | |||||
| base_snapshot, current_snapshot, incoming_snapshot), | |||||
| scalar_snapshot_change(:original_created_from, '元コンテンツ作成日時(開始)', | |||||
| base_snapshot, current_snapshot, incoming_snapshot), | |||||
| scalar_snapshot_change(:original_created_before, '元コンテンツ作成日時(終了)', | |||||
| base_snapshot, current_snapshot, incoming_snapshot), | |||||
| set_snapshot_change(:tag_names, 'タグ', | |||||
| base_snapshot, current_snapshot, incoming_snapshot), | |||||
| set_snapshot_change(:parent_post_ids, '親投稿', | |||||
| base_snapshot, current_snapshot, incoming_snapshot)].compact | |||||
| end | |||||
| def scalar_snapshot_change field, label, base_snapshot, current_snapshot, incoming_snapshot | |||||
| base = base_snapshot[field] | |||||
| current = current_snapshot[field] | |||||
| mine = incoming_snapshot[field] | |||||
| return nil if current == base && mine == base | |||||
| { field:, label:, base:, current:, mine:, | |||||
| changed_by_current: current != base, | |||||
| changed_by_me: mine != base, | |||||
| conflict: scalar_snapshot_conflict?(base, current, mine) } | |||||
| end | |||||
| def scalar_snapshot_conflict? base, current, mine | |||||
| current != base && mine != base && current != mine | |||||
| end | |||||
| def set_snapshot_change field, label, base_snapshot, current_snapshot, incoming_snapshot | |||||
| base = base_snapshot[field].to_a | |||||
| current = current_snapshot[field].to_a | |||||
| mine = incoming_snapshot[field].to_a | |||||
| added_by_current = current - base | |||||
| removed_by_current = base - current | |||||
| added_by_me = mine - base | |||||
| removed_by_me = base - mine | |||||
| if (added_by_current.empty? && | |||||
| removed_by_current.empty? && | |||||
| added_by_me.empty? && | |||||
| removed_by_me.empty?) | |||||
| return nil | |||||
| end | |||||
| { field:, label:, base:, current:, mine:, added_by_current:, removed_by_current:, | |||||
| added_by_me:, removed_by_me:, | |||||
| changed_by_current: added_by_current.present? || removed_by_current.present?, | |||||
| changed_by_me: added_by_me.present? || removed_by_me.present?, | |||||
| conflict: set_snapshot_conflict?(added_by_current:, | |||||
| removed_by_current:, | |||||
| added_by_me:, | |||||
| removed_by_me:) } | |||||
| end | |||||
| def set_snapshot_conflict? added_by_current:, removed_by_current:, | |||||
| added_by_me:, removed_by_me: | |||||
| (added_by_current & removed_by_me).present? || (removed_by_current & added_by_me).present? | |||||
| end | |||||
| end | end | ||||
| @@ -16,19 +16,20 @@ class VersionRecorder | |||||
| @record = record_class.unscoped.lock.find(@record.id) | @record = record_class.unscoped.lock.find(@record.id) | ||||
| latest = latest_version | latest = latest_version | ||||
| if !(latest) && @event_type != 'create' | |||||
| raise "#{ version_class.name } first event must be create" | |||||
| end | |||||
| validate_version_sequence! latest | |||||
| attrs = snapshot_attributes | |||||
| if @event_type == 'create' && latest | |||||
| raise "#{ version_class.name } create event already exists" | |||||
| if @event_type == 'update' && latest && same_snapshot?(latest, attrs) | |||||
| return latest | |||||
| end | end | ||||
| attrs = snapshot_attributes | |||||
| version = version_class.create!( | |||||
| base_attributes(latest).merge(record_key => @record).merge(attrs)) | |||||
| return latest if @event_type == 'update' && latest && same_snapshot?(latest, attrs) | |||||
| update_record_version_no! version.version_no | |||||
| version_class.create!(base_attributes(latest).merge(record_key => @record).merge(attrs)) | |||||
| version | |||||
| end | end | ||||
| end | end | ||||
| @@ -45,7 +46,31 @@ class VersionRecorder | |||||
| created_by_user: @created_by_user } | created_by_user: @created_by_user } | ||||
| end | end | ||||
| def same_snapshot?(version, attrs) = attrs.all? { |k, v| version.public_send(k) == v } | |||||
| def update_record_version_no! version_no | |||||
| @record.update_columns version_no: version_no | |||||
| @record.version_no = version_no | |||||
| end | |||||
| def validate_version_sequence! latest | |||||
| if !(latest) && @event_type != 'create' | |||||
| raise "#{ version_class.name } first event must be create" | |||||
| end | |||||
| if @event_type == 'create' && latest | |||||
| raise "#{ version_class.name } create event already exists" | |||||
| end | |||||
| return unless latest | |||||
| if @record.version_no != latest.version_no | |||||
| raise ("#{ record_class.name }##{ @record.id } version_no is #{ @record.version_no }, " + | |||||
| "but latest #{ version_class.name } version_no is #{ latest.version_no }") | |||||
| end | |||||
| end | |||||
| def same_snapshot? version, attrs | |||||
| attrs.all? { |k, v| version.public_send(k) == v } | |||||
| end | |||||
| def validate_event_type! | def validate_event_type! | ||||
| return if EVENT_TYPES.include?(@event_type) | return if EVENT_TYPES.include?(@event_type) | ||||
| @@ -0,0 +1,27 @@ | |||||
| class AddVersionNoToPosts < ActiveRecord::Migration[8.0] | |||||
| def up | |||||
| add_column :posts, :version_no, :integer | |||||
| execute <<~SQL | |||||
| UPDATE | |||||
| posts | |||||
| SET | |||||
| version_no = ( | |||||
| SELECT | |||||
| MAX(version_no) | |||||
| FROM | |||||
| post_versions | |||||
| WHERE | |||||
| post_id = posts.id) | |||||
| SQL | |||||
| change_column_null :posts, :version_no, false | |||||
| add_check_constraint :posts, 'version_no > 0', name: 'chk_posts_version_no_positive' | |||||
| end | |||||
| def down | |||||
| remove_check_constraint :posts, name: 'chk_posts_version_no_positive' | |||||
| remove_column :posts, :version_no | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,37 @@ | |||||
| class AddVersionNoToTags < ActiveRecord::Migration[8.0] | |||||
| def up | |||||
| add_column :tags, :version_no, :integer | |||||
| execute <<~SQL | |||||
| UPDATE | |||||
| tags | |||||
| SET | |||||
| version_no = ( | |||||
| CASE category | |||||
| WHEN 'nico' THEN | |||||
| (SELECT | |||||
| MAX(version_no) | |||||
| FROM | |||||
| nico_tag_versions | |||||
| WHERE | |||||
| tag_id = tags.id) | |||||
| ELSE | |||||
| (SELECT | |||||
| MAX(version_no) | |||||
| FROM | |||||
| tag_versions | |||||
| WHERE | |||||
| tag_id = tags.id) | |||||
| END) | |||||
| SQL | |||||
| change_column_null :tags, :version_no, false | |||||
| add_check_constraint :tags, 'version_no > 0', name: 'chk_tags_version_no_positive' | |||||
| end | |||||
| def down | |||||
| remove_check_constraint :tags, name: 'chk_tags_version_no_positive' | |||||
| remove_column :tags, :version_no | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,27 @@ | |||||
| class AddVersionNoToWikiPages < ActiveRecord::Migration[8.0] | |||||
| def up | |||||
| add_column :wiki_pages, :version_no, :integer | |||||
| execute <<~SQL | |||||
| UPDATE | |||||
| wiki_pages | |||||
| SET | |||||
| version_no = ( | |||||
| SELECT | |||||
| MAX(version_no) | |||||
| FROM | |||||
| wiki_versions | |||||
| WHERE | |||||
| wiki_page_id = wiki_pages.id) | |||||
| SQL | |||||
| change_column_null :wiki_pages, :version_no, false | |||||
| add_check_constraint :wiki_pages, 'version_no > 0', name: 'chk_wiki_pages_version_no_positive' | |||||
| end | |||||
| def down | |||||
| remove_check_constraint :wiki_pages, name: 'chk_wiki_pages_version_no_positive' | |||||
| remove_column :wiki_pages, :version_no | |||||
| end | |||||
| end | |||||
| @@ -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_05_01_153900) do | |||||
| ActiveRecord::Schema[8.0].define(version: 2026_05_07_213300) 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 | ||||
| @@ -186,8 +186,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_01_153900) do | |||||
| t.datetime "original_created_from" | t.datetime "original_created_from" | ||||
| t.datetime "original_created_before" | t.datetime "original_created_before" | ||||
| t.datetime "updated_at", null: false | t.datetime "updated_at", null: false | ||||
| t.integer "version_no", null: false | |||||
| t.index ["uploaded_user_id"], name: "index_posts_on_uploaded_user_id" | t.index ["uploaded_user_id"], name: "index_posts_on_uploaded_user_id" | ||||
| t.index ["url"], name: "index_posts_on_url", unique: true | t.index ["url"], name: "index_posts_on_url", unique: true | ||||
| t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive" | |||||
| end | end | ||||
| create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | ||||
| @@ -262,8 +264,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_01_153900) do | |||||
| 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 "discarded_at" | t.datetime "discarded_at" | ||||
| t.integer "version_no", null: false | |||||
| 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 "`version_no` > 0", name: "chk_tags_version_no_positive" | |||||
| end | end | ||||
| create_table "theatre_comments", primary_key: ["theatre_id", "no"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | create_table "theatre_comments", primary_key: ["theatre_id", "no"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | ||||
| @@ -369,10 +373,12 @@ ActiveRecord::Schema[8.0].define(version: 2026_05_01_153900) do | |||||
| t.datetime "updated_at", null: false | t.datetime "updated_at", null: false | ||||
| t.datetime "discarded_at" | t.datetime "discarded_at" | ||||
| t.integer "next_asset_no", default: 1, null: false | t.integer "next_asset_no", default: 1, null: false | ||||
| t.integer "version_no", null: false | |||||
| t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id" | t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id" | ||||
| t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at" | t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at" | ||||
| t.index ["tag_name_id"], name: "index_wiki_pages_on_tag_name_id", unique: true | t.index ["tag_name_id"], name: "index_wiki_pages_on_tag_name_id", unique: true | ||||
| t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id" | t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id" | ||||
| t.check_constraint "`version_no` > 0", name: "chk_wiki_pages_version_no_positive" | |||||
| end | end | ||||
| create_table "wiki_revision_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | create_table "wiki_revision_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| | ||||
| @@ -5,7 +5,7 @@ import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeFi | |||||
| import Label from '@/components/common/Label' | import Label from '@/components/common/Label' | ||||
| import { Button } from '@/components/ui/button' | import { Button } from '@/components/ui/button' | ||||
| import { toast } from '@/components/ui/use-toast' | import { toast } from '@/components/ui/use-toast' | ||||
| import { apiPut } from '@/lib/api' | |||||
| import { updatePost } from '@/lib/posts' | |||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| @@ -44,19 +44,17 @@ export default (({ post, onSave }: Props) => { | |||||
| const handleSubmit = async () => { | const handleSubmit = async () => { | ||||
| try | try | ||||
| { | { | ||||
| const data = await apiPut<Post> ( | |||||
| `/posts/${ post.id }`, | |||||
| { title, tags, parent_post_ids: parentPostIds, | |||||
| original_created_from: originalCreatedFrom, | |||||
| original_created_before: originalCreatedBefore }, | |||||
| { headers: { 'Content-Type': 'multipart/form-data' } }) | |||||
| const data = | |||||
| await updatePost ({ id: post.id, versionNo: post.versionNo + 1, | |||||
| title, tags, parentPostIds, | |||||
| originalCreatedFrom, originalCreatedBefore }) | |||||
| onSave ({ ...post, | onSave ({ ...post, | ||||
| title: data.title, | title: data.title, | ||||
| tags: data.tags, | tags: data.tags, | ||||
| parentPosts: data.parentPosts, | parentPosts: data.parentPosts, | ||||
| childPosts: data.childPosts, | childPosts: data.childPosts, | ||||
| siblingPosts: data.siblingPosts, | siblingPosts: data.siblingPosts, | ||||
| originalCreatedFrom: data.originalCreatedFrom, | |||||
| originalCreatedFrom: data.originalCreatedFrom, | |||||
| originalCreatedBefore: data.originalCreatedBefore } as Post) | originalCreatedBefore: data.originalCreatedBefore } as Post) | ||||
| toast ({ description: '更新しました.' }) | toast ({ description: '更新しました.' }) | ||||
| } | } | ||||
| @@ -1,4 +1,4 @@ | |||||
| import { apiDelete, apiGet, apiPost } from '@/lib/api' | |||||
| import { apiDelete, apiGet, apiPost, apiPut } from '@/lib/api' | |||||
| import type { FetchPostsParams, Post, PostVersion } from '@/types' | import type { FetchPostsParams, Post, PostVersion } from '@/types' | ||||
| @@ -42,6 +42,25 @@ export const fetchPostChanges = async ( | |||||
| page, limit } }) | page, limit } }) | ||||
| export const updatePost = async ( | |||||
| post: { id: number | |||||
| versionNo: number | |||||
| title: string | null | |||||
| tags: string | |||||
| parentPostIds: string | |||||
| originalCreatedFrom: string | null | |||||
| originalCreatedBefore: string | null }, | |||||
| ) => | |||||
| await apiPut<Post> ( | |||||
| `/posts/${ post.id }`, | |||||
| { version_no: post.versionNo, | |||||
| title: post.title, | |||||
| tags: post.tags, | |||||
| parent_post_ids: post.parentPostIds, | |||||
| original_created_from: post.originalCreatedFrom, | |||||
| original_created_before: post.originalCreatedBefore }) | |||||
| export const toggleViewedFlg = async (id: string, viewed: boolean): Promise<void> => { | export const toggleViewedFlg = async (id: string, viewed: boolean): Promise<void> => { | ||||
| await (viewed ? apiPost : apiDelete) (`/posts/${ id }/viewed`) | await (viewed ? apiPost : apiDelete) (`/posts/${ id }/viewed`) | ||||
| } | } | ||||
| @@ -11,13 +11,14 @@ import Pagination from '@/components/common/Pagination' | |||||
| import MainArea from '@/components/layout/MainArea' | import MainArea from '@/components/layout/MainArea' | ||||
| import { toast } from '@/components/ui/use-toast' | import { toast } from '@/components/ui/use-toast' | ||||
| import { SITE_TITLE } from '@/config' | import { SITE_TITLE } from '@/config' | ||||
| import { apiPut } from '@/lib/api' | |||||
| import { fetchPostChanges } from '@/lib/posts' | |||||
| import { fetchPostChanges, updatePost } from '@/lib/posts' | |||||
| import { postsKeys, tagsKeys } from '@/lib/queryKeys' | import { postsKeys, tagsKeys } from '@/lib/queryKeys' | ||||
| import { fetchTag } from '@/lib/tags' | import { fetchTag } from '@/lib/tags' | ||||
| import { cn, dateString, originalCreatedAtString } from '@/lib/utils' | import { cn, dateString, originalCreatedAtString } from '@/lib/utils' | ||||
| import type { FC } from 'react' | |||||
| import type { FC, MouseEvent } from 'react' | |||||
| import type { PostVersion } from '@/types' | |||||
| const renderDiff = (diff: { current: string | null; prev: string | null }) => ( | const renderDiff = (diff: { current: string | null; prev: string | null }) => ( | ||||
| @@ -62,6 +63,45 @@ export default (() => { | |||||
| const qc = useQueryClient () | const qc = useQueryClient () | ||||
| const handleRevert = async (e: MouseEvent<HTMLAnchorElement>, change: PostVersion) => { | |||||
| e.preventDefault () | |||||
| if (!(confirm (`『${ change.title.current || change.url.current }』を版 ${ | |||||
| change.versionNo } に差戻します.\nよろしいですか?`))) | |||||
| return | |||||
| try | |||||
| { | |||||
| const id = change.postId | |||||
| const versionNo = change.latestVersionNo + 1 | |||||
| const title = change.title.current | |||||
| const tags = | |||||
| change.tags | |||||
| .filter (t => t.type !== 'removed') | |||||
| .map (t => t.name) | |||||
| .filter (t => t.slice (0, 5) !== 'nico:') | |||||
| .join (' ') | |||||
| const parentPostIds = | |||||
| (change.parentPosts ?? []) | |||||
| .filter (p => p.type !== 'removed') | |||||
| .map (p => p.id) | |||||
| .join (' ') | |||||
| const originalCreatedFrom = change.originalCreatedFrom.current | |||||
| const originalCreatedBefore = change.originalCreatedBefore.current | |||||
| await updatePost ({ id, versionNo, title, tags, parentPostIds, | |||||
| originalCreatedFrom, originalCreatedBefore }) | |||||
| qc.invalidateQueries ({ queryKey: postsKeys.root }) | |||||
| qc.invalidateQueries ({ queryKey: tagsKeys.root }) | |||||
| toast ({ description: '差戻しました.' }) | |||||
| } | |||||
| catch | |||||
| { | |||||
| toast ({ description: '差戻に失敗……' }) | |||||
| } | |||||
| } | |||||
| useEffect (() => { | useEffect (() => { | ||||
| document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) | document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) | ||||
| }, [location.search]) | }, [location.search]) | ||||
| @@ -231,46 +271,7 @@ export default (() => { | |||||
| {dateString (change.createdAt)} | {dateString (change.createdAt)} | ||||
| </td> | </td> | ||||
| <td className="p-2"> | <td className="p-2"> | ||||
| <a | |||||
| href="#" | |||||
| onClick={async e => { | |||||
| e.preventDefault () | |||||
| if (!(confirm ( | |||||
| `『${ change.title.current | |||||
| || change.url.current }』を版 ${ | |||||
| change.versionNo } に差戻します.\nよろしいですか?`))) | |||||
| return | |||||
| try | |||||
| { | |||||
| await apiPut ( | |||||
| `/posts/${ change.postId }`, | |||||
| { title: change.title.current, | |||||
| tags: change.tags | |||||
| .filter (t => t.type !== 'removed') | |||||
| .map (t => t.name) | |||||
| .filter (t => t.slice (0, 5) !== 'nico:') | |||||
| .join (' '), | |||||
| parent_post_ids: | |||||
| (change.parentPosts ?? []) | |||||
| .filter (p => p.type !== 'removed') | |||||
| .map (p => p.id) | |||||
| .join (' '), | |||||
| original_created_from: | |||||
| change.originalCreatedFrom.current, | |||||
| original_created_before: | |||||
| change.originalCreatedBefore.current }) | |||||
| qc.invalidateQueries ({ queryKey: postsKeys.root }) | |||||
| qc.invalidateQueries ({ queryKey: tagsKeys.root }) | |||||
| toast ({ description: '差戻しました.' }) | |||||
| } | |||||
| catch | |||||
| { | |||||
| toast ({ description: '差戻に失敗……' }) | |||||
| } | |||||
| }}> | |||||
| <a href="#" onClick={async e => await handleRevert (e, change)}> | |||||
| 復元 | 復元 | ||||
| </a> | </a> | ||||
| </td> | </td> | ||||
| @@ -121,6 +121,7 @@ export type Platform = typeof PLATFORMS[number] | |||||
| export type Post = { | export type Post = { | ||||
| id: number | id: number | ||||
| versionNo: number | |||||
| url: string | url: string | ||||
| title: string | null | title: string | null | ||||
| thumbnail: string | null | thumbnail: string | null | ||||
| @@ -146,6 +147,7 @@ export type PostTagChange = { | |||||
| export type PostVersion = { | export type PostVersion = { | ||||
| postId: number | postId: number | ||||
| latestVersionNo: number | |||||
| versionNo: number | versionNo: number | ||||
| eventType: 'create' | 'update' | 'discard' | 'restore' | eventType: 'create' | 'update' | 'discard' | 'restore' | ||||
| title: { current: string | null; prev: string | null } | title: { current: string | null; prev: string | null } | ||||