From 7090f63ab6746b52a0abd23ae92c75b71c4b5b02 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Thu, 29 Oct 2026 12:30:31 +0900 Subject: [PATCH] #434 --- backend/app/services/counts_condition.rb | 2 +- backend/app/services/counts_population.rb | 29 +++++++++---- backend/app/services/query_state.rb | 4 +- .../counts/CountsAdditionalConditions.tsx | 30 +++---------- .../src/components/counts/CountsControls.tsx | 42 ++++++++++++++++++- frontend/src/lib/countsDefinitions.ts | 2 +- frontend/src/lib/countsDisplay.ts | 19 +++++++++ 7 files changed, 91 insertions(+), 37 deletions(-) diff --git a/backend/app/services/counts_condition.rb b/backend/app/services/counts_condition.rb index 2f44dce..0df9646 100644 --- a/backend/app/services/counts_condition.rb +++ b/backend/app/services/counts_condition.rb @@ -85,7 +85,7 @@ class CountsCondition if entry['operator'] == 'axis_eq' raise ArgumentError unless (entry.keys - ['field', 'operator', 'grain', 'value']).empty? value = entry['value'] - unless value.nil? || value.is_a?(String) || value.is_a?(Numeric) + unless value.nil? || value.is_a?(String) || value.is_a?(Numeric) || [true, false].include?(value) raise ArgumentError end raise ArgumentError if value.is_a?(String) && value.bytesize > 8192 diff --git a/backend/app/services/counts_population.rb b/backend/app/services/counts_population.rb index cb3493a..e71ed48 100644 --- a/backend/app/services/counts_population.rb +++ b/backend/app/services/counts_population.rb @@ -64,11 +64,17 @@ class CountsPopulation def axis_condition(scope, entry) key = entry.fetch('field') + raise ArgumentError unless definition.axes(history).include?(key) + if entry.key?('grain') + unless definition.fields[key] == :datetime && CountsAxes::GRAINS.include?(entry['grain']) + raise ArgumentError + end + end if key == 'tags' scope = scope.where(snapshot_membership(entry.fetch('value'))) elsif key == 'changed_field' raise ArgumentError unless history && definition.changes.include?(entry['value']) - + scope = scope.where(Arel.sql(changed_sql(entry['value']))) elsif definition.fields[key] == :datetime && entry['grain'] scope = scope.where(definition.expression(key) => nil) if entry['value'].nil? @@ -79,7 +85,7 @@ class CountsPopulation end else raise ArgumentError unless definition.axes(history).include?(key) - + scope = scope.where(definition.expression(key) => entry['value']) end scope @@ -130,6 +136,14 @@ class CountsPopulation end apply_tags if params['tags'].present? || params['tag'].present? conditions = QueryState.records(params, 'conditions') + drills = QueryState.records(params, 'drill') + raise ArgumentError unless drills.length <= 100 + drills.each do |entry| + raise ArgumentError unless (entry.keys - ['field', 'grain', 'value']).empty? + + condition = entry.merge('operator' => 'axis_eq') + conditions << condition unless conditions.include?(condition) + end raise ArgumentError unless conditions.is_a?(Array) && conditions.length <= 100 values = QueryState.records(params, 'condition_values') @@ -138,7 +152,13 @@ class CountsPopulation raise ArgumentError unless index.between?(0, conditions.length - 1) raise ArgumentError unless conditions[index]['values'] == true end + evaluated = [] conditions.each_with_index do |entry, index| + if entry['operator'] == 'axis_eq' + next if evaluated.include?(entry) + + evaluated << entry + end if entry.key?('values') raise ArgumentError unless entry['values'] == true @@ -147,12 +167,7 @@ class CountsPopulation end apply_condition(entry) end - drills = QueryState.records(params, 'drill') - raise ArgumentError unless drills.is_a?(Array) && drills.length <= 100 - drills.each do |entry| - @scope = axis_condition(@scope, entry) - end end def apply_condition(entry) diff --git a/backend/app/services/query_state.rb b/backend/app/services/query_state.rb index b508b81..3d4a4f6 100644 --- a/backend/app/services/query_state.rb +++ b/backend/app/services/query_state.rb @@ -57,8 +57,8 @@ class QueryState records.sort.map do |_index, record| type = record.delete('value_type') - if ['drill', 'measures', 'column_order', 'condition_values'].include?(key) || - (key == 'conditions' && record['operator'] == 'axis_eq') + if (['drill', 'measures', 'column_order', 'condition_values'].include?(key) || + (key == 'conditions' && record['operator'] == 'axis_eq')) raise ArgumentError unless ['null', 'string', 'number', 'boolean'].include?(type) raise ArgumentError unless type == 'null' || record.key?('value') diff --git a/frontend/src/components/counts/CountsAdditionalConditions.tsx b/frontend/src/components/counts/CountsAdditionalConditions.tsx index e2e9049..5048195 100644 --- a/frontend/src/components/counts/CountsAdditionalConditions.tsx +++ b/frontend/src/components/counts/CountsAdditionalConditions.tsx @@ -7,7 +7,7 @@ import { countsConditionInputs, countsEventTypes, countsFieldLabel, countsFilter countsSetOperators, useCountsDefinition } from '@/lib/countsDefinitions' import useDialogue from '@/lib/dialogues/useDialogue' -import { countsSummaryValue } from '@/lib/countsDisplay' +import { countsConditionValue } from '@/lib/countsDisplay' import { countsAxisLabel } from '@/lib/countsDefinitions' import { useLocale } from '@/lib/useLocale' import { inputClass } from '@/lib/utils' @@ -131,7 +131,7 @@ const CountsAdditionalConditions = ({ params, update }: { { const options: string[] = type === 'boolean' ? ['true', 'false'] : [...countsEventTypes] const namespace = type === 'boolean' ? 'truthValues' : 'events' - return {label ('additionalConditions')} - {params.get ('tags') && params.get ('match') &&
- match = {params.get ('match')} - -
} - {readCountState[]> ( - params, 'tag_refs', []).map ((reference, index, references) => ( -
- {Object.entries (reference).map (([key, value]) => - `${key}=${value}`).join (' ')} - -
))} {conditions.map ((condition, index) => condition.operator === 'axis_eq' ? (
{countsAxisLabel (condition, definition, locale).text} {label ('operatorSymbols.eq')} - {condition.value == null ? 'NULL' : (condition.field === 'tags' - ? condition.value : countsSummaryValue ( - condition.value, condition, 0, { count: 0, page: 1, limit: 100 }, locale, - params.get ('timezone') ?? 'UTC'))} + {countsConditionValue (condition.value, condition, locale, + params.get ('timezone') ?? 'UTC')}
{resource === 'posts' && - {() => update ('tags', value)}/>} + {() =>
+ update ('tags', value)}/> + + {draft.get ('tags') && <> + {readCountState[]> ( + draft, 'tag_refs', []).map ((reference, index) => ( + + {reference.name} ({[reference.tag_id == null ? '' + : `internal:${reference.tag_id}`, + reference.external_tag_id == null ? '' + : `external:${reference.external_tag_id}`].filter (Boolean).join (' ')}) + ))} + + } +
}
} diff --git a/frontend/src/lib/countsDefinitions.ts b/frontend/src/lib/countsDefinitions.ts index 645d897..ab9545b 100644 --- a/frontend/src/lib/countsDefinitions.ts +++ b/frontend/src/lib/countsDefinitions.ts @@ -8,7 +8,7 @@ import type { CountField, CountsResource } from '@/lib/counts' export type CountsAxis = { field: string; grain?: string } export type CountsCondition = { - field: string; operator: string; grain?: string; value?: string; end?: string; blank?: boolean + field: string; operator: string; grain?: string; value?: string | number | boolean | null; end?: string; blank?: boolean input?: 'single' | 'multiple' | 'list'; values?: (string | number | boolean)[] } export type CountsFieldType = 'string' | 'integer' | 'datetime' | 'enum' | 'json' | 'boolean' | 'interval' diff --git a/frontend/src/lib/countsDisplay.ts b/frontend/src/lib/countsDisplay.ts index 69b28e6..9f35998 100644 --- a/frontend/src/lib/countsDisplay.ts +++ b/frontend/src/lib/countsDisplay.ts @@ -445,6 +445,25 @@ const countsDateValue = ( } +export const countsConditionValue = ( + value: string | number | boolean | null | undefined, + axis: CountsAxis, locale: LocaleCode, timezone: string, +): string => { + if (value == null) return 'NULL' + if (typeof value === 'boolean') + return translateUIMessage (`truthValues.${value}`, 'counts', locale).text + if (axis.grain != null) + return countsDateValue (String (value), axis.grain, locale, timezone) ?? String (value) + if (axis.field === 'event_type') + return translateUIMessage (`events.${value}`, 'counts', locale).text + if (axis.field === 'category' || axis.field === 'tag_category') + return translateUIMessage (`common:categories.${value}`, 'counts', locale).text + if (['language_code', 'source_language_code'].includes (axis.field)) + return languageUIMessage (String (value), locale).text + return String (value) +} + + export const countsSummaryValue = ( value: string | number | null, axis: CountsAxis,