このコミットが含まれているのは:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)[]
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
import { translateUIMessage } from '@/lib/i18n'
|
||||
import { apiGet } from '@/lib/api'
|
||||
|
||||
import type { LocaleCode } from '@/lib/locales'
|
||||
import type { CountField, CountsResource } from '@/lib/counts'
|
||||
|
||||
export type CountsAxis = { field: string; grain?: string }
|
||||
@@ -51,6 +53,23 @@ export const countsFieldLabel = (key: string): string => {
|
||||
}
|
||||
|
||||
|
||||
export const countsEventTypes = ['create', 'update', 'discard', 'restore'] as const
|
||||
|
||||
|
||||
export const countsAxisLabel = (
|
||||
axis: CountsAxis,
|
||||
definition: CountsDefinition,
|
||||
locale: LocaleCode,
|
||||
): { text: string; language: string } => {
|
||||
const field = translateUIMessage (countsFieldLabel (axis.field), 'counts', locale)
|
||||
if (axis.grain == null || definition.fields.find (item => item.key === axis.field)
|
||||
?.type !== 'datetime') return field
|
||||
return translateUIMessage ('axisWithGrain', 'counts', locale, {
|
||||
field: field.text,
|
||||
grain: translateUIMessage (`grains.${axis.grain}`, 'counts', locale).text })
|
||||
}
|
||||
|
||||
|
||||
export const countsAxisFields = (
|
||||
definition: CountsDefinition,
|
||||
history: boolean,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fieldsForCounts, orderedCountFields, selectedCountChildren,
|
||||
sortedCountChildren } from '@/lib/counts'
|
||||
import { countsEventTypes } from '@/lib/countsDefinitions'
|
||||
import { translateUIMessage } from '@/lib/i18n'
|
||||
import { localisedDateString, msToTime, originalCreatedAtString } from '@/lib/utils'
|
||||
|
||||
@@ -51,9 +52,10 @@ export const countsCellText = (
|
||||
return row.deprecated == null ? '' : date (row.deprecated)
|
||||
case 'user':
|
||||
if (row.createdByUser == null)
|
||||
return translateUIMessage ('history:bot', 'counts', locale).text
|
||||
return translateUIMessage ('bot', 'counts', locale).text
|
||||
return row.createdByUser.name
|
||||
? `${row.createdByUser.name} (#${row.createdByUser.id})`
|
||||
? translateUIMessage ('actorWithId', 'counts', locale, {
|
||||
name: row.createdByUser.name, id: row.createdByUser.id }).text
|
||||
: translateUIMessage ('history:anonymous', 'counts', locale, {
|
||||
id: row.createdByUser.id }).text
|
||||
case 'created_at': return date (row.createdAt)
|
||||
@@ -274,8 +276,11 @@ export const countsExportLabels = (
|
||||
`history:languageNames.${key}`, 'counts', locale).text
|
||||
labels.id = translateUIMessage ('columns.resourceId', 'counts', locale).text
|
||||
labels.name = translateUIMessage ('history:name', 'counts', locale).text
|
||||
for (const event of ['create', 'update', 'discard', 'restore'])
|
||||
for (const event of countsEventTypes)
|
||||
labels[`event_type:${event}`] = translateUIMessage (`events.${event}`, 'counts', locale).text
|
||||
labels.bot = translateUIMessage ('bot', 'counts', locale).text
|
||||
labels.actorFormat = translateUIMessage ('actorWithId', 'counts', locale, {
|
||||
name: '{{name}}', id: '{{id}}' }).text
|
||||
labels.latest = translateUIMessage ('latestValue', 'counts', locale).text
|
||||
labels.notRegistered = translateUIMessage ('notRegistered', 'counts', locale).text
|
||||
return labels
|
||||
|
||||
@@ -49,7 +49,7 @@ export const countsSummaryCell = (
|
||||
switch (key)
|
||||
{
|
||||
case 'group': return row.key == null
|
||||
? translateUIMessage ('history:bot', 'counts', locale)
|
||||
? translateUIMessage ('bot', 'counts', locale)
|
||||
: { text: row.name ?? String (row.key) }
|
||||
case 'count': return { text: row.count == null ? '—' : String (row.count) }
|
||||
case 'tag_id': return { text: String (row.tagId) }
|
||||
|
||||
@@ -173,10 +173,10 @@
|
||||
"range": "範囲"
|
||||
},
|
||||
"events": {
|
||||
"create": "作成",
|
||||
"update": "更新",
|
||||
"discard": "破棄",
|
||||
"restore": "復元"
|
||||
"create": "起票",
|
||||
"update": "異動",
|
||||
"discard": "除票",
|
||||
"restore": "恢復"
|
||||
},
|
||||
"attributes": {
|
||||
"sourceLanguage": "翻訳元の言語",
|
||||
@@ -219,5 +219,8 @@
|
||||
"path": "出力パス"
|
||||
},
|
||||
"rowNumbers": "行番号",
|
||||
"periodSeparator": "〜"
|
||||
"periodSeparator": "〜",
|
||||
"bot": "$t(history:bot)",
|
||||
"actorWithId": "{{name}} (#{{id}})",
|
||||
"axisWithGrain": "{{field}}({{grain}})"
|
||||
}
|
||||
|
||||
@@ -17,11 +17,12 @@ import { Button } from '@/components/ui/button'
|
||||
import { countsRequestParams, defaultCountsMode, exportCounts, fieldsForCounts, fetchCounts,
|
||||
isCountsState, normaliseCountsParams, selectedCountColumns } from '@/lib/counts'
|
||||
import { countsExportLabels } from '@/lib/countsDisplay'
|
||||
import { CountsDefinitionContext, countsAxisFields, countsResources, countsFieldLabel,
|
||||
import { CountsDefinitionContext, countsAxisFields, countsAxisLabel,
|
||||
countsResources, countsFieldLabel,
|
||||
fetchCountsDefinition, readCountsJSON, useCountsDefinition }
|
||||
from '@/lib/countsDefinitions'
|
||||
import { countsKeys } from '@/lib/queryKeys'
|
||||
import { cn, inputClass } from '@/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useClientMotion } from '@/lib/useClientMotion'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { useUITranslation } from '@/lib/useUITranslation'
|
||||
@@ -54,7 +55,6 @@ const CountsResourceContent = ({ resource }: { resource: CountsResource }) => {
|
||||
const state = isCountsState (params)
|
||||
const cacheParams = new URLSearchParams (request)
|
||||
cacheParams.set ('definition', state ? 'latest' : 'history')
|
||||
const [format, setFormat] = useState<'json' | 'csv'> ('json')
|
||||
const [exporting, setExporting] = useState (false)
|
||||
const [exportError, setExportError] = useState (false)
|
||||
const { data, isPending, isError, isPlaceholderData, refetch } = useQuery ({
|
||||
@@ -100,20 +100,25 @@ const CountsResourceContent = ({ resource }: { resource: CountsResource }) => {
|
||||
if (key === 'sort') next.delete ('page')
|
||||
setParams (next)
|
||||
}
|
||||
const download = async () => {
|
||||
const download = async (format: 'csv' | 'json') => {
|
||||
setExporting (true)
|
||||
setExportError (false)
|
||||
try
|
||||
{
|
||||
const axes = [...readCountsJSON<CountsAxis[]> (params, 'rows', []),
|
||||
...readCountsJSON<CountsAxis[]> (params, 'cols', [])]
|
||||
const fields = mode === 'summary' ? [
|
||||
...[...readCountsJSON<CountsAxis[]> (params, 'rows', []),
|
||||
...readCountsJSON<CountsAxis[]> (params, 'cols', [])].map (axis => ({
|
||||
...axes.map (axis => ({
|
||||
key: axis.field, label: countsFieldLabel (axis.field) })),
|
||||
{ key: 'count', label: 'count' }]
|
||||
: selectedCountColumns (resource, params, definition).map (column =>
|
||||
fieldsForCounts (resource, definition).find (field => field.key === column)!)
|
||||
const headers = mode === 'summary'
|
||||
? [...axes.map (axis => countsAxisLabel (axis, definition, locale).text),
|
||||
message ('count').text]
|
||||
: fields.map (field => message (field.label).text)
|
||||
await exportCounts (resource, params, locale, format,
|
||||
fields.map (field => field.key), fields.map (field => message (field.label).text),
|
||||
fields.map (field => field.key), headers,
|
||||
{ ...countsExportLabels (resource, locale, definition),
|
||||
...Object.fromEntries (countsAxisFields (definition, !state).map (field => [
|
||||
field.key, message (field.label).text])), count: message ('count').text },
|
||||
@@ -193,20 +198,15 @@ const CountsResourceContent = ({ resource }: { resource: CountsResource }) => {
|
||||
execute={next => {
|
||||
if (next.toString () === canonical) void refetch ()
|
||||
else setParams (next)
|
||||
}} actions={<>
|
||||
<select value={format} aria-label={message ('exportFormat').text}
|
||||
lang={message ('exportFormat').language}
|
||||
className={inputClass (false, 'w-auto py-1')}
|
||||
onChange={event => setFormat (event.target.value as 'json' | 'csv')}>
|
||||
{(['json', 'csv'] as const).map (value => (
|
||||
<option key={value} value={value}>
|
||||
{message (`exportFormats.${value}`).text}</option>))}
|
||||
</select>
|
||||
<Button type="button" variant="outline" size="sm"
|
||||
}} actions={<div className="ml-auto flex shrink-0 items-center gap-2">
|
||||
{(['csv', 'json'] as const).map (format => <Button key={format}
|
||||
type="button" variant="outline" size="sm"
|
||||
disabled={data == null || isError || isPlaceholderData
|
||||
|| exporting || !params.get ('execution_at')}
|
||||
onClick={() => void download ()}>{label ('export')}</Button>
|
||||
</>}/>
|
||||
onClick={() => void download (format)}>
|
||||
{label (`exportFormats.${format}`)}</Button>)}
|
||||
</div>}/>
|
||||
|
||||
{exportError && <p role="alert">{label ('common:fetchError')}</p>}
|
||||
</div>
|
||||
{isPending && <p>{label ('shared:messages.loadingEllipsis')}</p>}
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする