コミットを比較
1
コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
|
|
63ea2727f0 |
+1
-1
@@ -424,7 +424,7 @@ races according to the target constraint's business meaning.
|
||||
and JSON keys are generally snake_case.
|
||||
- Preserve existing HTTP status conventions:
|
||||
`:unauthorized` for no user, `:forbidden` for insufficient role or banned
|
||||
user, `:not_found` for missing records, and `:unprocessable_entity` for
|
||||
user, `:not_found` for missing records, and `:unprocessable_content` for
|
||||
validation failures.
|
||||
- For diagnostic or internal helper JSON, prefer a deliberately light response
|
||||
shape over full representation classes when callers only need identifiers,
|
||||
|
||||
@@ -95,7 +95,7 @@ class ApplicationController < ActionController::API
|
||||
render_validation_error base: ['すでに存在してゐます.']
|
||||
end
|
||||
|
||||
def render_validation_error record = nil, fields: { }, base: [], status: :unprocessable_entity
|
||||
def render_validation_error record = nil, fields: { }, base: [], status: :unprocessable_content
|
||||
errors = { }
|
||||
|
||||
if record
|
||||
|
||||
@@ -18,7 +18,7 @@ class CountsExportsController < ApplicationController
|
||||
render json: { error: 'private_condition' }, status: :forbidden
|
||||
rescue CountsTableQuery::TooManyColumns
|
||||
render json: { error: 'too_many_columns', max_columns: CountsTableQuery::MAX_COLUMNS },
|
||||
status: :unprocessable_entity
|
||||
status: :unprocessable_content
|
||||
rescue ActiveRecord::StatementTimeout
|
||||
render json: { error: 'counts_query_timeout' }, status: :service_unavailable
|
||||
rescue ArgumentError, TypeError, KeyError, JSON::ParserError
|
||||
@@ -63,7 +63,7 @@ class CountsExportsController < ApplicationController
|
||||
render json: { error: 'private_condition' }, status: :forbidden
|
||||
rescue CountsTableQuery::TooManyColumns
|
||||
render json: { error: 'too_many_columns', max_columns: CountsTableQuery::MAX_COLUMNS },
|
||||
status: :unprocessable_entity
|
||||
status: :unprocessable_content
|
||||
rescue ActiveRecord::StatementTimeout
|
||||
render json: { error: 'counts_query_timeout' }, status: :service_unavailable
|
||||
rescue ArgumentError, TypeError, KeyError, JSON::ParserError
|
||||
@@ -107,7 +107,7 @@ class CountsExportsController < ApplicationController
|
||||
render json: { error: 'private_condition' }, status: :forbidden
|
||||
rescue CountsTableQuery::TooManyColumns
|
||||
render json: { error: 'too_many_columns', max_columns: CountsTableQuery::MAX_COLUMNS },
|
||||
status: :unprocessable_entity
|
||||
status: :unprocessable_content
|
||||
rescue ActiveRecord::StatementTimeout
|
||||
render json: { error: 'counts_query_timeout' }, status: :service_unavailable
|
||||
rescue ArgumentError, TypeError, KeyError, JSON::ParserError
|
||||
|
||||
@@ -24,7 +24,7 @@ class CountsTablesController < ApplicationController
|
||||
render json: { error: 'private_condition' }, status: :forbidden
|
||||
rescue CountsTableQuery::TooManyColumns
|
||||
render json: { error: 'too_many_columns', max_columns: CountsTableQuery::MAX_COLUMNS },
|
||||
status: :unprocessable_entity
|
||||
status: :unprocessable_content
|
||||
rescue CountsPivotTotals::TimedOut
|
||||
Rails.logger.warn('counts_query_timeout: pivot_totals')
|
||||
render json: { error: 'counts_query_timeout', stage: 'totals',
|
||||
|
||||
@@ -15,7 +15,7 @@ class GekanatorGamesController < ApplicationController
|
||||
answers:)
|
||||
|
||||
if game.invalid?
|
||||
render json: { errors: game.errors.full_messages }, status: :unprocessable_entity
|
||||
render json: { errors: game.errors.full_messages }, status: :unprocessable_content
|
||||
return
|
||||
end
|
||||
|
||||
@@ -31,7 +31,7 @@ class GekanatorGamesController < ApplicationController
|
||||
learned_example_count:
|
||||
}, status: :created
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
|
||||
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def extra_questions
|
||||
|
||||
@@ -536,7 +536,7 @@ class MaterialsController < ApplicationController
|
||||
errors: { },
|
||||
base_errors: ['ZIP export に必要な素材ファイルが欠損しています.'],
|
||||
missing_files: },
|
||||
status: :unprocessable_entity
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
def build_uploaded_material_blob! file, file_sha256
|
||||
|
||||
@@ -429,7 +429,7 @@ class PostsController < ApplicationController
|
||||
filters = literals.map do |literal|
|
||||
internal, = source_ids.fetch(literal[:name])
|
||||
name = internal.empty? ? literal[:name] : names[internal.first]
|
||||
return head :unprocessable_entity unless name && internal.length <= 1
|
||||
return head :unprocessable_content unless name && internal.length <= 1
|
||||
|
||||
literal[:negative] ? "not:#{ name }" : name
|
||||
end
|
||||
@@ -438,7 +438,7 @@ class PostsController < ApplicationController
|
||||
literals.zip(translated).each do |literal, translation|
|
||||
unless (source_ids.fetch(literal[:name]).map(&:sort) ==
|
||||
target_ids.fetch(translation[:name]).map(&:sort))
|
||||
return head :unprocessable_entity
|
||||
return head :unprocessable_content
|
||||
end
|
||||
end
|
||||
index = 0
|
||||
|
||||
@@ -55,7 +55,7 @@ class TheatresController < ApplicationController
|
||||
theatre = Theatre.find_by(id: params[:id])
|
||||
return head :not_found unless theatre
|
||||
requested_post_id = params[:post_id].to_i
|
||||
return head :unprocessable_entity if requested_post_id <= 0
|
||||
return head :unprocessable_content if requested_post_id <= 0
|
||||
|
||||
skipped = false
|
||||
conflicted = false
|
||||
@@ -95,7 +95,7 @@ class TheatresController < ApplicationController
|
||||
theatre = Theatre.find_by(id: params[:id])
|
||||
return head :not_found unless theatre
|
||||
requested_post_id = params[:post_id].to_i
|
||||
return head :unprocessable_entity if requested_post_id <= 0
|
||||
return head :unprocessable_content if requested_post_id <= 0
|
||||
|
||||
conflicted = false
|
||||
|
||||
|
||||
@@ -6,14 +6,8 @@ class CountsCandidates
|
||||
|
||||
def initialize(params)
|
||||
QueryState.validate!(params, KEYS)
|
||||
definition = CountsResourceDefinition.new(params.fetch('resource'))
|
||||
fields = ['resource_id', 'user']
|
||||
fields += ['tags', 'tags_json', 'tags.id'] if definition.resource == 'posts'
|
||||
fields << 'members' if definition.resource == 'tag_groups'
|
||||
if definition.resource == 'materials'
|
||||
fields += ['updated_by_user_id', 'file_blob_id', 'source_file_id', 'drive_file_id']
|
||||
end
|
||||
raise ArgumentError unless fields.include?(params.fetch('field'))
|
||||
raise ArgumentError unless params.fetch('resource') == 'posts'
|
||||
raise ArgumentError unless ['tags', 'tags_json'].include?(params.fetch('field'))
|
||||
|
||||
@params = params
|
||||
@page = Integer(params.fetch('page', '1'), 10)
|
||||
@@ -24,27 +18,25 @@ class CountsCandidates
|
||||
end
|
||||
|
||||
def call
|
||||
references = ['tags', 'tags_json', 'tags.id', 'members']
|
||||
return resource_candidates unless references.include?(@params['field'])
|
||||
|
||||
options = @params.slice('locale', 'at', 'execution_at', 'timezone')
|
||||
.merge('definition' => 'latest', 'resource' => 'tags')
|
||||
internal = CountsPopulation.new(options)
|
||||
groups_flg = @params['field'] == 'members'
|
||||
external = CountsPopulation.new(options.merge(
|
||||
'resource' => groups_flg ? 'tag_groups' : 'nico_tags',
|
||||
external = CountsPopulation.new(options.merge('resource' => 'nico_tags',
|
||||
'execution_at' => internal.at.iso8601(6)))
|
||||
connection = TagVersion.connection
|
||||
sources = [[internal, 'internal', 'v.category', 'v.names'],
|
||||
[external, groups_flg ? 'group' : 'external', 'NULL', 'JSON_OBJECT()']]
|
||||
[external, 'external', 'NULL', 'JSON_OBJECT()']]
|
||||
sources = sources.map do |population, kind, category, names|
|
||||
scope = population.scope.except(:select)
|
||||
identity = "v.#{ population.definition.keys.first }"
|
||||
scope = search(scope, identity, 'v.name', names)
|
||||
value = ['tags.id', 'members'].include?(@params['field']) ?
|
||||
"CAST(#{ identity } AS CHAR)" : "CONCAT('#{ kind }:', #{ identity })"
|
||||
if @params['query'].present?
|
||||
pattern = "%#{ ApplicationRecord.sanitize_sql_like(@params['query']) }%"
|
||||
scope = scope.where("(v.name LIKE ? OR CAST(v.tag_id AS CHAR) = ? OR EXISTS (" +
|
||||
"SELECT 1 FROM JSON_TABLE(COALESCE(JSON_EXTRACT(#{ names }, '$**.name'), " +
|
||||
"JSON_ARRAY()), '$[*]' COLUMNS (name TEXT PATH '$')) candidate_name " +
|
||||
'WHERE candidate_name.name LIKE ?))', pattern, @params['query'], pattern)
|
||||
end
|
||||
scope.select(Arel.sql(
|
||||
"#{ value } AS value, #{ identity } AS id, " +
|
||||
"CONCAT('#{ kind }:', v.tag_id) AS value, v.tag_id AS id, " +
|
||||
"v.name AS name, #{ names } AS names, #{ category } AS category, '#{ kind }' AS kind"))
|
||||
.to_sql
|
||||
end
|
||||
@@ -63,63 +55,4 @@ class CountsCandidates
|
||||
{ rows: rows.first(LIMIT), page: @page, more: rows.length > LIMIT,
|
||||
execution_at: internal.at.iso8601(6) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search(scope, identity, name, names)
|
||||
return scope if @params['query'].blank?
|
||||
|
||||
pattern = "%#{ ApplicationRecord.sanitize_sql_like(@params['query']) }%"
|
||||
scope.where("(#{ name } LIKE ? OR CAST(#{ identity } AS CHAR) = ? OR EXISTS (" +
|
||||
"SELECT 1 FROM JSON_TABLE(COALESCE(JSON_EXTRACT(#{ names }, '$**.name'), " +
|
||||
"JSON_ARRAY()), '$[*]' COLUMNS (name TEXT PATH '$')) candidate_name " +
|
||||
'WHERE candidate_name.name LIKE ?))', pattern, @params['query'], pattern)
|
||||
end
|
||||
|
||||
def resource_candidates
|
||||
options = @params.slice('at', 'execution_at', 'timezone')
|
||||
.merge('resource' => @params.fetch('resource'), 'definition' => 'latest')
|
||||
actor_flg = ['user', 'updated_by_user_id'].include?(@params['field'])
|
||||
options['definition'] = 'history' if actor_flg
|
||||
population = CountsPopulation.new(options)
|
||||
scope = population.scope.except(:select)
|
||||
if actor_flg
|
||||
point = population.timestamp(@params['at']) || population.at
|
||||
raise ArgumentError if point > population.at
|
||||
|
||||
column = @params['field'] == 'user' ? 'created_by_user_id' : 'updated_by_user_id'
|
||||
identities = scope.where('v.created_at <= ?', point).select("v.#{ column }")
|
||||
scope = User.where(id: identities)
|
||||
identity = 'users.id'
|
||||
name = 'users.name'
|
||||
names = 'JSON_OBJECT()'
|
||||
else
|
||||
definition = population.definition
|
||||
column = @params['field'] == 'resource_id' ? definition.keys.first : @params['field']
|
||||
if column == 'drive_file_id'
|
||||
column = 'source_file_id'
|
||||
scope = scope.where('v.source_kind' => 'google_drive_file')
|
||||
end
|
||||
identity = "v.#{ column }"
|
||||
label = ['title', 'name', 'url'].find { definition.fields.key?(_1) }
|
||||
name = label ? "v.#{ label }" : 'NULL'
|
||||
names = definition.resource == 'tags' ? 'v.names' : 'JSON_OBJECT()'
|
||||
if ['file_blob_id', 'source_file_id', 'drive_file_id'].include?(@params['field'])
|
||||
scope = scope.where.not("v.#{ column }" => nil).group(Arel.sql(identity))
|
||||
name = 'v.file_filename'
|
||||
end
|
||||
end
|
||||
scope = search(scope, identity, name, names)
|
||||
title = ['file_blob_id', 'source_file_id', 'drive_file_id'].include?(@params['field']) ?
|
||||
"MIN(#{ name })" : name
|
||||
query = scope.optimizer_hints('MAX_EXECUTION_TIME(10000)').distinct
|
||||
.select(Arel.sql("#{ identity } AS id, #{ title } AS name, #{ names } AS names"))
|
||||
.order(Arel.sql(identity)).limit(LIMIT + 1).offset((@page - 1) * LIMIT)
|
||||
rows = scope.klass.connection.select_all(query.to_sql).to_a.map do |values|
|
||||
values['names'] = JSON.parse(values['names']) if values['names'].is_a?(String)
|
||||
values.merge('value' => values.fetch('id').to_s)
|
||||
end
|
||||
{ rows: rows.first(LIMIT), page: @page, more: rows.length > LIMIT,
|
||||
execution_at: population.at.iso8601(6) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -262,7 +262,7 @@ RSpec.describe 'Counts table API', type: :request do
|
||||
tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024, 3))
|
||||
end
|
||||
get '/counts/query', params: base.merge(mode: 'summary', rows: '', cols: 'title', sort: '')
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(response.parsed_body).to eq('error' => 'too_many_columns', 'max_columns' => 100)
|
||||
end
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RSpec.describe 'error responses', type: :request do
|
||||
|
||||
patch "/tags/#{ tag.id }", params: { category: 'nico' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json).to include(
|
||||
'type' => 'validation_error',
|
||||
'message' => '入力内容を確認してください.',
|
||||
@@ -37,7 +37,7 @@ RSpec.describe 'error responses', type: :request do
|
||||
|
||||
put "/users/#{ user.id }", params: { name: 'a' * 256 }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json).to include(
|
||||
'type' => 'validation_error',
|
||||
'message' => '入力内容を確認してください.')
|
||||
|
||||
@@ -49,7 +49,7 @@ RSpec.describe 'Gekanator games API', type: :request do
|
||||
question_count: 4,
|
||||
answers: [{ question_id: 'tag:1', answer: 'no' }] }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
|
||||
it 'returns unauthorized without a user' do
|
||||
|
||||
@@ -126,7 +126,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
|
||||
}
|
||||
}.not_to change { GekanatorGame.count }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
|
||||
it 'stores a game result for a non-admin user' do
|
||||
@@ -785,7 +785,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
|
||||
}
|
||||
}.not_to change { GekanatorQuestionExample.count }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
|
||||
it 'rejects non-accepted questions' do
|
||||
@@ -805,7 +805,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
|
||||
]
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
|
||||
it 'rejects non-post_similarity questions' do
|
||||
@@ -829,7 +829,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
|
||||
]
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
|
||||
it 'allows a non-admin user to answer extra questions for their own game' do
|
||||
|
||||
@@ -55,7 +55,7 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
|
||||
deprecated: false, aliases: 'other_primary', parent_tags: '' }
|
||||
|
||||
aggregate_failures do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(other_primary.reload).to have_attributes(tag_id: other_tag.id, primary_flg: true)
|
||||
expect(TagName.where(tag: other_tag, language_code: 'ja', primary_flg: true))
|
||||
.to contain_exactly(other_primary)
|
||||
@@ -289,7 +289,7 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
|
||||
aliases: 'other_ainu_primary', parent_tags: '' }
|
||||
|
||||
aggregate_failures do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(other_primary.reload).to have_attributes(
|
||||
tag_id: other_tag.id, language_code: 'ain', primary_flg: true)
|
||||
expect(TagName.where(tag: other_tag, language_code: 'ain', primary_flg: true))
|
||||
|
||||
@@ -289,7 +289,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
it 'returns 422 when tag is blank' do
|
||||
post '/materials', params: { tag: ' ', file: dummy_upload }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'tag' => ['タグは必須です.'])
|
||||
end
|
||||
@@ -297,7 +297,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
it 'returns 422 when both file and url are blank' do
|
||||
post '/materials', params: { tag: 'material_create_blank' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'file' => ['ファイルまたは URL は必須です.'],
|
||||
'url' => ['ファイルまたは URL は必須です.'])
|
||||
@@ -381,7 +381,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
file: dummy_upload
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
|
||||
it 'persists url-only material' do
|
||||
@@ -424,7 +424,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
}
|
||||
end.not_to change(Material, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'file' => ['抑止された素材です: copyright_high_risk']
|
||||
)
|
||||
@@ -484,7 +484,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
file: dummy_upload
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'tag' => ['タグは必須です.'])
|
||||
end
|
||||
@@ -695,7 +695,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
file: dummy_upload(filename: 'blocked.png', body: 'blocked-update')
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(material.reload.file.blob.filename.to_s).to eq('old.png')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -218,7 +218,7 @@ RSpec.describe 'NicoTags', type: :request do
|
||||
|
||||
patch "/tags/nico/#{nico_tag.id}", params: { tags: 'nico:linked_ng' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'tags' => ['ニコニコ・タグ同士は連携できません.'])
|
||||
end
|
||||
@@ -239,7 +239,7 @@ RSpec.describe 'NicoTags', type: :request do
|
||||
|
||||
expect([Tag.count, TagName.count, TagVersion.count]).to eq(before_counts)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors').fetch('tags')).to include(
|
||||
a_string_including('タグ名 “invalid”:', '名前に使用できない文字が含まれてゐます.'))
|
||||
end
|
||||
|
||||
@@ -48,7 +48,7 @@ RSpec.describe 'Post filter identity across locales', type: :request do
|
||||
primary_tag_name_for(tag, 'ja').update_columns(name: 'nico:shared')
|
||||
get '/posts/tag_filters', params: {
|
||||
locale: 'ja', target_locale: 'ain-Kana', tags: 'nico:shared' }
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
|
||||
it 'rejects removed reference parameters instead of overriding the name' do
|
||||
|
||||
@@ -1042,7 +1042,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
|
||||
get '/posts/metadata', params: { url: 'file:///etc/passwd' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to have_key('url')
|
||||
end
|
||||
end
|
||||
@@ -1180,7 +1180,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
)
|
||||
}.not_to change(Post, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'thumbnail' => ['サムネイル画像の変換に失敗しました.']
|
||||
)
|
||||
@@ -1219,7 +1219,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
thumbnail: dummy_upload
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'tags' => ['廃止済みタグがあります: deprecated_direct_tag']
|
||||
)
|
||||
@@ -1265,7 +1265,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
thumbnail: dummy_upload
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json).to include(
|
||||
'type' => 'validation_error',
|
||||
'message' => '入力内容を確認してください.'
|
||||
@@ -1333,7 +1333,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
thumbnail: dummy_upload
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'video_ms' => ['タグ区間の終端が動画時間を超えてゐます.']
|
||||
)
|
||||
@@ -1444,7 +1444,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
thumbnail: dummy_upload
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'video_ms' => ['タグ区間の開始が動画時間以上です.']
|
||||
)
|
||||
@@ -1465,7 +1465,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
thumbnail: dummy_upload
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity), response.body
|
||||
expect(response).to have_http_status(:unprocessable_content), response.body
|
||||
expect(json).to include(
|
||||
'type' => 'validation_error',
|
||||
'message' => '入力内容を確認してください.',
|
||||
@@ -1485,7 +1485,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
tags: 'spec_tag', # 既存タグ名を投げる
|
||||
thumbnail: dummy_upload)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1499,7 +1499,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
tags: 'spec_tag', # 既存タグ名を投げる
|
||||
thumbnail: dummy_upload)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1601,7 +1601,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
thumbnail: dummy_upload }
|
||||
}.not_to change(Post, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to be_present
|
||||
end
|
||||
end
|
||||
@@ -1620,7 +1620,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
)
|
||||
}.not_to change(Post, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to be_present
|
||||
end
|
||||
end
|
||||
@@ -1639,7 +1639,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
)
|
||||
}.not_to change(PostImplication, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to be_present
|
||||
end
|
||||
end
|
||||
@@ -1770,7 +1770,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
tags: 'deprecated_update_tag'
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'tags' => ['廃止済みタグは付与できません.']
|
||||
)
|
||||
@@ -1789,7 +1789,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
title: 'updated title',
|
||||
tags: 'nico:nico_tag')
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity), response.body
|
||||
expect(response).to have_http_status(:unprocessable_content), response.body
|
||||
expect(json).to include(
|
||||
'type' => 'validation_error',
|
||||
'message' => '入力内容を確認してください.',
|
||||
@@ -1896,7 +1896,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
title: 'updated title',
|
||||
tags: 'spec_tag' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to be_present
|
||||
end
|
||||
end
|
||||
@@ -1922,7 +1922,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
parent_post_ids: 'abc'
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(post_record.reload.parent_posts.pluck(:id)).to eq([parent_post.id])
|
||||
end
|
||||
end
|
||||
@@ -1948,7 +1948,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
parent_post_ids: '999999999'
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(post_record.reload.parent_posts.pluck(:id)).to eq([parent_post.id])
|
||||
end
|
||||
end
|
||||
@@ -1964,7 +1964,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
parent_post_ids: post_record.id.to_s
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
|
||||
expect(PostImplication.exists?(
|
||||
post_id: post_record.id,
|
||||
@@ -2669,7 +2669,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
thumbnail: dummy_upload)
|
||||
end.not_to change(PostVersion, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
|
||||
it 'does not create a version when PUT /posts/:id is invalid' do
|
||||
@@ -2685,7 +2685,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
original_created_before: Time.zone.local(2020, 1, 1, 0, 0, 0).iso8601)
|
||||
end.not_to change(PostVersion, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
|
||||
it 'rejects second-bearing original created timestamps on POST /posts' do
|
||||
@@ -2698,7 +2698,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
thumbnail: dummy_upload,
|
||||
original_created_from: '2020-01-01T00:00:01Z')
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'original_created_from' => ['オリジナルの作成日時は分単位で入力してください.']
|
||||
)
|
||||
@@ -2715,7 +2715,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
thumbnail: dummy_upload,
|
||||
original_created_before: '2020-01-01T00:00:00.123Z')
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'original_created_before' => ['オリジナルの作成日時は分単位で入力してください.']
|
||||
)
|
||||
@@ -2733,7 +2733,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
original_created_from: '2020-01-01T00:00Z',
|
||||
original_created_before: '2020-01-01T00:00Z')
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'original_created_at' => ['オリジナルの作成日時の順番がをかしぃです.']
|
||||
)
|
||||
@@ -2767,7 +2767,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
tags: 'spec_tag',
|
||||
original_created_from: 'not-a-time')
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'original_created_from' => ['オリジナルの作成日時の形式が不正です.']
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ RSpec.describe "TagChildren", type: :request do
|
||||
do_request
|
||||
}.not_to change(TagImplication, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(TagImplication.where(tag: child, parent_tag: parent)).not_to exist
|
||||
end
|
||||
end
|
||||
|
||||
@@ -303,7 +303,7 @@ RSpec.describe 'Tag description native API', type: :request do
|
||||
headers
|
||||
before_state = stored_state
|
||||
native_write(body: '')
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json).to include('type' => 'validation_error',
|
||||
'errors' => include('body' => a_kind_of(Array)))
|
||||
expect(stored_state).to eq(before_state)
|
||||
|
||||
@@ -84,7 +84,7 @@ RSpec.describe 'Tag group locale contracts', type: :request do
|
||||
put "/wiki/#{ group.id }", params: {
|
||||
locale: 'ain-Kana', title: 'タググループ:partial rename', body: '- japanese_member' },
|
||||
headers: headers
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
|
||||
@@ -117,7 +117,7 @@ RSpec.describe 'Tag group locale contracts', type: :request do
|
||||
expect(representation.fetch('members').sole.fetch('tag').fetch('name')).to be_nil
|
||||
expect(representation.fetch('placements').sole.fetch('tag').fetch('name')).to be_nil
|
||||
get "/wiki/#{ old_group.id }", params: { locale: 'ain-Kana', version: version.id }
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(response.body).not_to include('historical_japanese', 'current_ainu')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -133,7 +133,7 @@ RSpec.describe 'Tag group Markdown round trips through the Wiki facade', type: :
|
||||
|
||||
get group_title_path('混在構造')
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_rows('tag_groups', id:).sole.fetch('id')).to eq(id)
|
||||
end
|
||||
|
||||
@@ -142,7 +142,7 @@ RSpec.describe 'Tag group Markdown round trips through the Wiki facade', type: :
|
||||
|
||||
get group_title_path('未配置構造')
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_rows('tag_groups', id:).sole.fetch('id')).to eq(id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,7 +21,7 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
|
||||
before_state = group_state
|
||||
yield
|
||||
aggregate_failures do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_state).to eq(before_state)
|
||||
expect(response.media_type).to eq('application/json')
|
||||
expect(response.body).not_to be_empty
|
||||
|
||||
@@ -27,7 +27,7 @@ RSpec.describe 'Tag group write integrity', type: :request do
|
||||
def expect_no_writes
|
||||
before_state = group_state
|
||||
yield
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ RSpec.describe 'Tag group native API', type: :request do
|
||||
before_state = group_state
|
||||
native_write(operation, payload.merge(invalid))
|
||||
aggregate_failures do
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
end
|
||||
@@ -158,7 +158,7 @@ RSpec.describe 'Tag group native API', type: :request do
|
||||
before_state = group_state
|
||||
invalid = [{ tag_id: tag.id, section_name: '不正', children: [] }]
|
||||
native_write(operation, payload.merge(placements: invalid))
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
|
||||
@@ -167,7 +167,7 @@ RSpec.describe 'Tag group native API', type: :request do
|
||||
before_state = group_state
|
||||
native_write(operation, payload.merge(
|
||||
members: [{ tag_id: missing }], placements: [{ tag_id: missing, children: [] }]))
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
|
||||
@@ -177,7 +177,7 @@ RSpec.describe 'Tag group native API', type: :request do
|
||||
native_write(operation, payload.merge(
|
||||
members: [{ tag_group_id: missing }],
|
||||
placements: [{ tag_group_id: missing, children: [] }]))
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
|
||||
@@ -186,7 +186,7 @@ RSpec.describe 'Tag group native API', type: :request do
|
||||
6.times { nodes = [{ tag_id: tag.id, children: nodes }] }
|
||||
before_state = group_state
|
||||
native_write(operation, payload.merge(placements: nodes))
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
end
|
||||
@@ -206,7 +206,7 @@ RSpec.describe 'Tag group native API', type: :request do
|
||||
before_state = group_state
|
||||
native_write(:update, payload.merge(
|
||||
members: [{ tag_group_id: parent }], placements: []))
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
|
||||
@@ -214,7 +214,7 @@ RSpec.describe 'Tag group native API', type: :request do
|
||||
before_state = group_state
|
||||
native_write(:update, payload.merge(
|
||||
members: [{ tag_group_id: group_id }], placements: []))
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
|
||||
do_request
|
||||
}.not_to change { Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] } }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json).to include(
|
||||
'type' => 'validation_error',
|
||||
'message' => '入力内容を確認してください.',
|
||||
@@ -321,7 +321,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
|
||||
do_request
|
||||
}.not_to change { Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] } }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'deerjikists.0.platform' => [be_present],
|
||||
)
|
||||
@@ -352,7 +352,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
|
||||
do_request
|
||||
}.not_to change { Deerjikist.order(:platform, :code).pluck(:platform, :code, :tag_id) }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'deerjikists.1.code' => [include('existing-deerjikist')],
|
||||
)
|
||||
|
||||
@@ -696,7 +696,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
it 'nico category への変更は 422 を返す' do
|
||||
patch "/tags/#{tag.id}", params: { name: 'new', category: 'nico' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
|
||||
expect(tag.category).to eq('general')
|
||||
end
|
||||
@@ -740,7 +740,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
patch "/tags/#{tag.id}", params: { category: 'nico' }
|
||||
}.not_to change(TagVersion, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(tag.reload.category).to eq('general')
|
||||
end
|
||||
|
||||
@@ -824,7 +824,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
|
||||
patch "/tags/#{ tag.id }", params: { name: 'unknown' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'name' => ['その名前は既に使はれてゐます.']
|
||||
)
|
||||
@@ -840,7 +840,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
|
||||
patch "/tags/#{ system_tag.id }", params: { name: 'patch_system_tag_renamed' }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(primary_tag_name_for(system_tag.reload, 'ja').name).to eq('bot操作')
|
||||
end
|
||||
end
|
||||
@@ -1144,7 +1144,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
parent_tags: '',
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
|
||||
end
|
||||
|
||||
@@ -1156,7 +1156,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
parent_tags: '',
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
|
||||
expect(tag.category).to eq('general')
|
||||
end
|
||||
@@ -1169,7 +1169,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
parent_tags: '',
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'deprecated' => ['廃止状態は必須です.']
|
||||
)
|
||||
@@ -1366,7 +1366,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
}
|
||||
}.not_to change(TagVersion, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
|
||||
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
|
||||
expect(tag.category).to eq('general')
|
||||
@@ -1387,7 +1387,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
}
|
||||
}.not_to change(TagVersion, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
|
||||
expect(primary_tag_name_for(system_tag.reload, 'ja').name).to eq(old_name)
|
||||
expect(system_tag.category).to eq(old_category)
|
||||
@@ -1569,7 +1569,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
deprecated: '0',
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
|
||||
expect(TagImplication.where(tag:, parent_tag: child)).not_to exist
|
||||
end
|
||||
|
||||
@@ -131,7 +131,7 @@ RSpec.describe 'TheatreComments', type: :request do
|
||||
post "/theatres/#{theatre.id}/comments", params: { content: ' ' }
|
||||
}.not_to change(TheatreComment, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
|
||||
it 'theatre が存在しなければ 404 を返す' do
|
||||
|
||||
@@ -376,7 +376,7 @@ RSpec.describe 'Theatres API', type: :request do
|
||||
put "/theatres/#{theatre.id}/skip_vote", params: { post_id: 'invalid' }
|
||||
}.not_to change(TheatreSkipVote, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
|
||||
it 'records a vote and returns the current vote status before majority' do
|
||||
|
||||
@@ -79,7 +79,7 @@ RSpec.describe 'user settings', type: :request do
|
||||
|
||||
patch '/users/settings', params: { theme: 1 }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json).to include('type' => 'validation_error')
|
||||
expect(json.fetch('errors')).to include(
|
||||
'theme' => ['値の型が不正です.'])
|
||||
@@ -90,7 +90,7 @@ RSpec.describe 'user settings', type: :request do
|
||||
|
||||
patch '/users/settings', params: { theme: 'neon' }, as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json).to include('type' => 'validation_error')
|
||||
expect(json.fetch('errors').fetch('theme')).to be_present
|
||||
end
|
||||
|
||||
@@ -84,7 +84,7 @@ RSpec.describe 'user theme slots', type: :request do
|
||||
params: { tokens: 'not-object' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'base_theme' => ['値が不正です.'])
|
||||
end
|
||||
@@ -96,7 +96,7 @@ RSpec.describe 'user theme slots', type: :request do
|
||||
params: { tokens: 'not-object' },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'tokens' => ['JSON object で指定してください.'])
|
||||
end
|
||||
|
||||
@@ -119,7 +119,7 @@ RSpec.describe 'Users', type: :request do
|
||||
params: { name: ' ' },
|
||||
headers: auth_headers(user)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'name' => ['名前は必須です.'])
|
||||
end
|
||||
|
||||
@@ -39,7 +39,7 @@ RSpec.describe 'Wiki creation freeze', type: :request do
|
||||
it "rejects invalid description body #{ body.inspect } atomically" do
|
||||
before_state = group_state
|
||||
put "/wiki/#{ page.id }", params: { title: tag.name('ja'), body: }, headers: headers
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -100,12 +100,12 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
context 'when params invalid' do
|
||||
it 'returns 422 when title blank' do
|
||||
post endpoint, params: { title: '', body: 'Hello' }, headers: auth_headers(member)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
|
||||
it 'returns 422 when body blank' do
|
||||
post endpoint, params: { title: 'Test', body: '' }, headers: auth_headers(member)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -160,7 +160,7 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
put "/wiki/#{page.id}",
|
||||
params: { title: 'TestPage', body: '' },
|
||||
headers: auth_headers(member)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ RSpec.describe 'Wiki title collision', type: :request do
|
||||
},
|
||||
headers: auth_headers(user)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
|
||||
source.reload
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ RSpec.describe 'Removed standalone Wiki resources', type: :request do
|
||||
before_state = group_state
|
||||
post '/wiki', params: { title:, body: 'body' },
|
||||
headers: cookie_headers(user)
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response).to have_http_status(:unprocessable_content)
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import ResourcePickerButton from '@/components/common/ResourcePickerButton'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
||||
import FieldError from '@/components/common/FieldError'
|
||||
@@ -185,12 +184,7 @@ const PostEditForm: FC<Props> = ({ post, onSave }) => {
|
||||
value={parentPostIds}
|
||||
disabled={disabled}
|
||||
errors={fieldErrors.parentPostIds}
|
||||
onChange={setParentPostIds}
|
||||
after={<ResourcePickerButton
|
||||
source={{ resource: 'posts', field: 'resource_id' }}
|
||||
values={parentPostIds.split (/\s+/).filter (Boolean)}
|
||||
maximum={Number.MAX_SAFE_INTEGER} disabled={disabled}
|
||||
onSelect={values => setParentPostIds (values.join (' '))}/>}/>
|
||||
onChange={setParentPostIds}/>
|
||||
|
||||
<PostTagsField
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import CandidatePicker from '@/components/common/CandidatePicker'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import useDialogue from '@/lib/dialogues/useDialogue'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import type { CandidateSource } from '@/components/common/CandidatePicker'
|
||||
|
||||
|
||||
const ResourcePickerButton = (
|
||||
{ source, values, maximum = 1, disabled, onSelect }: {
|
||||
source: CandidateSource; values: (string | number | boolean)[]
|
||||
maximum?: number; disabled?: boolean
|
||||
onSelect: (values: string[]) => void },
|
||||
) => {
|
||||
const dialogue = useDialogue ()
|
||||
const { label } = useUITranslation ('counts')
|
||||
return <Button type="button" variant="outline" disabled={disabled || maximum === 0}
|
||||
onClick={() => void dialogue.form ({
|
||||
title: label ('pattern.link'), size: 'large', footerLayout: 'inline',
|
||||
body: controls => <CandidatePicker source={source} initial={values}
|
||||
maximum={maximum} controls={controls} onSelect={onSelect}/> })}>
|
||||
{label ('pattern.selection')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
|
||||
export default ResourcePickerButton
|
||||
@@ -2,10 +2,9 @@ import { useEffect, useState } from 'react'
|
||||
|
||||
import CountsConditionValues from '@/components/counts/CountsConditionValues'
|
||||
import DateTimeField from '@/components/common/DateTimeField'
|
||||
import ResourcePickerButton from '@/components/common/ResourcePickerButton'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { countsCandidateSource, countsConditionInputs, countsEventTypes, countsFieldLabel,
|
||||
countsFilterFields, countsSetOperators, useCountsDefinition }
|
||||
import { countsConditionInputs, countsEventTypes, countsFieldLabel, countsFilterFields,
|
||||
countsSetOperators, useCountsDefinition }
|
||||
from '@/lib/countsDefinitions'
|
||||
import useDialogue from '@/lib/dialogues/useDialogue'
|
||||
import { countsConditionValue } from '@/lib/countsDisplay'
|
||||
@@ -16,7 +15,7 @@ import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import { readCountState, encodeCountState } from '@/lib/queryState'
|
||||
|
||||
import type { CountsResource, CountsResult } from '@/lib/counts'
|
||||
import type { CountsResult } from '@/lib/counts'
|
||||
import type { DialogueFormControls } from '@/lib/dialogues/useDialogue'
|
||||
import type { CountsCondition, CountsDefinition } from '@/lib/countsDefinitions'
|
||||
|
||||
@@ -95,12 +94,10 @@ const ConditionEditor = (
|
||||
}
|
||||
|
||||
|
||||
const CountsAdditionalConditions = (
|
||||
{ resource, params, update, conditionLabels }: {
|
||||
resource: CountsResource
|
||||
conditionLabels?: CountsResult['conditionLabels']
|
||||
params: URLSearchParams
|
||||
update: (key: string, value: string) => void },
|
||||
const CountsAdditionalConditions = ({ params, update, conditionLabels }: {
|
||||
conditionLabels?: CountsResult['conditionLabels']
|
||||
params: URLSearchParams
|
||||
update: (key: string, value: string) => void },
|
||||
) => {
|
||||
const { label, message } = useUITranslation ('counts')
|
||||
const definition = useCountsDefinition ()
|
||||
@@ -152,19 +149,11 @@ const CountsAdditionalConditions = (
|
||||
value={condition[key] == null ? '' : String (condition[key])} disabled={condition.blank}
|
||||
aria-label={aria.text} lang={aria.language}
|
||||
onChange={value => patch (index, { [key]: value ?? '' })}/>
|
||||
const source = countsCandidateSource (resource, condition.field, params)
|
||||
const value = condition[key]
|
||||
return <>
|
||||
<input value={condition[key] == null ? '' : String (condition[key])}
|
||||
disabled={condition.blank}
|
||||
type={(type === 'integer' || type === 'interval') ? 'number' : 'text'}
|
||||
className={inputClass (false, 'h-10 w-36 min-w-0 max-w-full py-1')}
|
||||
aria-label={aria.text} lang={aria.language}
|
||||
onChange={event => patch (index, { [key]: event.target.value })}/>
|
||||
{source != null && <ResourcePickerButton source={source} disabled={condition.blank}
|
||||
values={value == null ? [] : [value]}
|
||||
onSelect={values => patch (index, { [key]: values[0] ?? '' })}/>}
|
||||
</>
|
||||
return <input value={condition[key] == null ? '' : String (condition[key])} disabled={condition.blank}
|
||||
type={(type === 'integer' || type === 'interval') ? 'number' : 'text'}
|
||||
className={inputClass (false, 'h-10 w-36 min-w-0 max-w-full py-1')}
|
||||
aria-label={aria.text} lang={aria.language}
|
||||
onChange={event => patch (index, { [key]: event.target.value })}/>
|
||||
}
|
||||
return <fieldset className="min-w-0 space-y-1">
|
||||
<legend className="flex items-center gap-2">{label ('additionalConditions')}
|
||||
@@ -193,7 +182,7 @@ const CountsAdditionalConditions = (
|
||||
</Button>
|
||||
{countsSetOperators.includes (condition.operator) ? (
|
||||
<CountsConditionValues key={`${condition.field}:${condition.operator}`}
|
||||
condition={condition} resource={resource}
|
||||
condition={condition}
|
||||
field={metadata.find (field => field.key === condition.field)!}
|
||||
definition={definition} params={params}
|
||||
maximum={Math.max (0, 100 - conditions.reduce ((sum, item, position) =>
|
||||
|
||||
+31
-46
@@ -10,19 +10,16 @@ import { useUITranslation } from '@/lib/useUITranslation'
|
||||
import { inputClass } from '@/lib/utils'
|
||||
|
||||
import type { DialogueFormControls } from '@/lib/dialogues/useDialogue'
|
||||
import type { CountsResource } from '@/lib/counts'
|
||||
import type { CountsCandidate } from '@/lib/countsDefinitions'
|
||||
import type { CountsCandidate, CountsDefinition, CountsFieldMetadata }
|
||||
from '@/lib/countsDefinitions'
|
||||
|
||||
|
||||
type CandidateSource = {
|
||||
resource: CountsResource; field: string; params?: URLSearchParams }
|
||||
|
||||
const CandidatePicker = (
|
||||
{ source, choices = [], categories = [], initial, maximum, controls, onSelect }: {
|
||||
source?: CandidateSource; choices?: CountsCandidate[]
|
||||
categories?: { value: string; label: string }[]
|
||||
const CountsCandidatePicker = (
|
||||
{ field, definition, params, initial, maximum, controls, save }: {
|
||||
field: CountsFieldMetadata; params: URLSearchParams
|
||||
definition: CountsDefinition
|
||||
initial: (string | number | boolean)[]; maximum: number
|
||||
controls: DialogueFormControls; onSelect: (values: string[]) => void },
|
||||
controls: DialogueFormControls; save: (values: string[]) => void },
|
||||
) => {
|
||||
const { label, message } = useUITranslation ('counts')
|
||||
const locale = useLocale ()
|
||||
@@ -32,58 +29,49 @@ const CandidatePicker = (
|
||||
const [search, setSearch] = useState ('')
|
||||
const [category, setCategory] = useState ('')
|
||||
const [page, setPage] = useState (1)
|
||||
const params = source?.params ?? new URLSearchParams ()
|
||||
const point = useRef (params.get ('execution_at'))
|
||||
const resource = source?.resource ?? 'posts'
|
||||
const request = new URLSearchParams ({
|
||||
field: source?.field ?? '', query: search, page: String (page) })
|
||||
const request = new URLSearchParams ({ field: field.key, query: search, page: String (page) })
|
||||
for (const key of ['timezone', 'at', 'execution_at'])
|
||||
if (params.has (key)) request.set (key, params.get (key)!)
|
||||
if (countsUseLatest (params)) request.delete ('at')
|
||||
if (category) request.set ('category', category)
|
||||
const dynamic = source != null
|
||||
const dynamic = field.candidateSource === 'tags'
|
||||
const { data, isFetching: fetching, isError: errorFlg } = useQuery ({
|
||||
queryKey: countsKeys.candidates (resource, request.toString (), locale), enabled: dynamic,
|
||||
queryKey: countsKeys.candidates (request.toString (), locale), enabled: dynamic,
|
||||
queryFn: ({ signal }) => {
|
||||
const pinned = new URLSearchParams (request)
|
||||
if (point.current != null) pinned.set ('execution_at', point.current)
|
||||
return fetchCountsCandidates (resource, pinned, locale, signal)
|
||||
}, retry: false, staleTime: 0, gcTime: 0 })
|
||||
return fetchCountsCandidates (pinned, locale, signal)
|
||||
}, retry: false })
|
||||
useEffect (() => {
|
||||
if (data != null) point.current = data.executionAt
|
||||
}, [data])
|
||||
useEffect (() => {
|
||||
controls.setActions ([{ label: label ('counts:pattern.selection'),
|
||||
onSelect: () => onSelect ([...selected]) }])
|
||||
}, [controls, label, onSelect, selected])
|
||||
const rows = dynamic ? data?.rows ?? [] : choices.filter (row =>
|
||||
onSelect: () => save ([...selected]) }])
|
||||
}, [controls, label, save, selected])
|
||||
const fixed: CountsCandidate[] = (field.choices ?? []).map (choice => ({
|
||||
value: choice.value, name: message (choice.label).text }))
|
||||
const rows = dynamic ? data?.rows ?? [] : fixed.filter (row =>
|
||||
row.name?.includes (search))
|
||||
const headings: Record<string, string> = {
|
||||
id: 'columns.resourceId', name: 'columns.name', category: 'common:categoryLabel',
|
||||
kind: 'columns.kind' }
|
||||
const referenceFlg = ['tags', 'tags_json', 'tags.id', 'members'].includes (source?.field ?? '')
|
||||
const available =
|
||||
dynamic
|
||||
? (referenceFlg ? ['id', 'name', 'category', 'kind'] : ['id', 'name']) : ['name']
|
||||
const available = dynamic ? ['id', 'name', 'category', 'kind'] : ['name']
|
||||
const visible = available.filter (column => columns.includes (column))
|
||||
const categories = definition.filterFields?.find (field =>
|
||||
field.candidateSource === 'categories')?.choices ?? []
|
||||
const cell = (row: CountsCandidate, column: string) => {
|
||||
if (column === 'category')
|
||||
return row.category == null ? '' : message (`common:categories.${row.category}`).text
|
||||
if (column === 'kind')
|
||||
{
|
||||
const key = row.kind === 'group' ? 'resources.tag_groups' : `kinds.${row.kind}`
|
||||
return row.kind == null ? '' : message (key).text
|
||||
}
|
||||
if (column === 'category') return row.category == null ? ''
|
||||
: message (`common:categories.${row.category}`).text
|
||||
if (column === 'kind') return row.kind == null ? '' : message (`kinds.${row.kind}`).text
|
||||
return column === 'id' ? row.id : row.name
|
||||
}
|
||||
return <div className="min-w-0 space-y-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<input className={inputClass (false, 'h-9 min-w-0 flex-1')}
|
||||
aria-label={message ('common:search').text} value={draft} maxLength={1000}
|
||||
onChange={event => {
|
||||
const value = event.target.value
|
||||
if (new TextEncoder ().encode (value).length <= 1000) setDraft (value)
|
||||
}}
|
||||
aria-label={message ('common:search').text} value={draft}
|
||||
onChange={event => setDraft (event.target.value)}
|
||||
onKeyDown={event => {
|
||||
if (event.key !== 'Enter') return
|
||||
event.preventDefault ()
|
||||
@@ -93,7 +81,7 @@ const CandidatePicker = (
|
||||
<Button type="button" onClick={() => { setSearch (draft); setPage (1) }}>
|
||||
{label ('common:search')}</Button>
|
||||
</div>
|
||||
{categories.length > 0 && <select className={inputClass (false, 'h-9 w-full')}
|
||||
{dynamic && <select className={inputClass (false, 'h-9 w-full')}
|
||||
aria-label={message ('common:categoryLabel').text} value={category}
|
||||
onChange={event => { setCategory (event.target.value); setPage (1) }}>
|
||||
<option value=""/>
|
||||
@@ -120,13 +108,12 @@ const CandidatePicker = (
|
||||
{visible.map (column => <th key={column} className="p-2">
|
||||
{label (headings[column])}</th>)}
|
||||
</tr></thead>
|
||||
<tbody>{rows.map (row => <tr key={`${row.kind ?? ''}:${row.value}`}
|
||||
className="border-t dark:border-slate-700">
|
||||
<tbody>{rows.map (row => <tr key={row.value} className="border-t dark:border-slate-700">
|
||||
<td className="p-2"><input type="checkbox" checked={selected.has (row.value)}
|
||||
aria-label={row.name ?? String (row.id ?? '')}
|
||||
disabled={maximum !== 1 && !selected.has (row.value) && selected.size >= maximum}
|
||||
disabled={!selected.has (row.value) && selected.size >= maximum}
|
||||
onChange={event => setSelected (previous => {
|
||||
const next = new Set (maximum === 1 ? [] : previous)
|
||||
const next = new Set (previous)
|
||||
if (event.target.checked) next.add (row.value)
|
||||
else next.delete (row.value)
|
||||
return next
|
||||
@@ -141,8 +128,7 @@ const CandidatePicker = (
|
||||
onClick={() => setPage (previous => previous - 1)}>
|
||||
{label ('common:pagination.previous')}</Button>
|
||||
<span>{page}</span>
|
||||
<Button type="button" variant="outline"
|
||||
disabled={!data?.more || page >= 100_000 || fetching}
|
||||
<Button type="button" variant="outline" disabled={!data?.more || fetching}
|
||||
onClick={() => setPage (previous => previous + 1)}>
|
||||
{label ('common:pagination.next')}</Button>
|
||||
</div>}
|
||||
@@ -150,5 +136,4 @@ const CandidatePicker = (
|
||||
}
|
||||
|
||||
|
||||
export default CandidatePicker
|
||||
export type { CandidateSource }
|
||||
export default CountsCandidatePicker
|
||||
@@ -1,22 +1,18 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import CandidatePicker from '@/components/common/CandidatePicker'
|
||||
import CountsCandidatePicker from '@/components/counts/CountsCandidatePicker'
|
||||
import DateTimeField from '@/components/common/DateTimeField'
|
||||
import ResourcePickerButton from '@/components/common/ResourcePickerButton'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { countsCandidateSource } from '@/lib/countsDefinitions'
|
||||
import useDialogue from '@/lib/dialogues/useDialogue'
|
||||
import { inputClass } from '@/lib/utils'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import type { CountsResource } from '@/lib/counts'
|
||||
import type { CountsCondition, CountsDefinition, CountsFieldMetadata }
|
||||
from '@/lib/countsDefinitions'
|
||||
|
||||
|
||||
const CountsConditionValues = (
|
||||
{ resource, condition, field, definition, params, maximum, change }: {
|
||||
resource: CountsResource
|
||||
{ condition, field, definition, params, maximum, change }: {
|
||||
condition: CountsCondition; field: CountsFieldMetadata; definition: CountsDefinition
|
||||
params: URLSearchParams; maximum: number
|
||||
change: (values: (string | number | boolean)[]) => void },
|
||||
@@ -25,21 +21,11 @@ const CountsConditionValues = (
|
||||
const dialogue = useDialogue ()
|
||||
const [draft, setDraft] = useState ('')
|
||||
const values = condition.values ?? []
|
||||
const source = countsCandidateSource (resource, field.key, params)
|
||||
const pick = () => {
|
||||
void dialogue.form ({ title: label (field.candidateSource === 'tags'
|
||||
? 'counts:pattern.link' : 'counts:pattern.pattern'), size: 'large', footerLayout: 'inline',
|
||||
body: controls => <CandidatePicker
|
||||
source={
|
||||
field.candidateSource === 'tags'
|
||||
? { resource: 'posts', field: field.key, params } : undefined}
|
||||
choices={(field.choices ?? []).map (choice => ({
|
||||
value: choice.value, name: message (choice.label).text }))}
|
||||
categories={
|
||||
field.candidateSource === 'tags'
|
||||
? definition.filterFields?.find (field =>
|
||||
field.candidateSource === 'categories')?.choices : undefined}
|
||||
initial={values} maximum={maximum} controls={controls} onSelect={change}/> })
|
||||
body: controls => <CountsCandidatePicker field={field} definition={definition}
|
||||
params={params} initial={values} maximum={maximum} controls={controls} save={change}/> })
|
||||
}
|
||||
if (condition.input === 'list' || field.candidateSource === 'tags')
|
||||
return <Button type="button" variant="outline" disabled={condition.blank} onClick={pick}>
|
||||
@@ -87,8 +73,6 @@ const CountsConditionValues = (
|
||||
onClick={() => change (values.filter ((_, position) => position !== index))}>×</Button>
|
||||
</span>)}
|
||||
{input ()}
|
||||
{source != null && <ResourcePickerButton source={source} values={values}
|
||||
maximum={maximum} disabled={condition.blank} onSelect={change}/>}
|
||||
<Button type="button" variant="outline" disabled={condition.blank || draft === ''
|
||||
|| values.length >= maximum} onClick={append}>{label ('add')}</Button>
|
||||
<Button type="button" variant="ghost" disabled={condition.blank}
|
||||
|
||||
@@ -7,7 +7,6 @@ import AnimatedCollapse from '@/components/common/AnimatedCollapse'
|
||||
import DateTimeField from '@/components/common/DateTimeField'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import HelpPopover from '@/components/common/HelpPopover'
|
||||
import ResourcePickerButton from '@/components/common/ResourcePickerButton'
|
||||
import TagInput from '@/components/common/TagInput'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { countsRequestParams, countsUseLatest, fieldsForCounts, normaliseCountsParams, readCountSort,
|
||||
@@ -224,17 +223,11 @@ const CountsControls = (
|
||||
{['resource_id', 'user_id'].map (key => <FormField key={key}
|
||||
label={label (key === 'resource_id' ? 'columns.resourceId'
|
||||
: 'fields.actor')}>
|
||||
{() => <div className="flex flex-wrap items-center gap-1">
|
||||
<input value={draft.get (key) ?? ''}
|
||||
aria-label={message (key === 'resource_id' ? 'columns.resourceId'
|
||||
: 'fields.actor').text}
|
||||
className={inputClass (false, 'w-28 py-1')}
|
||||
onChange={event => update (key, event.target.value)}/>
|
||||
<ResourcePickerButton source={{ resource,
|
||||
field: key === 'resource_id' ? key : 'user', params: draft }}
|
||||
values={draft.get (key) ? [draft.get (key)!] : []}
|
||||
onSelect={values => update (key, values[0] ?? '')}/>
|
||||
</div>}
|
||||
{() => <input value={draft.get (key) ?? ''}
|
||||
aria-label={message (key === 'resource_id' ? 'columns.resourceId'
|
||||
: 'fields.actor').text}
|
||||
className={inputClass (false, 'w-28 py-1')}
|
||||
onChange={event => update (key, event.target.value)}/>}
|
||||
</FormField>)}
|
||||
</div>
|
||||
{resource === 'posts' && <FormField label={label ('columns.tags')}>
|
||||
@@ -270,7 +263,7 @@ const CountsControls = (
|
||||
</>}
|
||||
</div>}
|
||||
</FormField>}
|
||||
<CountsAdditionalConditions resource={resource} params={draft} update={update}
|
||||
<CountsAdditionalConditions params={draft} update={update}
|
||||
conditionLabels={(labelParams != null
|
||||
&& draft.get ('current') === labelParams.get ('current')
|
||||
&& draft.get ('at') === labelParams.get ('at')
|
||||
|
||||
@@ -227,13 +227,11 @@ const DialogueProvider: FC<Props> = ({ children }) => {
|
||||
</DialogHeader>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
||||
<DialogueProvider key={active.id}>
|
||||
{active.options.body (
|
||||
formControls.current[active.id] ??= {
|
||||
{active.options.body (
|
||||
formControls.current[active.id] ??= {
|
||||
close: () => closeRequest (active.id),
|
||||
setActions: actions => setRequestActions (active.id, actions),
|
||||
confirm: options => openNestedConfirm (active.id, options) })}
|
||||
</DialogueProvider>
|
||||
</div>
|
||||
|
||||
<DialogFooter
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import ResourcePickerButton from '@/components/common/ResourcePickerButton'
|
||||
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
||||
import PostTagsField from '@/components/posts/PostTagsField'
|
||||
import PostTextField from '@/components/posts/PostTextField'
|
||||
@@ -89,12 +88,7 @@ const PostCoreDataFields: FC<PostCoreDataFieldsProps> = (
|
||||
disabled={parentPostIds.disabled}
|
||||
warnings={parentPostIds.warnings}
|
||||
errors={parentPostIds.errors}
|
||||
onChange={parentPostIds.onChange}
|
||||
after={<ResourcePickerButton
|
||||
source={{ resource: 'posts', field: 'resource_id' }}
|
||||
values={parentPostIds.value.split (/\s+/).filter (Boolean)}
|
||||
maximum={Number.MAX_SAFE_INTEGER} disabled={parentPostIds.disabled}
|
||||
onSelect={values => parentPostIds.onChange (values.join (' '))}/>}/>
|
||||
onChange={parentPostIds.onChange}/>
|
||||
</>)
|
||||
|
||||
export default PostCoreDataFields
|
||||
|
||||
@@ -34,22 +34,18 @@ const PostTextField: FC<Props> = (
|
||||
<FormField label={label} messages={errors}>
|
||||
{({ describedBy, invalid }) => (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
onBlur={onBlur}
|
||||
onChange={ev => onChange (ev.target.value)}
|
||||
aria-describedby={describedBy}
|
||||
aria-invalid={invalid}
|
||||
className={inputClass (invalid, className)}/>
|
||||
</div>
|
||||
{after}
|
||||
</div>
|
||||
<FieldWarning messages={warnings}/>
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
onBlur={onBlur}
|
||||
onChange={ev => onChange (ev.target.value)}
|
||||
aria-describedby={describedBy}
|
||||
aria-invalid={invalid}
|
||||
className={inputClass (invalid, className)}/>
|
||||
<FieldWarning messages={warnings}/>
|
||||
{after}
|
||||
</>)}
|
||||
</FormField>)
|
||||
|
||||
|
||||
@@ -89,19 +89,18 @@ export const fetchCountsDefinition = (
|
||||
.then (definition => localiseCountsDefinition (definition, locale))
|
||||
|
||||
export type CountsCandidate = {
|
||||
value: string; id?: number | string; name: string | null; category?: string | null
|
||||
value: string; id?: number; name: string | null; category?: string | null
|
||||
names?: Record<string, unknown>
|
||||
kind?: 'internal' | 'external' | 'group' }
|
||||
kind?: 'internal' | 'external' }
|
||||
export type CountsCandidatesResult = {
|
||||
rows: CountsCandidate[]; page: number; more: boolean; executionAt: string }
|
||||
|
||||
export const fetchCountsCandidates = (
|
||||
resource: CountsResource,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CountsCandidatesResult> => apiGet (
|
||||
`/counts/definitions/${encodeURIComponent (resource)}/candidates`, {
|
||||
'/counts/definitions/posts/candidates', {
|
||||
params: Object.fromEntries (params), locale, signal })
|
||||
|
||||
export const countsSetOperators = ['in', 'not_in', 'any', 'all', 'none']
|
||||
@@ -156,25 +155,3 @@ export const countsAxisFields = (
|
||||
history: boolean,
|
||||
): CountField[] => definition.axes[history ? 'history' : 'latest']
|
||||
.map (key => ({ key, label: countsFieldLabel (key) }))
|
||||
|
||||
|
||||
export const countsCandidateSource = (
|
||||
resource: CountsResource,
|
||||
field: string,
|
||||
params: URLSearchParams,
|
||||
): { resource: CountsResource; field: string; params: URLSearchParams } | undefined => {
|
||||
if (['resource_id', 'user', 'updated_by_user_id', 'file_blob_id', 'source_file_id']
|
||||
.includes (field)) return { resource, field, params }
|
||||
if (field === 'user.id') return { resource, field: 'user', params }
|
||||
if ((field === 'tags.id' && resource === 'posts')
|
||||
|| (field === 'members' && resource === 'tag_groups'))
|
||||
return { resource, field, params }
|
||||
const leaf = field.split ('.').at (-1) ?? ''
|
||||
const targets: Record<string, CountsResource> = {
|
||||
tagGroupId: 'tag_groups', tagId: 'tags', tag_id: 'tags', parent_id: 'materials',
|
||||
parents: resource === 'posts' ? 'posts' : 'tags',
|
||||
'parents.id': resource === 'posts' ? 'posts' : 'tags',
|
||||
parent_post_ids: 'posts', parent_tag_ids: 'tags' }
|
||||
const target = targets[field] ?? targets[leaf]
|
||||
return target == null ? undefined : { resource: target, field: 'resource_id', params }
|
||||
}
|
||||
|
||||
@@ -174,7 +174,6 @@ export const theatresKeys = {
|
||||
export const countsKeys = {
|
||||
definition: (resource: string, locale: LocaleCode = 'ja') =>
|
||||
['counts', 'definition', resource, locale] as const,
|
||||
candidates: (resource: string, params: string, locale: LocaleCode) =>
|
||||
['counts', 'candidates', resource, locale, params] as const,
|
||||
candidates: (params: string, locale: LocaleCode) => ['counts', 'candidates', locale, params] as const,
|
||||
index: (resource: string, params: string, locale: LocaleCode) =>
|
||||
['counts', resource, locale, params] as const }
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import ResourcePickerButton from '@/components/common/ResourcePickerButton'
|
||||
import Pagination from '@/components/common/Pagination'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import PageHead from '@/components/common/PageHead'
|
||||
@@ -97,19 +96,13 @@ const MaterialHistoryPage: FC = () => {
|
||||
<div className="space-y-3">
|
||||
<FormField label={label ('materialId')}>
|
||||
{({ invalid }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
<input
|
||||
type="text"
|
||||
value={materialIdInput}
|
||||
aria-label={message ('materialId').text}
|
||||
lang={message ('materialId').language}
|
||||
onChange={e => setMaterialIdInput (e.target.value)}
|
||||
className={inputClass (invalid, 'min-w-0 flex-1')}/>
|
||||
<ResourcePickerButton
|
||||
source={{ resource: 'materials', field: 'resource_id' }}
|
||||
values={materialIdInput === '' ? [] : [materialIdInput]}
|
||||
onSelect={values => setMaterialIdInput (values[0] ?? '')}/>
|
||||
</div>)}
|
||||
className={inputClass (invalid)}/>)}
|
||||
</FormField>
|
||||
|
||||
<FormField label={label ('tagName')}>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useState } from 'react'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import ResourcePickerButton from '@/components/common/ResourcePickerButton'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
@@ -127,17 +126,11 @@ const MaterialSyncSuppressionsPage: FC = () => {
|
||||
|
||||
<FormField label={label ('suppressions.driveFileId')}>
|
||||
{({ invalid }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
<input
|
||||
type="text"
|
||||
value={driveFileId}
|
||||
onChange={e => setDriveFileId (e.target.value)}
|
||||
className={inputClass (invalid, 'min-w-0 flex-1')}/>
|
||||
<ResourcePickerButton
|
||||
source={{ resource: 'materials', field: 'drive_file_id' }}
|
||||
values={driveFileId === '' ? [] : [driveFileId]}
|
||||
onSelect={values => setDriveFileId (values[0] ?? '')}/>
|
||||
</div>)}
|
||||
className={inputClass (invalid)}/>)}
|
||||
</FormField>
|
||||
|
||||
<FormField label={label ('suppressions.reason')}>
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする