diff --git a/backend/spec/requests/posts_spec.rb b/backend/spec/requests/posts_spec.rb index 1295165..0d7e679 100644 --- a/backend/spec/requests/posts_spec.rb +++ b/backend/spec/requests/posts_spec.rb @@ -756,6 +756,211 @@ RSpec.describe 'Posts API', type: :request do end end + describe 'GET /posts/versions' do + let(:member) { create(:user, :member, name: 'version member') } + + let(:t_v1) { Time.zone.local(2020, 1, 1, 12, 0, 0) } + let(:t_v2) { Time.zone.local(2020, 1, 2, 12, 0, 0) } + let(:t_other) { Time.zone.local(2020, 1, 3, 12, 0, 0) } + + let(:oc_from) { Time.zone.local(2019, 12, 31, 0, 0, 0) } + let(:oc_before) { Time.zone.local(2020, 1, 1, 0, 0, 0) } + + let!(:tag_name2) { TagName.create!(name: 'spec_tag_2') } + let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :general) } + + def snapshot_tags(post) + post.snapshot_tag_names.join(' ') + end + + def create_post_version!(post, version_no:, event_type:, created_by_user:, created_at:) + PostVersion.create!( + post: post, + version_no: version_no, + event_type: event_type, + title: post.title, + url: post.url, + thumbnail_base: post.thumbnail_base, + tags: snapshot_tags(post), + parent: post.parent, + original_created_from: post.original_created_from, + original_created_before: post.original_created_before, + created_at: created_at, + created_by_user: created_by_user + ) + end + + let!(:v1) do + travel_to(t_v1) do + create_post_version!( + post_record, + version_no: 1, + event_type: 'create', + created_by_user: member, + created_at: t_v1 + ) + end + end + + let!(:v2) do + post_record.post_tags.kept.find_by!(tag: tag).discard_by!(member) + PostTag.create!(post: post_record, tag: tag2, created_user: member) + post_record.update!( + title: 'updated spec post', + original_created_from: oc_from, + original_created_before: oc_before + ) + + travel_to(t_v2) do + create_post_version!( + post_record.reload, + version_no: 2, + event_type: 'update', + created_by_user: member, + created_at: t_v2 + ) + end + end + + let!(:other_post_version) do + other_post = Post.create!( + title: 'other versioned post', + url: 'https://example.com/other-versioned' + ) + PostTag.create!(post: other_post, tag: tag) + + travel_to(t_other) do + create_post_version!( + other_post, + version_no: 1, + event_type: 'create', + created_by_user: member, + created_at: t_other + ) + end + end + + it 'returns versions for the specified post in reverse chronological order' do + get '/posts/versions', params: { post: post_record.id } + + expect(response).to have_http_status(:ok) + expect(json).to include('versions', 'count') + expect(json.fetch('count')).to eq(2) + + versions = json.fetch('versions') + expect(versions.map { |v| v['post_id'] }.uniq).to eq([post_record.id]) + expect(versions.map { |v| v['version_no'] }).to eq([2, 1]) + + latest = versions.first + expect(latest).to include( + 'post_id' => post_record.id, + 'version_no' => 2, + 'event_type' => 'update', + 'created_by_user' => { + 'id' => member.id, + 'name' => member.name + } + ) + + expect(latest.fetch('title')).to eq( + 'current' => 'updated spec post', + 'prev' => 'spec post' + ) + expect(latest.fetch('url')).to eq( + 'current' => 'https://example.com/spec', + 'prev' => 'https://example.com/spec' + ) + expect(latest.fetch('thumbnail')).to eq( + 'current' => nil, + 'prev' => nil + ) + expect(latest.fetch('thumbnail_base')).to eq( + 'current' => nil, + 'prev' => nil + ) + expect(latest.fetch('tags')).to include( + { 'name' => 'spec_tag_2', 'type' => 'added' }, + { 'name' => 'spec_tag', 'type' => 'removed' } + ) + expect(latest.fetch('original_created_from')).to eq( + 'current' => oc_from.iso8601, + 'prev' => nil + ) + expect(latest.fetch('original_created_before')).to eq( + 'current' => oc_before.iso8601, + 'prev' => nil + ) + expect(latest.fetch('created_at')).to eq(t_v2.iso8601) + + first = versions.second + expect(first).to include( + 'post_id' => post_record.id, + 'version_no' => 1, + 'event_type' => 'create', + 'created_by_user' => { + 'id' => member.id, + 'name' => member.name + } + ) + expect(first.fetch('title')).to eq( + 'current' => 'spec post', + 'prev' => nil + ) + expect(first.fetch('tags')).to include( + { 'name' => 'spec_tag', 'type' => 'added' } + ) + expect(first.fetch('created_at')).to eq(t_v1.iso8601) + end + + it 'filters versions by tag when the current snapshot includes the tag' do + get '/posts/versions', params: { post: post_record.id, tag: tag2.id } + + expect(response).to have_http_status(:ok) + expect(json.fetch('count')).to eq(1) + + versions = json.fetch('versions') + expect(versions.size).to eq(1) + expect(versions[0]['post_id']).to eq(post_record.id) + expect(versions[0]['version_no']).to eq(2) + expect(versions[0]['tags']).to include( + { 'name' => 'spec_tag_2', 'type' => 'added' } + ) + end + + it 'matches tag filter against current tags snapshot only' do + get '/posts/versions', params: { post: post_record.id, tag: tag.id } + + expect(response).to have_http_status(:ok) + expect(json.fetch('count')).to eq(1) + + versions = json.fetch('versions') + expect(versions.size).to eq(1) + expect(versions[0]['version_no']).to eq(1) + expect(versions[0]['tags']).to include( + { 'name' => 'spec_tag', 'type' => 'added' } + ) + end + + it 'returns empty when tag does not exist' do + get '/posts/versions', params: { tag: 999_999_999 } + + expect(response).to have_http_status(:ok) + expect(json.fetch('versions')).to eq([]) + expect(json.fetch('count')).to eq(0) + end + + it 'clamps page and limit to at least 1' do + get '/posts/versions', params: { post: post_record.id, page: 0, limit: 0 } + + expect(response).to have_http_status(:ok) + expect(json.fetch('count')).to eq(2) + + versions = json.fetch('versions') + expect(versions.size).to eq(1) + expect(versions[0]['version_no']).to eq(2) + end + end + describe 'POST /posts/:id/viewed' do let(:user) { create(:user) } diff --git a/frontend/src/pages/posts/PostHistoryPage.tsx b/frontend/src/pages/posts/PostHistoryPage.tsx index ad61308..fb6b27e 100644 --- a/frontend/src/pages/posts/PostHistoryPage.tsx +++ b/frontend/src/pages/posts/PostHistoryPage.tsx @@ -215,22 +215,29 @@ export default (() => { change.versionNo } に差戻します.\nよろしいですか?`))) return - 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 (' '), - original_created_from: - change.originalCreatedFrom.current, - original_created_before: - change.originalCreatedBefore.current }) - - qc.invalidateQueries ({ queryKey: postsKeys.root }) - qc.invalidateQueries ({ queryKey: tagsKeys.root }) - toast ({ description: '更新しました.' }) + 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 (' '), + 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: '差戻に失敗……' }) + } }}> 復元