このコミットが含まれているのは:
@@ -2,7 +2,7 @@ import SortableList from '@/components/common/SortableList'
|
|||||||
import { countsAxisFields, countsFilterFields, readCountsJSON, useCountsDefinition }
|
import { countsAxisFields, countsFilterFields, readCountsJSON, useCountsDefinition }
|
||||||
from '@/lib/countsDefinitions'
|
from '@/lib/countsDefinitions'
|
||||||
import { useClientMotion } from '@/lib/useClientMotion'
|
import { useClientMotion } from '@/lib/useClientMotion'
|
||||||
import { inputClass } from '@/lib/utils'
|
import { cn, inputClass } from '@/lib/utils'
|
||||||
import { useUITranslation } from '@/lib/useUITranslation'
|
import { useUITranslation } from '@/lib/useUITranslation'
|
||||||
|
|
||||||
import type { CountsAxis } from '@/lib/countsDefinitions'
|
import type { CountsAxis } from '@/lib/countsDefinitions'
|
||||||
@@ -43,8 +43,12 @@ const CountsAxes = ({ params, update }: {
|
|||||||
<div className="rounded border dark:border-slate-700">{content}</div>
|
<div className="rounded border dark:border-slate-700">{content}</div>
|
||||||
</fieldset>} renderItem={(id, group) => {
|
</fieldset>} renderItem={(id, group) => {
|
||||||
const axis = axes.find (axis => axis.field === id)
|
const axis = axes.find (axis => axis.field === id)
|
||||||
return <div className="flex flex-wrap items-center gap-2 text-sm">
|
return <div className={cn ('flex items-center gap-2 text-sm',
|
||||||
{group === 'available' && <span className="inline-flex gap-1">
|
group !== 'available' && 'flex-wrap')}>
|
||||||
|
<span className="min-w-0 flex-1 break-words">
|
||||||
|
{label (available.find (field => field.key === id)?.label ?? 'axis')}
|
||||||
|
</span>
|
||||||
|
{group === 'available' && <span className="ml-auto inline-flex shrink-0 gap-1">
|
||||||
{(['rows', 'cols'] as const).map (target => <button key={target} type="button"
|
{(['rows', 'cols'] as const).map (target => <button key={target} type="button"
|
||||||
className="h-7 w-7 rounded-full border border-slate-300 bg-slate-100
|
className="h-7 w-7 rounded-full border border-slate-300 bg-slate-100
|
||||||
text-xs text-slate-700 hover:bg-blue-100 hover:text-blue-900
|
text-xs text-slate-700 hover:bg-blue-100 hover:text-blue-900
|
||||||
@@ -58,7 +62,6 @@ const CountsAxes = ({ params, update }: {
|
|||||||
lang={message ('axisPlace').language} onClick={() => place (id, target)}>
|
lang={message ('axisPlace').language} onClick={() => place (id, target)}>
|
||||||
{label (target)}</button>)}
|
{label (target)}</button>)}
|
||||||
</span>}
|
</span>}
|
||||||
{label (available.find (field => field.key === id)?.label ?? 'axis')}
|
|
||||||
{group !== 'available' && <>
|
{group !== 'available' && <>
|
||||||
{countsFilterFields (definition)[id] === 'datetime' && <select
|
{countsFilterFields (definition)[id] === 'datetime' && <select
|
||||||
className={inputClass (false, 'w-auto py-0')}
|
className={inputClass (false, 'w-auto py-0')}
|
||||||
|
|||||||
@@ -64,23 +64,28 @@ const CountsControls = (
|
|||||||
execute (normaliseCountsParams (resource, next))
|
execute (normaliseCountsParams (resource, next))
|
||||||
}}>
|
}}>
|
||||||
<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-2 border-b pb-1 dark:border-slate-700">
|
||||||
{history ? <>
|
{history ? <div className="flex min-w-0 max-w-full items-center gap-1">
|
||||||
{['from', 'through'].map (key => <label key={key}
|
<DateTimeField precision="second" value={draft.get ('from') ?? ''}
|
||||||
className="flex flex-wrap items-center gap-1 text-sm">
|
aria-label={message ('from').text} lang={message ('from').language}
|
||||||
{label (key)}<DateTimeField precision="second" value={draft.get (key) ?? ''}
|
className="min-w-0 flex-1" onChange={value => update ('from', value ?? '')}/>
|
||||||
aria-label={message (key).text} onChange={value => update (key, value ?? '')}/>
|
<span className="shrink-0">{label ('periodSeparator')}</span>
|
||||||
</label>)}
|
<DateTimeField precision="second" value={draft.get ('through') ?? ''}
|
||||||
</> : <div className="flex flex-wrap items-center gap-1 text-sm">
|
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">
|
||||||
{label ('referenceAt')}<HelpPopover label={message ('referenceAt')}>
|
{label ('referenceAt')}<HelpPopover label={message ('referenceAt')}>
|
||||||
{label ('referenceHelp')}</HelpPopover>
|
{label ('referenceHelp')}</HelpPopover>
|
||||||
<label className="inline-flex items-center gap-1">
|
<div className="flex min-w-0 max-w-full items-center gap-1">
|
||||||
<input type="checkbox" checked={countsUseLatest (draft)}
|
|
||||||
onChange={event => update ('current', event.target.checked ? '1' : '0')}/>
|
|
||||||
{label ('latest')}</label>
|
|
||||||
<DateTimeField precision="second" disabled={countsUseLatest (draft)}
|
<DateTimeField precision="second" disabled={countsUseLatest (draft)}
|
||||||
|
className="min-w-0 flex-1"
|
||||||
value={draft.get ('at') ?? ''}
|
value={draft.get ('at') ?? ''}
|
||||||
aria-label={message ('referenceAt').text} lang={message ('referenceAt').language}
|
aria-label={message ('referenceAt').text} lang={message ('referenceAt').language}
|
||||||
onChange={value => update ('at', value ?? '')}/>
|
onChange={value => update ('at', value ?? '')}/>
|
||||||
|
<label className="inline-flex shrink-0 items-center gap-1">
|
||||||
|
<input type="checkbox" checked={countsUseLatest (draft)}
|
||||||
|
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">
|
<div className="flex flex-wrap items-center gap-2 border-b py-1 dark:border-slate-700">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import CountsSortHeader from '@/components/counts/CountsSortHeader'
|
|||||||
import CountsSelectableTable from '@/components/counts/CountsSelectableTable'
|
import CountsSelectableTable from '@/components/counts/CountsSelectableTable'
|
||||||
import { changeCountSort, readCountSort, selectedCountChildren,
|
import { changeCountSort, readCountSort, selectedCountChildren,
|
||||||
writeCountSort, orderedCountFields } from '@/lib/counts'
|
writeCountSort, orderedCountFields } from '@/lib/counts'
|
||||||
import { countsLogicalName, countsNestedExport,
|
import { countsLogicalName, countsNestedClipboardTable, countsNestedExport,
|
||||||
countsNestedOrder, countsNestedText } from '@/lib/countsDisplay'
|
countsNestedOrder, countsNestedText } from '@/lib/countsDisplay'
|
||||||
import { useLocale } from '@/lib/useLocale'
|
import { useLocale } from '@/lib/useLocale'
|
||||||
import { useUITranslation } from '@/lib/useUITranslation'
|
import { useUITranslation } from '@/lib/useUITranslation'
|
||||||
@@ -67,6 +67,7 @@ const CountsNestedTable = (
|
|||||||
className="relative isolate max-h-72 w-full min-w-0 overflow-auto">
|
className="relative isolate max-h-72 w-full min-w-0 overflow-auto">
|
||||||
<CountsSelectableTable family={path} columns={columns.map (field => field.key)}
|
<CountsSelectableTable family={path} columns={columns.map (field => field.key)}
|
||||||
identity={`${path}:${params}`} name={title}
|
identity={`${path}:${params}`} name={title}
|
||||||
|
copyTable={() => countsNestedClipboardTable (rows, fields, path, params, locale)}
|
||||||
className="text-xs">
|
className="text-xs">
|
||||||
<thead className="sticky top-0 z-10 bg-background text-foreground
|
<thead className="sticky top-0 z-10 bg-background text-foreground
|
||||||
dark:bg-background dark:text-foreground"><tr>
|
dark:bg-background dark:text-foreground"><tr>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import CountsSelectableTable from '@/components/counts/CountsSelectableTable'
|
|||||||
import PostThumbnailPreview from '@/components/posts/PostThumbnailPreview'
|
import PostThumbnailPreview from '@/components/posts/PostThumbnailPreview'
|
||||||
import { changeCountSort, countColumns, countsRequestParams, fieldsForCounts,
|
import { changeCountSort, countColumns, countsRequestParams, fieldsForCounts,
|
||||||
readCountSort, selectedCountColumns, writeCountSort } from '@/lib/counts'
|
readCountSort, selectedCountColumns, writeCountSort } from '@/lib/counts'
|
||||||
import { countsCellCopy, countsCellText, countsImageURL, countsNestedRows }
|
import { countsCellCopy, countsCellText, countsDetailClipboardTable,
|
||||||
from '@/lib/countsDisplay'
|
countsImageURL, countsNestedRows } from '@/lib/countsDisplay'
|
||||||
import useDialogue from '@/lib/dialogues/useDialogue'
|
import useDialogue from '@/lib/dialogues/useDialogue'
|
||||||
import { useLocale } from '@/lib/useLocale'
|
import { useLocale } from '@/lib/useLocale'
|
||||||
import { useCountsDefinition } from '@/lib/countsDefinitions'
|
import { useCountsDefinition } from '@/lib/countsDefinitions'
|
||||||
@@ -101,6 +101,9 @@ const CountsResults = (
|
|||||||
{tableColumns.length === 0 ? <p>{label ('nested.noColumns')}</p> : (
|
{tableColumns.length === 0 ? <p>{label ('nested.noColumns')}</p> : (
|
||||||
<CountsSelectableTable family={resource}
|
<CountsSelectableTable family={resource}
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
|
copyTable={() => countsDetailClipboardTable (data.versions ?? [],
|
||||||
|
columns.map (column => fields.find (field => field.key === column)!),
|
||||||
|
params, locale, rowNumbers ? (data.page - 1) * data.limit + 1 : null)}
|
||||||
identity={`${resource}:${countsRequestParams (params)}:${tableColumns.join (',')}:${
|
identity={`${resource}:${countsRequestParams (params)}:${tableColumns.join (',')}:${
|
||||||
(data.versions ?? []).map (row => row.id)}`}
|
(data.versions ?? []).map (row => row.id)}`}
|
||||||
className="text-left text-sm">
|
className="text-left text-sm">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useUITranslation } from '@/lib/useUITranslation'
|
|||||||
|
|
||||||
import type { ClipboardEvent, ReactNode } from 'react'
|
import type { ClipboardEvent, ReactNode } from 'react'
|
||||||
|
|
||||||
|
import type { CountsClipboardTable } from '@/lib/countsClipboard'
|
||||||
import type { CountsMenuAction } from '@/components/counts/CountsCellMenu'
|
import type { CountsMenuAction } from '@/components/counts/CountsCellMenu'
|
||||||
|
|
||||||
type Point = { row: number; column: number }
|
type Point = { row: number; column: number }
|
||||||
@@ -20,13 +21,14 @@ const cellAt = (table: HTMLTableElement, point: Point) =>
|
|||||||
|
|
||||||
|
|
||||||
const CountsSelectableTable = (
|
const CountsSelectableTable = (
|
||||||
{ family, columns, identity, children, className, name }: {
|
{ family, columns, identity, children, className, name, copyTable }: {
|
||||||
family: string
|
family: string
|
||||||
columns: string[]
|
columns: string[]
|
||||||
identity: string
|
identity: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
name?: { text: string; language: string } },
|
name?: { text: string; language: string }
|
||||||
|
copyTable?: () => CountsClipboardTable },
|
||||||
) => {
|
) => {
|
||||||
const id = useId ()
|
const id = useId ()
|
||||||
const root = useRef<HTMLTableElement> (null)
|
const root = useRef<HTMLTableElement> (null)
|
||||||
@@ -103,6 +105,14 @@ const CountsSelectableTable = (
|
|||||||
}
|
}
|
||||||
const clipboard = (headers = false) => {
|
const clipboard = (headers = false) => {
|
||||||
if (bounds == null || root.current == null) return null
|
if (bounds == null || root.current == null) return null
|
||||||
|
if (copyTable != null)
|
||||||
|
{
|
||||||
|
const table = copyTable ()
|
||||||
|
return countsClipboard ({
|
||||||
|
columns: table.columns.slice (bounds.left, bounds.right + 1),
|
||||||
|
rows: table.rows.slice (bounds.top, bounds.bottom + 1)
|
||||||
|
.map (row => row.slice (bounds.left, bounds.right + 1)) }, headers)
|
||||||
|
}
|
||||||
const rows: string[][] = []
|
const rows: string[][] = []
|
||||||
for (let row = bounds.top; row <= bounds.bottom; ++row)
|
for (let row = bounds.top; row <= bounds.bottom; ++row)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -269,6 +269,10 @@ export const selectedCountFields = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const defaultCountsMode = (definition: string | null): 'summary' | 'details' =>
|
||||||
|
definition === 'history' ? 'details' : 'summary'
|
||||||
|
|
||||||
|
|
||||||
export const normaliseCountsParams = (
|
export const normaliseCountsParams = (
|
||||||
resource: CountsResource,
|
resource: CountsResource,
|
||||||
source: URLSearchParams,
|
source: URLSearchParams,
|
||||||
@@ -276,10 +280,11 @@ export const normaliseCountsParams = (
|
|||||||
const params = new URLSearchParams (source)
|
const params = new URLSearchParams (source)
|
||||||
if (!params.has ('definition'))
|
if (!params.has ('definition'))
|
||||||
params.set ('definition', params.get ('basis') === 'history' ? 'history' : 'latest')
|
params.set ('definition', params.get ('basis') === 'history' ? 'history' : 'latest')
|
||||||
|
if (!params.has ('current')) params.set ('current', params.get ('at') ? '0' : '1')
|
||||||
const legacy = params.get ('mode')
|
const legacy = params.get ('mode')
|
||||||
if (['state', 'trend', 'snapshot'].includes (legacy ?? ''))
|
if (['state', 'trend', 'snapshot'].includes (legacy ?? ''))
|
||||||
params.set ('mode', legacy === 'snapshot' ? 'details' : 'summary')
|
params.set ('mode', legacy === 'snapshot' ? 'details' : 'summary')
|
||||||
if (!params.has ('mode')) params.set ('mode', 'summary')
|
if (!params.has ('mode')) params.set ('mode', defaultCountsMode (params.get ('definition')))
|
||||||
if (!params.has ('rows') && params.has ('axis'))
|
if (!params.has ('rows') && params.has ('axis'))
|
||||||
{
|
{
|
||||||
const field = params.get ('axis') === 'month' ? 'created_at' : params.get ('axis')!
|
const field = params.get ('axis') === 'month' ? 'created_at' : params.get ('axis')!
|
||||||
@@ -305,8 +310,7 @@ export const isCountsState = (params: URLSearchParams): boolean =>
|
|||||||
|
|
||||||
|
|
||||||
export const countsUseLatest = (params: URLSearchParams): boolean =>
|
export const countsUseLatest = (params: URLSearchParams): boolean =>
|
||||||
params.get ('current') === '1'
|
params.get ('current') !== '0'
|
||||||
|| (params.get ('current') !== '0' && !params.get ('at'))
|
|
||||||
|
|
||||||
|
|
||||||
export const countsRequestParams = (params: URLSearchParams): URLSearchParams => {
|
export const countsRequestParams = (params: URLSearchParams): URLSearchParams => {
|
||||||
|
|||||||
@@ -1,11 +1,67 @@
|
|||||||
export type CountsClipboard = { text: string; html: string }
|
export type CountsClipboard = { text: string; html: string }
|
||||||
|
export type CountsClipboardColumn = { label: string; children?: CountsClipboardColumn[] }
|
||||||
|
export type CountsClipboardCell = string | CountsClipboardTable
|
||||||
|
export type CountsClipboardTable = {
|
||||||
|
columns: CountsClipboardColumn[]; rows: CountsClipboardCell[][] }
|
||||||
|
|
||||||
|
|
||||||
const escapeHTML = (value: string): string => value.replace (/[&<>"']/g, character =>
|
const escapeHTML = (value: string): string => value.replace (/[&<>"']/g, character =>
|
||||||
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]!)
|
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]!)
|
||||||
|
|
||||||
|
|
||||||
export const countsClipboard = (rows: string[][], headers = false): CountsClipboard => {
|
const columnWidth = (column: CountsClipboardColumn): number =>
|
||||||
|
column.children?.reduce ((sum, child) => sum + columnWidth (child), 0) || 1
|
||||||
|
|
||||||
|
|
||||||
|
const headerDepth = (columns: CountsClipboardColumn[]): number =>
|
||||||
|
columns.length === 0 ? 0 : 1 + Math.max (0, ...columns.map (column =>
|
||||||
|
headerDepth (column.children ?? [])))
|
||||||
|
|
||||||
|
|
||||||
|
const headerRows = (columns: CountsClipboardColumn[]): string[][] => {
|
||||||
|
const width = columns.reduce ((sum, column) => sum + columnWidth (column), 0)
|
||||||
|
const rows = Array.from ({ length: headerDepth (columns) },
|
||||||
|
() => Array<string> (width).fill (''))
|
||||||
|
const place = (fields: CountsClipboardColumn[], row: number, offset: number) => {
|
||||||
|
for (const field of fields)
|
||||||
|
{
|
||||||
|
rows[row][offset] = field.label
|
||||||
|
if (field.children?.length) place (field.children, row + 1, offset)
|
||||||
|
offset += columnWidth (field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (columns.length) place (columns, 0, 0)
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const dataRows = (
|
||||||
|
table: CountsClipboardTable,
|
||||||
|
columns = table.columns,
|
||||||
|
): string[][] => {
|
||||||
|
if (columns.length === 0) return []
|
||||||
|
return table.rows.flatMap (record => {
|
||||||
|
const blocks = columns.map ((column, index) => {
|
||||||
|
const value = record[index] ?? ''
|
||||||
|
return typeof value === 'string' ? [[value]]
|
||||||
|
: dataRows (value, column.children ?? value.columns)
|
||||||
|
})
|
||||||
|
const height = Math.max (1, ...blocks.map (block => block.length))
|
||||||
|
// Sibling arrays share the parent row's height; they never form a Cartesian product.
|
||||||
|
return Array.from ({ length: height }, (_, row) => columns.flatMap ((column, index) =>
|
||||||
|
Array.from ({ length: columnWidth (column) }, (_, offset) =>
|
||||||
|
blocks[index][row]?.[offset] ?? '')))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const countsClipboard = (
|
||||||
|
source: string[][] | CountsClipboardTable,
|
||||||
|
headers = false,
|
||||||
|
): CountsClipboard => {
|
||||||
|
const headings = !Array.isArray (source) && headers ? headerRows (source.columns) : []
|
||||||
|
const rows = Array.isArray (source) ? source : [...headings, ...dataRows (source)]
|
||||||
|
const headingCount = Array.isArray (source) ? (headers ? 1 : 0) : headings.length
|
||||||
// Excel accepts quoted TSV fields only when delimiters or quotes require them.
|
// Excel accepts quoted TSV fields only when delimiters or quotes require them.
|
||||||
// Prefix formula-like text in both representations, including leading whitespace.
|
// Prefix formula-like text in both representations, including leading whitespace.
|
||||||
const safe = rows.map (row => row.map (value => /^\s*[=+@-]/.test (value)
|
const safe = rows.map (row => row.map (value => /^\s*[=+@-]/.test (value)
|
||||||
@@ -13,7 +69,7 @@ export const countsClipboard = (rows: string[][], headers = false): CountsClipbo
|
|||||||
const text = safe.map (row => row.map (value => /[\t\r\n"]/.test (value)
|
const text = safe.map (row => row.map (value => /[\t\r\n"]/.test (value)
|
||||||
? `"${value.replaceAll ('"', '""')}"` : value).join ('\t')).join ('\r\n')
|
? `"${value.replaceAll ('"', '""')}"` : value).join ('\t')).join ('\r\n')
|
||||||
const html = '<html><body><table>' + safe.map ((row, index) => '<tr>' + row.map (value => {
|
const html = '<html><body><table>' + safe.map ((row, index) => '<tr>' + row.map (value => {
|
||||||
const tag = headers && index === 0 ? 'th' : 'td'
|
const tag = index < headingCount ? 'th' : 'td'
|
||||||
// Preserve time strings, leading zeroes, and user text instead of inferring dates/formulae.
|
// Preserve time strings, leading zeroes, and user text instead of inferring dates/formulae.
|
||||||
const numeric = value.length <= 15 && /^(?:0|[1-9]\d*)(?:\.\d+)?$/.test (value)
|
const numeric = value.length <= 15 && /^(?:0|[1-9]\d*)(?:\.\d+)?$/.test (value)
|
||||||
const style = numeric ? 'white-space:pre-wrap' : 'white-space:pre-wrap;mso-number-format:"\\@"'
|
const style = numeric ? 'white-space:pre-wrap' : 'white-space:pre-wrap;mso-number-format:"\\@"'
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { localisedDateString, msToTime, originalCreatedAtString } from '@/lib/ut
|
|||||||
|
|
||||||
import type { CountColumn, CountField, CountRecord,
|
import type { CountColumn, CountField, CountRecord,
|
||||||
CountsResource, CountsVersion } from '@/lib/counts'
|
CountsResource, CountsVersion } from '@/lib/counts'
|
||||||
|
import type { CountsClipboardCell, CountsClipboardColumn, CountsClipboardTable }
|
||||||
|
from '@/lib/countsClipboard'
|
||||||
import type { CountsDefinition } from '@/lib/countsDefinitions'
|
import type { CountsDefinition } from '@/lib/countsDefinitions'
|
||||||
import type { LocaleCode } from '@/lib/locales'
|
import type { LocaleCode } from '@/lib/locales'
|
||||||
|
|
||||||
@@ -278,3 +280,61 @@ export const countsExportLabels = (
|
|||||||
labels.notRegistered = translateUIMessage ('notRegistered', 'counts', locale).text
|
labels.notRegistered = translateUIMessage ('notRegistered', 'counts', locale).text
|
||||||
return labels
|
return labels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const clipboardColumns = (
|
||||||
|
fields: CountField[],
|
||||||
|
path: string,
|
||||||
|
params: URLSearchParams,
|
||||||
|
locale: LocaleCode,
|
||||||
|
): CountsClipboardColumn[] => {
|
||||||
|
const selected = selectedCountChildren (params)
|
||||||
|
return orderedCountFields (fields, params, path)
|
||||||
|
.filter (field => selected.has (`${path}.${field.key}`))
|
||||||
|
.map (field => ({ label: translateUIMessage (field.label, 'counts', locale).text,
|
||||||
|
children: field.children == null ? undefined : clipboardColumns (
|
||||||
|
field.children, `${path}.${field.key}`, params, locale) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const countsNestedClipboardTable = (
|
||||||
|
rows: CountRecord[],
|
||||||
|
fields: CountField[],
|
||||||
|
path: string,
|
||||||
|
params: URLSearchParams,
|
||||||
|
locale: LocaleCode,
|
||||||
|
): CountsClipboardTable => {
|
||||||
|
const selected = selectedCountChildren (params)
|
||||||
|
const columns = orderedCountFields (fields, params, path)
|
||||||
|
.filter (field => selected.has (`${path}.${field.key}`))
|
||||||
|
return {
|
||||||
|
columns: clipboardColumns (fields, path, params, locale),
|
||||||
|
rows: countsNestedOrder (rows, path, params, locale).map (row => columns.map (field =>
|
||||||
|
field.children == null ? countsLogicalName (row, field.key, locale).text
|
||||||
|
: countsNestedClipboardTable (row[field.key] as CountRecord[] ?? [],
|
||||||
|
field.children, `${path}.${field.key}`, params, locale))) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const countsDetailClipboardTable = (
|
||||||
|
rows: CountsVersion[],
|
||||||
|
fields: CountField[],
|
||||||
|
params: URLSearchParams,
|
||||||
|
locale: LocaleCode,
|
||||||
|
rowStart: number | null = null,
|
||||||
|
): CountsClipboardTable => {
|
||||||
|
const columns = fields.map (field => ({
|
||||||
|
label: translateUIMessage (field.label, 'counts', locale).text,
|
||||||
|
children: field.children == null ? undefined : clipboardColumns (
|
||||||
|
field.children, field.key, params, locale) }))
|
||||||
|
if (rowStart != null)
|
||||||
|
columns.unshift ({ label: translateUIMessage ('rowNumbers', 'counts', locale).text,
|
||||||
|
children: undefined })
|
||||||
|
return { columns, rows: rows.map ((row, index) => {
|
||||||
|
const cells: CountsClipboardCell[] = fields.map (field => field.children == null
|
||||||
|
? countsCellText (row, field.key, locale, params)
|
||||||
|
: countsNestedClipboardTable (countsNestedRows (row, field.key), field.children,
|
||||||
|
field.key, params, locale))
|
||||||
|
return rowStart == null ? cells : [String (rowStart + index), ...cells]
|
||||||
|
}) }
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
"deprecated": "廃止状態",
|
"deprecated": "廃止状態",
|
||||||
"latest": "最新フラグ",
|
"latest": "最新フラグ",
|
||||||
"thumbnailURL": "サムネール元 URL",
|
"thumbnailURL": "サムネール元 URL",
|
||||||
"originalFrom": "オリジナルの投稿日時(開始境界)",
|
"originalFrom": "オリジナルの投稿日時(以降)",
|
||||||
"originalBefore": "オリジナルの投稿日時(終了境界・未満)",
|
"originalBefore": "オリジナルの投稿日時(より前)",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
"kind": "内部・外部",
|
"kind": "内部・外部",
|
||||||
"sections": "記載区間",
|
"sections": "記載区間",
|
||||||
@@ -136,8 +136,8 @@
|
|||||||
"history": "履歴",
|
"history": "履歴",
|
||||||
"referenceAt": "基準日時",
|
"referenceAt": "基準日時",
|
||||||
"referenceHelp": "指定した日時における各データの最新版を集計・表示します。空欄の場合は現在の最新版を使用します。",
|
"referenceHelp": "指定した日時における各データの最新版を集計・表示します。空欄の場合は現在の最新版を使用します。",
|
||||||
"from": "開始(以上)",
|
"from": "開始",
|
||||||
"through": "終了(以下)",
|
"through": "終了",
|
||||||
"axisItems": "項目一覧",
|
"axisItems": "項目一覧",
|
||||||
"rows": "行",
|
"rows": "行",
|
||||||
"cols": "列",
|
"cols": "列",
|
||||||
@@ -210,13 +210,14 @@
|
|||||||
"parent_post_ids": "親投稿",
|
"parent_post_ids": "親投稿",
|
||||||
"parent_tag_ids": "上位タグ",
|
"parent_tag_ids": "上位タグ",
|
||||||
"video_ms": "動画時間",
|
"video_ms": "動画時間",
|
||||||
"original_created_from": "オリジナルの投稿日時(開始境界)",
|
"original_created_from": "オリジナルの投稿日時(以降)",
|
||||||
"original_created_before": "オリジナルの投稿日時(終了境界)",
|
"original_created_before": "オリジナルの投稿日時(より前)",
|
||||||
"deprecated_at": "廃止日時",
|
"deprecated_at": "廃止日時",
|
||||||
"section_name": "節の名称",
|
"section_name": "節の名称",
|
||||||
"children": "子項目",
|
"children": "子項目",
|
||||||
"profile": "出力設定",
|
"profile": "出力設定",
|
||||||
"path": "出力パス"
|
"path": "出力パス"
|
||||||
},
|
},
|
||||||
"rowNumbers": "行番号"
|
"rowNumbers": "行番号",
|
||||||
|
"periodSeparator": "〜"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Pagination from '@/components/common/Pagination'
|
|||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import SidebarComponent from '@/components/layout/SidebarComponent'
|
import SidebarComponent from '@/components/layout/SidebarComponent'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { countsRequestParams, exportCounts, fieldsForCounts, fetchCounts,
|
import { countsRequestParams, defaultCountsMode, exportCounts, fieldsForCounts, fetchCounts,
|
||||||
isCountsState, normaliseCountsParams, selectedCountColumns } from '@/lib/counts'
|
isCountsState, normaliseCountsParams, selectedCountColumns } from '@/lib/counts'
|
||||||
import { countsExportLabels } from '@/lib/countsDisplay'
|
import { countsExportLabels } from '@/lib/countsDisplay'
|
||||||
import { CountsDefinitionContext, countsAxisFields, countsResources, countsFieldLabel,
|
import { CountsDefinitionContext, countsAxisFields, countsResources, countsFieldLabel,
|
||||||
@@ -163,8 +163,9 @@ const CountsResourceContent = ({ resource }: { resource: CountsResource }) => {
|
|||||||
<AnimatedCollapse open={expanded.has (target)} id={`counts-definition-${target}`}>
|
<AnimatedCollapse open={expanded.has (target)} id={`counts-definition-${target}`}>
|
||||||
<ul className="ml-3">
|
<ul className="ml-3">
|
||||||
{['latest', 'history'].map (definition => {
|
{['latest', 'history'].map (definition => {
|
||||||
const next = new URLSearchParams ({ definition, mode })
|
const next = new URLSearchParams ({
|
||||||
for (const key of ['timezone', 'at', 'current'])
|
definition, mode: defaultCountsMode (definition) })
|
||||||
|
for (const key of ['timezone', 'at', 'current', 'row_numbers'])
|
||||||
if (params.get (key)) next.set (key, params.get (key)!)
|
if (params.get (key)) next.set (key, params.get (key)!)
|
||||||
const active = target === resource
|
const active = target === resource
|
||||||
&& params.get ('definition') === definition
|
&& params.get ('definition') === definition
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする