From c61d9d76e381ddf859eb7d9d00db24eabd1c20ef Mon Sep 17 00:00:00 2001 From: miteruzo Date: Mon, 26 Oct 2026 19:16:19 +0900 Subject: [PATCH] #434 --- .../app/services/counts_export_projection.rb | 2 + backend/app/services/counts_table_export.rb | 24 +++++++++++- .../counts/CountsAdditionalConditions.tsx | 18 +++++++-- frontend/src/components/counts/CountsAxes.tsx | 15 ++++++-- .../src/components/counts/CountsControls.tsx | 26 +++++++------ .../src/components/counts/CountsResults.tsx | 5 ++- .../counts/CountsSummaryResults.tsx | 19 +++++++--- frontend/src/lib/countsDefinitions.ts | 19 ++++++++++ frontend/src/lib/countsDisplay.ts | 11 ++++-- frontend/src/lib/countsState.ts | 2 +- frontend/src/locales/ja/counts.json | 13 ++++--- frontend/src/pages/CountsResourcePage.tsx | 38 +++++++++---------- 12 files changed, 138 insertions(+), 54 deletions(-) diff --git a/backend/app/services/counts_export_projection.rb b/backend/app/services/counts_export_projection.rb index 0e42632..0b15f1e 100644 --- a/backend/app/services/counts_export_projection.rb +++ b/backend/app/services/counts_export_projection.rb @@ -108,6 +108,8 @@ class CountsExportProjection end def csv_display(value, key) + return @labels.fetch('bot', '') if key == 'created_by_user' && value.nil? + return @labels.fetch("event_type:#{ value }", value) if key == 'event_type' return value ? @labels.fetch('latest', 'true') : '' if key == 'is_latest' return @labels.fetch("kind:#{ value }", value) if key == 'kind' return @labels.fetch("category:#{ value }", value) if key == 'category' diff --git a/backend/app/services/counts_table_export.rb b/backend/app/services/counts_table_export.rb index aeb0caf..3f9fbf6 100644 --- a/backend/app/services/counts_table_export.rb +++ b/backend/app/services/counts_table_export.rb @@ -30,6 +30,9 @@ class CountsTableExport @columns = params.fetch('columns', '').split(',') @headers = JSON.parse(params.fetch('headers', '[]')) raise ArgumentError unless @headers.is_a?(Array) && @headers.all? { _1.is_a?(String) } + if @summary && @headers.any? && @headers.length != @query.axes.all.length + 1 + raise ArgumentError + end unless @summary if ['posts', 'tags'].include?(resource) @@ -71,7 +74,7 @@ class CountsTableExport else values = if @summary row[:values].each_with_index.map do |value, index| - page[:axis_labels].fetch("a#{ index }", {}).fetch(value.to_s, value) + summary_value(value, index, page) end + [row[:count]] elsif @projection @projection.csv_record(record, row) @@ -91,11 +94,29 @@ class CountsTableExport private def summary_headers + return @headers if @headers.any? + labels = @labels @query.axes.all.map { labels.fetch(_1['field'], _1['field']) } + [labels.fetch('count', 'count')] end + def summary_value(value, index, page) + field = @query.axes.all.fetch(index).fetch('field') + return @labels.fetch('bot', '') if field == 'user' && value.nil? + + names = page[:axis_labels].fetch("a#{ index }", {}) + name = field == 'user' ? names[value.to_s] : names.fetch(value.to_s, value) + if field == 'user' + name = @labels.fetch('notRegistered', '') if name.nil? || name == '' + pattern = @labels.fetch('actorFormat', '{{name}} (#{{id}})') + return pattern.gsub(/\{\{(name|id)\}\}/) do + Regexp.last_match(1) == 'name' ? name.to_s : value.to_s + end + end + @labels.fetch("#{ field }:#{ value }", name) + end + def project(row) return { row: row[:values].first(@query.axes.rows.length), column: row[:values].drop(@query.axes.rows.length), count: row[:count] } if @summary @@ -148,6 +169,7 @@ class CountsTableExport end def csv_logical(key, value) + return @labels.fetch('bot', '') if key == 'created_by_user' && value.nil? return value ? @labels.fetch('latest', '') : '' if key == 'is_latest' if value.is_a?(Time) || value.is_a?(ActiveSupport::TimeWithZone) return @query.population.timezone.utc_to_local(value.utc).iso8601 diff --git a/frontend/src/components/counts/CountsAdditionalConditions.tsx b/frontend/src/components/counts/CountsAdditionalConditions.tsx index 4647210..61f2487 100644 --- a/frontend/src/components/counts/CountsAdditionalConditions.tsx +++ b/frontend/src/components/counts/CountsAdditionalConditions.tsx @@ -2,7 +2,8 @@ import { useState } from 'react' import DateTimeField from '@/components/common/DateTimeField' import { Button } from '@/components/ui/button' -import { countsFieldLabel, countsFilterFields, readCountsJSON, useCountsDefinition } +import { countsEventTypes, countsFieldLabel, countsFilterFields, + readCountsJSON, useCountsDefinition } from '@/lib/countsDefinitions' import useDialogue from '@/lib/dialogues/useDialogue' import { inputClass } from '@/lib/utils' @@ -70,14 +71,25 @@ const CountsAdditionalConditions = ({ params, update }: { {' · '}{label (`operators.${condition.operator}`)} {(condition.operator === 'range' ? ['value', 'end'] as const : ['value'] as const) - .map (key => datetime ? condition.field === 'event_type' ? ( + ) : (datetime ? patch (index, { [key]: value ?? '' })}/> : ( patch (index, { [key]: event.target.value })}/>))} + onChange={event => patch (index, { [key]: event.target.value })}/>)))}