このコミットが含まれているのは:
@@ -85,7 +85,7 @@ class CountsCondition
|
|||||||
if entry['operator'] == 'axis_eq'
|
if entry['operator'] == 'axis_eq'
|
||||||
raise ArgumentError unless (entry.keys - ['field', 'operator', 'grain', 'value']).empty?
|
raise ArgumentError unless (entry.keys - ['field', 'operator', 'grain', 'value']).empty?
|
||||||
value = entry['value']
|
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
|
raise ArgumentError
|
||||||
end
|
end
|
||||||
raise ArgumentError if value.is_a?(String) && value.bytesize > 8192
|
raise ArgumentError if value.is_a?(String) && value.bytesize > 8192
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ class CountsPopulation
|
|||||||
|
|
||||||
def axis_condition(scope, entry)
|
def axis_condition(scope, entry)
|
||||||
key = entry.fetch('field')
|
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'
|
if key == 'tags'
|
||||||
scope = scope.where(snapshot_membership(entry.fetch('value')))
|
scope = scope.where(snapshot_membership(entry.fetch('value')))
|
||||||
elsif key == 'changed_field'
|
elsif key == 'changed_field'
|
||||||
@@ -130,6 +136,14 @@ class CountsPopulation
|
|||||||
end
|
end
|
||||||
apply_tags if params['tags'].present? || params['tag'].present?
|
apply_tags if params['tags'].present? || params['tag'].present?
|
||||||
conditions = QueryState.records(params, 'conditions')
|
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
|
raise ArgumentError unless conditions.is_a?(Array) && conditions.length <= 100
|
||||||
|
|
||||||
values = QueryState.records(params, 'condition_values')
|
values = QueryState.records(params, 'condition_values')
|
||||||
@@ -138,7 +152,13 @@ class CountsPopulation
|
|||||||
raise ArgumentError unless index.between?(0, conditions.length - 1)
|
raise ArgumentError unless index.between?(0, conditions.length - 1)
|
||||||
raise ArgumentError unless conditions[index]['values'] == true
|
raise ArgumentError unless conditions[index]['values'] == true
|
||||||
end
|
end
|
||||||
|
evaluated = []
|
||||||
conditions.each_with_index do |entry, index|
|
conditions.each_with_index do |entry, index|
|
||||||
|
if entry['operator'] == 'axis_eq'
|
||||||
|
next if evaluated.include?(entry)
|
||||||
|
|
||||||
|
evaluated << entry
|
||||||
|
end
|
||||||
if entry.key?('values')
|
if entry.key?('values')
|
||||||
raise ArgumentError unless entry['values'] == true
|
raise ArgumentError unless entry['values'] == true
|
||||||
|
|
||||||
@@ -147,12 +167,7 @@ class CountsPopulation
|
|||||||
end
|
end
|
||||||
apply_condition(entry)
|
apply_condition(entry)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def apply_condition(entry)
|
def apply_condition(entry)
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ class QueryState
|
|||||||
|
|
||||||
records.sort.map do |_index, record|
|
records.sort.map do |_index, record|
|
||||||
type = record.delete('value_type')
|
type = record.delete('value_type')
|
||||||
if ['drill', 'measures', 'column_order', 'condition_values'].include?(key) ||
|
if (['drill', 'measures', 'column_order', 'condition_values'].include?(key) ||
|
||||||
(key == 'conditions' && record['operator'] == 'axis_eq')
|
(key == 'conditions' && record['operator'] == 'axis_eq'))
|
||||||
raise ArgumentError unless ['null', 'string', 'number', 'boolean'].include?(type)
|
raise ArgumentError unless ['null', 'string', 'number', 'boolean'].include?(type)
|
||||||
|
|
||||||
raise ArgumentError unless type == 'null' || record.key?('value')
|
raise ArgumentError unless type == 'null' || record.key?('value')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { countsConditionInputs, countsEventTypes, countsFieldLabel, countsFilter
|
|||||||
countsSetOperators, useCountsDefinition }
|
countsSetOperators, useCountsDefinition }
|
||||||
from '@/lib/countsDefinitions'
|
from '@/lib/countsDefinitions'
|
||||||
import useDialogue from '@/lib/dialogues/useDialogue'
|
import useDialogue from '@/lib/dialogues/useDialogue'
|
||||||
import { countsSummaryValue } from '@/lib/countsDisplay'
|
import { countsConditionValue } from '@/lib/countsDisplay'
|
||||||
import { countsAxisLabel } from '@/lib/countsDefinitions'
|
import { countsAxisLabel } from '@/lib/countsDefinitions'
|
||||||
import { useLocale } from '@/lib/useLocale'
|
import { useLocale } from '@/lib/useLocale'
|
||||||
import { inputClass } from '@/lib/utils'
|
import { inputClass } from '@/lib/utils'
|
||||||
@@ -131,7 +131,7 @@ const CountsAdditionalConditions = ({ params, update }: {
|
|||||||
{
|
{
|
||||||
const options: string[] = type === 'boolean' ? ['true', 'false'] : [...countsEventTypes]
|
const options: string[] = type === 'boolean' ? ['true', 'false'] : [...countsEventTypes]
|
||||||
const namespace = type === 'boolean' ? 'truthValues' : 'events'
|
const namespace = type === 'boolean' ? 'truthValues' : 'events'
|
||||||
return <select value={condition[key] ?? ''} disabled={condition.blank}
|
return <select value={condition[key] == null ? '' : String (condition[key])} disabled={condition.blank}
|
||||||
className={inputClass (false, 'h-10 w-36 min-w-0 max-w-full py-1')}
|
className={inputClass (false, 'h-10 w-36 min-w-0 max-w-full py-1')}
|
||||||
aria-label={aria.text}
|
aria-label={aria.text}
|
||||||
lang={aria.language}
|
lang={aria.language}
|
||||||
@@ -144,10 +144,10 @@ const CountsAdditionalConditions = ({ params, update }: {
|
|||||||
}
|
}
|
||||||
if (type === 'datetime')
|
if (type === 'datetime')
|
||||||
return <DateTimeField precision="second" className="h-10 w-48 min-w-0 max-w-full py-1"
|
return <DateTimeField precision="second" className="h-10 w-48 min-w-0 max-w-full py-1"
|
||||||
value={condition[key] ?? ''} disabled={condition.blank}
|
value={condition[key] == null ? '' : String (condition[key])} disabled={condition.blank}
|
||||||
aria-label={aria.text} lang={aria.language}
|
aria-label={aria.text} lang={aria.language}
|
||||||
onChange={value => patch (index, { [key]: value ?? '' })}/>
|
onChange={value => patch (index, { [key]: value ?? '' })}/>
|
||||||
return <input value={condition[key] ?? ''} disabled={condition.blank}
|
return <input value={condition[key] == null ? '' : String (condition[key])} disabled={condition.blank}
|
||||||
type={(type === 'integer' || type === 'interval') ? 'number' : 'text'}
|
type={(type === 'integer' || type === 'interval') ? 'number' : 'text'}
|
||||||
className={inputClass (false, 'h-10 w-36 min-w-0 max-w-full py-1')}
|
className={inputClass (false, 'h-10 w-36 min-w-0 max-w-full py-1')}
|
||||||
aria-label={aria.text} lang={aria.language}
|
aria-label={aria.text} lang={aria.language}
|
||||||
@@ -157,30 +157,12 @@ const CountsAdditionalConditions = ({ params, update }: {
|
|||||||
<legend className="flex items-center gap-2">{label ('additionalConditions')}
|
<legend className="flex items-center gap-2">{label ('additionalConditions')}
|
||||||
<Button type="button" variant="outline" size="sm" disabled={fields.length === 0}
|
<Button type="button" variant="outline" size="sm" disabled={fields.length === 0}
|
||||||
onClick={() => edit ()}>{label ('add')}</Button></legend>
|
onClick={() => edit ()}>{label ('add')}</Button></legend>
|
||||||
{params.get ('tags') && params.get ('match') && <div
|
|
||||||
className="flex items-center gap-2">
|
|
||||||
<span>match = {params.get ('match')}</span>
|
|
||||||
<Button type="button" variant="ghost" size="sm"
|
|
||||||
aria-label={message ('remove').text} onClick={() => update ('match', '')}>×</Button>
|
|
||||||
</div>}
|
|
||||||
{readCountState<Record<string, string | number | boolean>[]> (
|
|
||||||
params, 'tag_refs', []).map ((reference, index, references) => (
|
|
||||||
<div key={`reference-${index}`} className="flex flex-wrap items-center gap-2">
|
|
||||||
<span>{Object.entries (reference).map (([key, value]) =>
|
|
||||||
`${key}=${value}`).join (' ')}</span>
|
|
||||||
<Button type="button" variant="ghost" size="sm"
|
|
||||||
aria-label={message ('remove').text}
|
|
||||||
onClick={() => update ('tag_refs', encodeCountState ('tag_refs',
|
|
||||||
references.filter ((_, position) => position !== index)))}>×</Button>
|
|
||||||
</div>))}
|
|
||||||
{conditions.map ((condition, index) => condition.operator === 'axis_eq' ? (
|
{conditions.map ((condition, index) => condition.operator === 'axis_eq' ? (
|
||||||
<div key={index} className="flex flex-wrap items-center gap-2 border-b py-2">
|
<div key={index} className="flex flex-wrap items-center gap-2 border-b py-2">
|
||||||
<span>{countsAxisLabel (condition, definition, locale).text}</span>
|
<span>{countsAxisLabel (condition, definition, locale).text}</span>
|
||||||
<span>{label ('operatorSymbols.eq')}</span>
|
<span>{label ('operatorSymbols.eq')}</span>
|
||||||
<span>{condition.value == null ? 'NULL' : (condition.field === 'tags'
|
<span>{countsConditionValue (condition.value, condition, locale,
|
||||||
? condition.value : countsSummaryValue (
|
params.get ('timezone') ?? 'UTC')}</span>
|
||||||
condition.value, condition, 0, { count: 0, page: 1, limit: 100 }, locale,
|
|
||||||
params.get ('timezone') ?? 'UTC'))}</span>
|
|
||||||
<Button type="button" variant="ghost" size="sm"
|
<Button type="button" variant="ghost" size="sm"
|
||||||
aria-label={message ('remove').text}
|
aria-label={message ('remove').text}
|
||||||
onClick={() => write (conditions.filter ((_, position) => position !== index))}>
|
onClick={() => write (conditions.filter ((_, position) => position !== index))}>
|
||||||
|
|||||||
@@ -226,8 +226,46 @@ const CountsControls = (
|
|||||||
</FormField>)}
|
</FormField>)}
|
||||||
</div>
|
</div>
|
||||||
{resource === 'posts' && <FormField label={label ('columns.tags')}>
|
{resource === 'posts' && <FormField label={label ('columns.tags')}>
|
||||||
{() => <TagInput includeNico includeUnused value={draft.get ('tags') ?? ''}
|
{() => <div className="flex flex-wrap items-center gap-2">
|
||||||
setValue={value => update ('tags', value)}/>}
|
<TagInput includeNico includeUnused value={draft.get ('tags') ?? ''}
|
||||||
|
setValue={value => update ('tags', value)}/>
|
||||||
|
<select value={(draft.get ('match') === 'any'
|
||||||
|
|| draft.get ('tags')?.split (/\s+/).some (token =>
|
||||||
|
token.toLowerCase () === 'type:or')) ? 'any' : 'all'}
|
||||||
|
className={inputClass (false, 'h-10 w-auto')}
|
||||||
|
aria-label={message ('columns.tags').text}
|
||||||
|
onChange={event => {
|
||||||
|
const match = event.target.value
|
||||||
|
setDraft (previous => {
|
||||||
|
const next = new URLSearchParams (previous)
|
||||||
|
next.set ('match', match)
|
||||||
|
next.set ('tags', (next.get ('tags') ?? '').split (/\s+/)
|
||||||
|
.filter (token => token.toLowerCase () !== 'type:or').join (' '))
|
||||||
|
next.delete ('page')
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<option value="all">AND</option><option value="any">OR</option>
|
||||||
|
</select>
|
||||||
|
{draft.get ('tags') && <>
|
||||||
|
{readCountState<Record<string, string | number>[]> (
|
||||||
|
draft, 'tag_refs', []).map ((reference, index) => (
|
||||||
|
<span key={index} className="text-sm">
|
||||||
|
{reference.name} ({[reference.tag_id == null ? ''
|
||||||
|
: `internal:${reference.tag_id}`,
|
||||||
|
reference.external_tag_id == null ? ''
|
||||||
|
: `external:${reference.external_tag_id}`].filter (Boolean).join (' ')})
|
||||||
|
</span>))}
|
||||||
|
<Button type="button" variant="ghost" size="sm"
|
||||||
|
aria-label={message ('remove').text} onClick={() => setDraft (previous => {
|
||||||
|
const next = new URLSearchParams (previous)
|
||||||
|
for (const key of [...next.keys ()])
|
||||||
|
if (['tags', 'tag', 'kind', 'match', 'tag_refs', 'page'].includes (key)
|
||||||
|
|| key.startsWith ('tag_refs.')) next.delete (key)
|
||||||
|
return next
|
||||||
|
})}>×</Button>
|
||||||
|
</>}
|
||||||
|
</div>}
|
||||||
</FormField>}
|
</FormField>}
|
||||||
<CountsAdditionalConditions params={draft} update={update}/>
|
<CountsAdditionalConditions params={draft} update={update}/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { CountField, CountsResource } from '@/lib/counts'
|
|||||||
|
|
||||||
export type CountsAxis = { field: string; grain?: string }
|
export type CountsAxis = { field: string; grain?: string }
|
||||||
export type CountsCondition = {
|
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)[] }
|
input?: 'single' | 'multiple' | 'list'; values?: (string | number | boolean)[] }
|
||||||
export type CountsFieldType = 'string' | 'integer' | 'datetime' | 'enum'
|
export type CountsFieldType = 'string' | 'integer' | 'datetime' | 'enum'
|
||||||
| 'json' | 'boolean' | 'interval'
|
| 'json' | 'boolean' | 'interval'
|
||||||
|
|||||||
@@ -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 = (
|
export const countsSummaryValue = (
|
||||||
value: string | number | null,
|
value: string | number | null,
|
||||||
axis: CountsAxis,
|
axis: CountsAxis,
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする