このコミットが含まれているのは:
@@ -13,6 +13,7 @@ class CountsDefaults
|
|||||||
'rows' => '', 'cols' => '', 'conditions' => '', 'tags' => '' } }
|
'rows' => '', 'cols' => '', 'conditions' => '', 'tags' => '' } }
|
||||||
details = detail_params(resource)
|
details = detail_params(resource)
|
||||||
if details
|
if details
|
||||||
|
details = details.merge('sort' => 'resource_id:asc version_no:asc')
|
||||||
language = Locale.find_by(code: locale)&.language_code || locale
|
language = Locale.find_by(code: locale)&.language_code || locale
|
||||||
name = CountsResourceDefinition.name_key(language)
|
name = CountsResourceDefinition.name_key(language)
|
||||||
aliases = CountsResourceDefinition.aliases_key(language)
|
aliases = CountsResourceDefinition.aliases_key(language)
|
||||||
@@ -72,7 +73,7 @@ class CountsDefaults
|
|||||||
entry[:definition] == params.fetch('definition', 'latest') &&
|
entry[:definition] == params.fetch('definition', 'latest') &&
|
||||||
entry[:mode] == params.fetch('mode', 'summary')
|
entry[:mode] == params.fetch('mode', 'summary')
|
||||||
end&.fetch(:params) || {}
|
end&.fetch(:params) || {}
|
||||||
if settings.key?('sort') && params.key?('rows')
|
if settings.key?('sort') && params.key?('rows') && params.fetch('mode', 'summary') == 'summary'
|
||||||
actual = QueryState.axes(params['rows'])
|
actual = QueryState.axes(params['rows'])
|
||||||
expected = QueryState.axes(settings.fetch('rows'))
|
expected = QueryState.axes(settings.fetch('rows'))
|
||||||
compatible = actual.is_a?(Array) && actual.first.is_a?(Hash) &&
|
compatible = actual.is_a?(Array) && actual.first.is_a?(Hash) &&
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class CountsTableQuery
|
|||||||
@connection = @definition.model.connection
|
@connection = @definition.model.connection
|
||||||
@axes = CountsAxes.new(population)
|
@axes = CountsAxes.new(population)
|
||||||
@page = Integer(params.fetch('page', '1'), 10)
|
@page = Integer(params.fetch('page', '1'), 10)
|
||||||
@limit = Integer(params.fetch('limit', '20'), 10)
|
@limit = Integer(params.fetch('limit', '100'), 10)
|
||||||
@mode = params.fetch('mode', 'summary')
|
@mode = params.fetch('mode', 'summary')
|
||||||
raise ArgumentError unless page.between?(1, 1_000_000) && limit.between?(1, 100)
|
raise ArgumentError unless page.between?(1, 1_000_000) && limit.between?(1, 100)
|
||||||
raise ArgumentError unless ['summary', 'details'].include?(mode)
|
raise ArgumentError unless ['summary', 'details'].include?(mode)
|
||||||
@@ -178,7 +178,7 @@ class CountsTableQuery
|
|||||||
query = population.scope.select(Arel.sql(
|
query = population.scope.select(Arel.sql(
|
||||||
"(#{ population.latest_sql }) AS counts_latest"))
|
"(#{ population.latest_sql }) AS counts_latest"))
|
||||||
order = sort_sql { detail_expression(_1) }
|
order = sort_sql { detail_expression(_1) }
|
||||||
query = query.order(Arel.sql((order + ['v.id DESC']).join(', ')))
|
query = query.order(Arel.sql((order + ['v.id ASC']).join(', ')))
|
||||||
.limit(limit).offset((page - 1) * limit)
|
.limit(limit).offset((page - 1) * limit)
|
||||||
rows = query.to_a
|
rows = query.to_a
|
||||||
if ['posts', 'tags'].include?(@definition.resource)
|
if ['posts', 'tags'].include?(@definition.resource)
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ const CountsControls = (
|
|||||||
const update = (key: string, value: string) => setDraft (previous => {
|
const update = (key: string, value: string) => setDraft (previous => {
|
||||||
const next = new URLSearchParams (previous)
|
const next = new URLSearchParams (previous)
|
||||||
setCountState (next, key, value)
|
setCountState (next, key, value)
|
||||||
|
if (key === 'limit') next.delete ('page')
|
||||||
if (next.toString () === previous.toString ()) return previous
|
if (next.toString () === previous.toString ()) return previous
|
||||||
if (['rows', 'cols'].includes (key) && next.get ('sort') !== '') next.delete ('sort')
|
if (['rows', 'cols'].includes (key) && next.get ('sort') !== '') next.delete ('sort')
|
||||||
if (key !== 'mode') return next
|
if (key !== 'mode') return next
|
||||||
@@ -153,10 +154,13 @@ const CountsControls = (
|
|||||||
<legend className="sr-only">{label ('displaySettings')}</legend>
|
<legend className="sr-only">{label ('displaySettings')}</legend>
|
||||||
<label className="inline-flex items-center gap-1 text-sm">
|
<label className="inline-flex items-center gap-1 text-sm">
|
||||||
{label ('fields.limit')}
|
{label ('fields.limit')}
|
||||||
<input value={draft.get ('limit') ?? '20'}
|
<select value={draft.get ('limit') ?? '100'}
|
||||||
aria-label={message ('fields.limit').text}
|
aria-label={message ('fields.limit').text}
|
||||||
className={inputClass (false, 'w-20 py-1')}
|
className={inputClass (false, 'w-20 py-1')}
|
||||||
onChange={event => update ('limit', event.target.value)}/>
|
onChange={event => update ('limit', event.target.value)}>
|
||||||
|
{[20, 50, 100].map (count => <option key={count} value={count}>
|
||||||
|
{label ('shared:labels.resultsCount', { count })}</option>)}
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className="inline-flex items-center gap-1 text-sm">
|
<label className="inline-flex items-center gap-1 text-sm">
|
||||||
<input type="checkbox" checked={draft.get ('row_numbers') === '1'}
|
<input type="checkbox" checked={draft.get ('row_numbers') === '1'}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const CountsResults = (
|
|||||||
const columns = selectedCountColumns (resource, params, definition)
|
const columns = selectedCountColumns (resource, params, definition)
|
||||||
const rowNumbers = params.get ('row_numbers') === '1'
|
const rowNumbers = params.get ('row_numbers') === '1'
|
||||||
const fields = fieldsForCounts (resource, definition)
|
const fields = fieldsForCounts (resource, definition)
|
||||||
const sorts = readCountSort (params.get ('sort') ?? 'created_at:desc')
|
const sorts = readCountSort (params.get ('sort') ?? 'resource_id:asc version_no:asc')
|
||||||
const sort = (key: string, additive: boolean) =>
|
const sort = (key: string, additive: boolean) =>
|
||||||
update ('sort', writeCountSort (changeCountSort (sorts, key, additive)))
|
update ('sort', writeCountSort (changeCountSort (sorts, key, additive)))
|
||||||
const cell = (row: CountsVersion, column: CountColumn) => {
|
const cell = (row: CountsVersion, column: CountColumn) => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useContext, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import SortableItem from '@/components/common/SortableItem'
|
import SortableItem from '@/components/common/SortableItem'
|
||||||
import CountsResizeHandle from '@/components/counts/CountsResizeHandle'
|
import CountsResizeHandle from '@/components/counts/CountsResizeHandle'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useUITranslation } from '@/lib/useUITranslation'
|
import { useUITranslation } from '@/lib/useUITranslation'
|
||||||
|
import { CountsTableScope, useCountsTableLayout } from '@/lib/countsTableLayout'
|
||||||
|
|
||||||
import type { CountSort } from '@/lib/counts'
|
import type { CountSort } from '@/lib/counts'
|
||||||
|
|
||||||
@@ -19,6 +20,42 @@ const CountsSortHeader = (
|
|||||||
dragId?: string; totalKind?: string },
|
dragId?: string; totalKind?: string },
|
||||||
) => {
|
) => {
|
||||||
const { message } = useUITranslation ('counts')
|
const { message } = useUITranslation ('counts')
|
||||||
|
const family = useContext (CountsTableScope)
|
||||||
|
const layout = useCountsTableLayout ()
|
||||||
|
const heading = useRef<HTMLDivElement> (null)
|
||||||
|
useEffect (() => {
|
||||||
|
if (!resizable || colSpan != null && colSpan > 1) return
|
||||||
|
const node = heading.current
|
||||||
|
const text = node?.querySelector<HTMLElement> ('[data-counts-label]')
|
||||||
|
const button = text?.closest ('button')
|
||||||
|
if (node == null || text == null || button == null) return
|
||||||
|
const canvas = document.createElement ('canvas')
|
||||||
|
const context = canvas.getContext ('2d')
|
||||||
|
if (context == null) return
|
||||||
|
const measure = () => {
|
||||||
|
context.font = getComputedStyle (text).font
|
||||||
|
const padding = (element: Element) => {
|
||||||
|
const style = getComputedStyle (element)
|
||||||
|
return parseFloat (style.paddingLeft) + parseFloat (style.paddingRight)
|
||||||
|
+ parseFloat (style.borderLeftWidth) + parseFloat (style.borderRightWidth)
|
||||||
|
}
|
||||||
|
const handles = Array.from (node.children).filter (child => child !== button)
|
||||||
|
.reduce ((sum, child) => sum + Math.max (
|
||||||
|
child.scrollWidth, child.getBoundingClientRect ().width), 0)
|
||||||
|
const resize = node.parentElement?.querySelector ('[data-counts-resize]')
|
||||||
|
const resizeWidth = resize?.getBoundingClientRect ().width ?? 0
|
||||||
|
const indicator = button.querySelector ('[data-counts-sort-indicator]')
|
||||||
|
const sortWidth = indicator?.getBoundingClientRect ().width ?? 0
|
||||||
|
const gap = parseFloat (getComputedStyle (button).columnGap) || 0
|
||||||
|
layout.fit (family, field, context.measureText (label.text).width
|
||||||
|
+ handles + padding (button) + padding (node.parentElement!)
|
||||||
|
+ sortWidth + gap + resizeWidth)
|
||||||
|
}
|
||||||
|
measure ()
|
||||||
|
let active = true
|
||||||
|
void document.fonts.ready.then (() => { if (active) measure () })
|
||||||
|
return () => { active = false }
|
||||||
|
}, [family, field, label.text, dragId, resizable, colSpan, layout])
|
||||||
const press = useRef<{ x: number; y: number
|
const press = useRef<{ x: number; y: number
|
||||||
timer: ReturnType<typeof setTimeout> } | null> (null)
|
timer: ReturnType<typeof setTimeout> } | null> (null)
|
||||||
const suppressClick = useRef (false)
|
const suppressClick = useRef (false)
|
||||||
@@ -44,7 +81,7 @@ const CountsSortHeader = (
|
|||||||
data-counts-heading={label.text} rowSpan={rowSpan} colSpan={colSpan}
|
data-counts-heading={label.text} rowSpan={rowSpan} colSpan={colSpan}
|
||||||
className="relative overflow-hidden whitespace-nowrap px-1 py-1"
|
className="relative overflow-hidden whitespace-nowrap px-1 py-1"
|
||||||
aria-sort={(sorts.length > 1 && priority >= 0) ? 'other' : state}>
|
aria-sort={(sorts.length > 1 && priority >= 0) ? 'other' : state}>
|
||||||
<div className="flex min-w-0 items-center">
|
<div ref={heading} className="flex min-w-0 items-center">
|
||||||
{shell?.handle}
|
{shell?.handle}
|
||||||
<Button type="button" variant="ghost" size="sm"
|
<Button type="button" variant="ghost" size="sm"
|
||||||
className="h-auto w-full max-w-full justify-start select-none px-1 py-0 pr-3"
|
className="h-auto w-full max-w-full justify-start select-none px-1 py-0 pr-3"
|
||||||
@@ -92,9 +129,10 @@ const CountsSortHeader = (
|
|||||||
}
|
}
|
||||||
change (field, event.ctrlKey || event.metaKey)
|
change (field, event.ctrlKey || event.metaKey)
|
||||||
}}>
|
}}>
|
||||||
<span className="truncate" lang={label.language}>{label.text}</span>
|
<span data-counts-label className="truncate" lang={label.language}>{label.text}</span>
|
||||||
{priority >= 0 && <span aria-hidden>
|
<span aria-hidden data-counts-sort-indicator className="inline-block w-10 shrink-0">
|
||||||
{direction === 'asc' ? '▲' : '▼'}{sorts.length > 1 && priority + 1}</span>}
|
{priority >= 0 && <>{direction === 'asc' ? '▲' : '▼'}
|
||||||
|
{sorts.length > 1 && priority + 1}</>}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{resizable && <CountsResizeHandle field={field} label={label}/>}
|
{resizable && <CountsResizeHandle field={field} label={label}/>}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { CountsLayoutContext, defaultCountWidths, MAX_COUNT_WIDTH, MIN_COUNT_WIDTH }
|
import { CountsLayoutContext, defaultCountWidths, MAX_COUNT_WIDTH, MIN_COUNT_WIDTH,
|
||||||
|
MAX_INITIAL_COUNT_WIDTH }
|
||||||
from '@/lib/countsTableLayout'
|
from '@/lib/countsTableLayout'
|
||||||
|
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
@@ -8,8 +9,15 @@ import type { ReactNode } from 'react'
|
|||||||
|
|
||||||
const CountsTableLayout = ({ children }: { children: ReactNode }) => {
|
const CountsTableLayout = ({ children }: { children: ReactNode }) => {
|
||||||
const [widths, setWidths] = useState<Record<string, number>> ({ })
|
const [widths, setWidths] = useState<Record<string, number>> ({ })
|
||||||
|
const [headings, setHeadings] = useState<Record<string, number>> ({ })
|
||||||
const width = (family: string, key: string) =>
|
const width = (family: string, key: string) =>
|
||||||
widths[`${family}/${key}`] ?? defaultCountWidths[key] ?? 160
|
widths[`${family}/${key}`]
|
||||||
|
?? Math.max (defaultCountWidths[key] ?? 160, headings[`${family}/${key}`] ?? 0)
|
||||||
|
const fit = (family: string, key: string, value: number) => {
|
||||||
|
const next = Math.ceil (Math.min (MAX_INITIAL_COUNT_WIDTH, value))
|
||||||
|
setHeadings (previous => previous[`${family}/${key}`] === next ? previous
|
||||||
|
: { ...previous, [`${family}/${key}`]: next })
|
||||||
|
}
|
||||||
const custom = (family: string, key: string) => widths[`${family}/${key}`] != null
|
const custom = (family: string, key: string) => widths[`${family}/${key}`] != null
|
||||||
const resize = (family: string, key: string, value: number) => {
|
const resize = (family: string, key: string, value: number) => {
|
||||||
const next = Math.round (Math.max (MIN_COUNT_WIDTH, Math.min (MAX_COUNT_WIDTH, value)))
|
const next = Math.round (Math.max (MIN_COUNT_WIDTH, Math.min (MAX_COUNT_WIDTH, value)))
|
||||||
@@ -17,7 +25,7 @@ const CountsTableLayout = ({ children }: { children: ReactNode }) => {
|
|||||||
: { ...previous, [`${family}/${key}`]: next })
|
: { ...previous, [`${family}/${key}`]: next })
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<CountsLayoutContext.Provider value={{ width, custom, resize }}>
|
<CountsLayoutContext.Provider value={{ width, custom, resize, fit }}>
|
||||||
{children}
|
{children}
|
||||||
</CountsLayoutContext.Provider>)
|
</CountsLayoutContext.Provider>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.counts-grid > tbody > tr > td:has(> .counts-nested) {
|
.counts-grid > tbody > tr > td:has(> .counts-nested) {
|
||||||
padding: 0;
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counts-nested .counts-grid > thead > tr > th,
|
||||||
|
.counts-nested .counts-grid > tbody > tr > td {
|
||||||
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.counts-nested {
|
.counts-nested {
|
||||||
|
|||||||
@@ -262,6 +262,17 @@ export const selectedCountChildren = (
|
|||||||
): Set<string> => {
|
): Set<string> => {
|
||||||
const selected = new Set (params.has ('children') ? params.get ('children')!.split (' ')
|
const selected = new Set (params.has ('children') ? params.get ('children')!.split (' ')
|
||||||
: ['tags.name', 'parents.id', 'aliases.name', 'members.tagId'])
|
: ['tags.name', 'parents.id', 'aliases.name', 'members.tagId'])
|
||||||
|
if (!params.has ('children') && fields != null)
|
||||||
|
{
|
||||||
|
const language = localeLanguage (currentViewingLocale ())
|
||||||
|
const name = fields.find (field => field.key === 'tags')?.children?.find (field =>
|
||||||
|
!field.multipleValues && field.nameLocale?.languageCode === language)
|
||||||
|
if (name != null)
|
||||||
|
{
|
||||||
|
selected.delete ('tags.name')
|
||||||
|
selected.add (`tags.${name.key}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (fields != null)
|
if (fields != null)
|
||||||
{
|
{
|
||||||
const valid = new Set (countChildPaths (fields).filter (path => path.includes ('.')))
|
const valid = new Set (countChildPaths (fields).filter (path => path.includes ('.')))
|
||||||
@@ -357,6 +368,7 @@ export const normaliseCountsParams = (
|
|||||||
definition?: CountsDefinition,
|
definition?: CountsDefinition,
|
||||||
): URLSearchParams => {
|
): URLSearchParams => {
|
||||||
const params = new URLSearchParams (source)
|
const params = new URLSearchParams (source)
|
||||||
|
if (!params.has ('limit')) params.set ('limit', '100')
|
||||||
const nameScope = params.get ('name_scope')
|
const nameScope = params.get ('name_scope')
|
||||||
if (definition?.conditionAliases != null && (nameScope == null || nameScope === 'locale'))
|
if (definition?.conditionAliases != null && (nameScope == null || nameScope === 'locale'))
|
||||||
{
|
{
|
||||||
@@ -448,9 +460,18 @@ export const normaliseCountsParams = (
|
|||||||
.filter (key => key !== 'original_created_at').join (' '))
|
.filter (key => key !== 'original_created_at').join (' '))
|
||||||
const defaults = definition?.defaults?.find (entry =>
|
const defaults = definition?.defaults?.find (entry =>
|
||||||
entry.definition === params.get ('definition') && entry.mode === params.get ('mode'))
|
entry.definition === params.get ('definition') && entry.mode === params.get ('mode'))
|
||||||
|
if (params.get ('mode') === 'details' && !params.has ('sort'))
|
||||||
|
params.set ('sort', 'resource_id:asc version_no:asc')
|
||||||
for (const { key, value } of defaults?.params ?? [])
|
for (const { key, value } of defaults?.params ?? [])
|
||||||
{
|
{
|
||||||
if (params.has (key)) continue
|
if (params.has (key)) continue
|
||||||
|
if (key === 'children' && resource === 'posts' && params.get ('mode') === 'details')
|
||||||
|
{
|
||||||
|
const name = definition?.nameColumns?.find (field => !field.multipleValues
|
||||||
|
&& field.nameLocale?.languageCode === localeLanguage (currentViewingLocale ()))
|
||||||
|
params.set (key, name == null ? value : `tags.${name.key} parents.id`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (key === 'sort')
|
if (key === 'sort')
|
||||||
{
|
{
|
||||||
const expected = defaults?.params.find (entry => entry.key === 'rows')?.value
|
const expected = defaults?.params.find (entry => entry.key === 'rows')?.value
|
||||||
@@ -541,7 +562,7 @@ export const countsExportPayload = (
|
|||||||
const payload = new URLSearchParams (params)
|
const payload = new URLSearchParams (params)
|
||||||
if (!payload.has ('children'))
|
if (!payload.has ('children'))
|
||||||
{
|
{
|
||||||
const selected = selectedCountChildren (params)
|
const selected = selectedCountChildren (params, fieldsForCounts (resource, definition))
|
||||||
payload.set ('children', countChildPaths (fieldsForCounts (resource, definition))
|
payload.set ('children', countChildPaths (fieldsForCounts (resource, definition))
|
||||||
.filter (path => selected.has (path)).join (' '))
|
.filter (path => selected.has (path)).join (' '))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createContext, useContext } from 'react'
|
|||||||
|
|
||||||
export const MIN_COUNT_WIDTH = 64
|
export const MIN_COUNT_WIDTH = 64
|
||||||
export const MAX_COUNT_WIDTH = 2000
|
export const MAX_COUNT_WIDTH = 2000
|
||||||
|
export const MAX_INITIAL_COUNT_WIDTH = 640
|
||||||
|
|
||||||
export const defaultCountWidths: Record<string, number> = {
|
export const defaultCountWidths: Record<string, number> = {
|
||||||
row_number: 72, thumbnail: 88, resource_id: 88, version_no: 88, latest: 104,
|
row_number: 72, thumbnail: 88, resource_id: 88, version_no: 88, latest: 104,
|
||||||
@@ -15,6 +16,7 @@ export const defaultCountWidths: Record<string, number> = {
|
|||||||
type Layout = {
|
type Layout = {
|
||||||
width: (family: string, key: string) => number
|
width: (family: string, key: string) => number
|
||||||
custom: (family: string, key: string) => boolean
|
custom: (family: string, key: string) => boolean
|
||||||
|
fit: (family: string, key: string, width: number) => void
|
||||||
resize: (family: string, key: string, width: number) => void }
|
resize: (family: string, key: string, width: number) => void }
|
||||||
export const CountsLayoutContext = createContext<Layout | null> (null)
|
export const CountsLayoutContext = createContext<Layout | null> (null)
|
||||||
export const CountsTableScope = createContext ('')
|
export const CountsTableScope = createContext ('')
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする