このコミットが含まれているのは:
@@ -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}`)}
|
||||
</button>
|
||||
{(condition.operator === 'range' ? ['value', 'end'] as const : ['value'] as const)
|
||||
.map (key => datetime ? <DateTimeField precision="second" key={key}
|
||||
.map (key => condition.field === 'event_type' ? (
|
||||
<select key={key} value={condition[key] ?? ''} disabled={condition.blank}
|
||||
className={inputClass (false, 'w-36 py-1')}
|
||||
aria-label={message ('value').text}
|
||||
onChange={event => patch (index, { [key]: event.target.value })}>
|
||||
<option value=""/>
|
||||
{condition[key] && !countsEventTypes.some (event => event === condition[key])
|
||||
&& <option value={condition[key]}>{condition[key]}</option>}
|
||||
{countsEventTypes.map (event => <option key={event} value={event}
|
||||
lang={message (`events.${event}`).language}>
|
||||
{message (`events.${event}`).text}</option>)}
|
||||
</select>) : (datetime ? <DateTimeField precision="second" key={key}
|
||||
value={condition[key] ?? ''} disabled={condition.blank}
|
||||
aria-label={message (key === 'end' ? 'through' : 'value').text}
|
||||
onChange={value => patch (index, { [key]: value ?? '' })}/> : (
|
||||
<input key={key} value={condition[key] ?? ''} disabled={condition.blank}
|
||||
className={inputClass (false, 'w-36 py-1')}
|
||||
aria-label={message ('value').text}
|
||||
onChange={event => patch (index, { [key]: event.target.value })}/>))}
|
||||
onChange={event => patch (index, { [key]: event.target.value })}/>)))}
|
||||
<label className="inline-flex items-center gap-1">
|
||||
<input type="checkbox" checked={condition.blank ?? false}
|
||||
onChange={event => patch (index, { blank: event.target.checked })}/>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import LocalisedText from '@/components/LocalisedText'
|
||||
import SortableList from '@/components/common/SortableList'
|
||||
import { countsAxisFields, countsFilterFields, readCountsJSON, useCountsDefinition }
|
||||
import { countsAxisFields, countsAxisLabel, countsFilterFields,
|
||||
readCountsJSON, useCountsDefinition }
|
||||
from '@/lib/countsDefinitions'
|
||||
import { useClientMotion } from '@/lib/useClientMotion'
|
||||
import { cn, inputClass } from '@/lib/utils'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import type { CountsAxis } from '@/lib/countsDefinitions'
|
||||
@@ -13,6 +16,7 @@ const CountsAxes = ({ params, update }: {
|
||||
) => {
|
||||
const { label, message } = useUITranslation ('counts')
|
||||
const definition = useCountsDefinition ()
|
||||
const locale = useLocale ()
|
||||
const motion = useClientMotion ()
|
||||
const available = countsAxisFields (definition, params.get ('definition') === 'history')
|
||||
const rows = readCountsJSON<CountsAxis[]> (params, 'rows', [])
|
||||
@@ -22,8 +26,11 @@ const CountsAxes = ({ params, update }: {
|
||||
available: available.map (field => field.key).filter (id => !axes.some (
|
||||
axis => axis.field === id)),
|
||||
rows: rows.map (axis => axis.field), cols: cols.map (axis => axis.field) }
|
||||
const name = (id: string) => message (
|
||||
available.find (field => field.key === id)?.label ?? 'axis')
|
||||
const name = (id: string) => {
|
||||
const axis = axes.find (item => item.field === id)
|
||||
return axis == null ? message (available.find (field => field.key === id)?.label ?? 'axis')
|
||||
: countsAxisLabel (axis, definition, locale)
|
||||
}
|
||||
const commit = (next: Record<string, string[]>) => {
|
||||
for (const group of ['rows', 'cols'] as const)
|
||||
update (group, JSON.stringify (next[group].map (field => axes.find (
|
||||
@@ -46,7 +53,7 @@ const CountsAxes = ({ params, update }: {
|
||||
return <div className={cn ('flex items-center gap-2 text-sm',
|
||||
group !== 'available' && 'flex-wrap')}>
|
||||
<span className="min-w-0 flex-1 break-words">
|
||||
{label (available.find (field => field.key === id)?.label ?? 'axis')}
|
||||
<LocalisedText language={name (id).language}>{name (id).text}</LocalisedText>
|
||||
</span>
|
||||
{group === 'available' && <span className="ml-auto inline-flex shrink-0 gap-1">
|
||||
{(['rows', 'cols'] as const).map (target => <button key={target} type="button"
|
||||
|
||||
@@ -63,16 +63,9 @@ const CountsControls = (
|
||||
next.delete ('execution_at')
|
||||
execute (normaliseCountsParams (resource, next))
|
||||
}}>
|
||||
<div className="flex flex-wrap items-center gap-2 border-b pb-1 dark:border-slate-700">
|
||||
{history ? <div className="flex min-w-0 max-w-full items-center gap-1">
|
||||
<DateTimeField precision="second" value={draft.get ('from') ?? ''}
|
||||
aria-label={message ('from').text} lang={message ('from').language}
|
||||
className="min-w-0 flex-1" onChange={value => update ('from', value ?? '')}/>
|
||||
<span className="shrink-0">{label ('periodSeparator')}</span>
|
||||
<DateTimeField precision="second" value={draft.get ('through') ?? ''}
|
||||
aria-label={message ('through').text} lang={message ('through').language}
|
||||
className="min-w-0 flex-1" onChange={value => update ('through', value ?? '')}/>
|
||||
</div> : <div className="flex flex-wrap items-center gap-1 text-sm">
|
||||
{!history && <div className="flex flex-wrap items-center gap-2 border-b pb-1
|
||||
dark:border-slate-700">
|
||||
<div className="flex flex-wrap items-center gap-1 text-sm">
|
||||
{label ('referenceAt')}<HelpPopover label={message ('referenceAt')}>
|
||||
{label ('referenceHelp')}</HelpPopover>
|
||||
<div className="flex min-w-0 max-w-full items-center gap-1">
|
||||
@@ -86,8 +79,8 @@ const CountsControls = (
|
||||
onChange={event => update ('current', event.target.checked ? '1' : '0')}/>
|
||||
{label ('latest')}</label>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
<div className="flex flex-wrap items-center gap-2 border-b py-1 dark:border-slate-700">
|
||||
<Button type="submit" size="sm">{label ('run')}</Button>
|
||||
<Button type="button" size="sm" variant="outline" aria-expanded={open}
|
||||
@@ -132,6 +125,15 @@ const CountsControls = (
|
||||
</fieldset>
|
||||
<fieldset className="min-w-0 space-y-2" data-counts-filters>
|
||||
<legend className="font-semibold">{label ('filters')}</legend>
|
||||
{history && <div className="flex min-w-0 max-w-full items-center gap-1">
|
||||
<DateTimeField precision="second" value={draft.get ('from') ?? ''}
|
||||
aria-label={message ('from').text} lang={message ('from').language}
|
||||
className="min-w-0 flex-1" onChange={value => update ('from', value ?? '')}/>
|
||||
<span className="shrink-0">{label ('periodSeparator')}</span>
|
||||
<DateTimeField precision="second" value={draft.get ('through') ?? ''}
|
||||
aria-label={message ('through').text} lang={message ('through').language}
|
||||
className="min-w-0 flex-1" onChange={value => update ('through', value ?? '')}/>
|
||||
</div>}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['resource_id', 'user_id', 'limit'].map (key => <FormField key={key}
|
||||
label={label (key === 'resource_id' ? 'columns.resourceId'
|
||||
|
||||
@@ -88,8 +88,11 @@ const CountsResults = (
|
||||
case 'user':
|
||||
return row.createdByUser?.name ? value : (
|
||||
<LocalisedText language={message (
|
||||
row.createdByUser == null ? 'history:bot' : 'history:anonymous').language}>
|
||||
row.createdByUser == null ? 'bot' : 'history:anonymous').language}>
|
||||
{value}</LocalisedText>)
|
||||
case 'event_type':
|
||||
return <LocalisedText language={message (`events.${row.eventType}`).language}>
|
||||
{value}</LocalisedText>
|
||||
case 'category':
|
||||
return <LocalisedText language={message (`common:categories.${row.category}`).language}>
|
||||
{value}</LocalisedText>
|
||||
|
||||
@@ -3,7 +3,9 @@ import CountsRowNumber from '@/components/counts/CountsRowNumber'
|
||||
import CountsSelectableTable from '@/components/counts/CountsSelectableTable'
|
||||
import CountsSortHeader from '@/components/counts/CountsSortHeader'
|
||||
import { changeCountSort, countsRequestParams, readCountSort, writeCountSort } from '@/lib/counts'
|
||||
import { countsFieldLabel, readCountsJSON } from '@/lib/countsDefinitions'
|
||||
import { countsAxisLabel, countsFieldLabel, readCountsJSON, useCountsDefinition }
|
||||
from '@/lib/countsDefinitions'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
|
||||
import type { CountsResource, CountsResult } from '@/lib/counts'
|
||||
@@ -15,6 +17,8 @@ const CountsSummaryResults = (
|
||||
resource: CountsResource; data: CountsResult; params: URLSearchParams
|
||||
pending: boolean; update: (key: string, value: string) => void },
|
||||
) => {
|
||||
const definition = useCountsDefinition ()
|
||||
const locale = useLocale ()
|
||||
const { label, message } = useUITranslation ('counts')
|
||||
const rowNumbers = params.get ('row_numbers') === '1'
|
||||
const rows = data.rows ?? []
|
||||
@@ -28,8 +32,10 @@ const CountsSummaryResults = (
|
||||
if (cols.length === 0 && columnKeys.length === 0) columnKeys.push ('[]')
|
||||
const sorts = readCountSort (params.get ('sort'))
|
||||
const display = (value: string | number | null, axis: CountsAxis, index: number): string => {
|
||||
if (value == null) return ''
|
||||
if (value == null) return axis.field === 'user' ? message ('bot').text : ''
|
||||
const logical = data.axisLabels?.[`a${index}`]?.[String (value)]
|
||||
if (axis.field === 'user') return message ('actorWithId', {
|
||||
name: logical || message ('notRegistered').text, id: value }).text
|
||||
if (logical != null) return logical
|
||||
if (['tags', 'user', 'language_code', 'source_language_code'].includes (axis.field))
|
||||
return message ('notRegistered').text
|
||||
@@ -57,13 +63,13 @@ const CountsSummaryResults = (
|
||||
<thead className="sticky top-0 z-20"><tr>
|
||||
{rowNumbers && <CountsRowNumber/>}
|
||||
{rows.map ((axis, index) => <CountsSortHeader key={axis.field} field={`a${index}`}
|
||||
label={message (countsFieldLabel (axis.field))} sorts={sorts}
|
||||
label={countsAxisLabel (axis, definition, locale)} sorts={sorts}
|
||||
change={(key, additive) => update ('sort', writeCountSort (
|
||||
changeCountSort (sorts, key, additive)))}/>)}
|
||||
{columnKeys.map ((key, index) => {
|
||||
const values = JSON.parse (key) as (string | number | null)[]
|
||||
const name = values.map ((value, position) =>
|
||||
`${message (countsFieldLabel (cols[position].field)).text}: ${
|
||||
`${countsAxisLabel (cols[position], definition, locale).text}: ${
|
||||
display (value, cols[position], rows.length + position)}`).join (' / ')
|
||||
const sortKey = cols.length === 0 ? 'count' : `measure_${[
|
||||
...new TextEncoder ().encode (key)].map (byte =>
|
||||
@@ -83,7 +89,10 @@ const CountsSummaryResults = (
|
||||
{rowNumbers && <CountsRowNumber value={(data.page - 1) * data.limit + rowIndex + 1}/>}
|
||||
{rowValues.map ((value, index) => <td key={index}
|
||||
className="overflow-hidden break-words border px-2 py-1 dark:border-slate-700"
|
||||
data-counts-copy={display (value, rows[index], index)}>
|
||||
data-counts-copy={display (value, rows[index], index)}
|
||||
lang={(value == null && rows[index].field === 'user')
|
||||
? message ('bot').language : (rows[index].field === 'event_type'
|
||||
? message (`events.${value}`).language : undefined)}>
|
||||
{display (value, rows[index], index)}</td>)}
|
||||
{columnKeys.map (columnKey => {
|
||||
const columnValues = JSON.parse (columnKey) as (string | number | null)[]
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする