このコミットが含まれているのは:
@@ -4,6 +4,7 @@ import { SortableContext, arrayMove, horizontalListSortingStrategy,
|
||||
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
||||
import { useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
|
||||
import { useClientMotion } from '@/lib/useClientMotion'
|
||||
|
||||
@@ -55,11 +56,16 @@ const SortableColumns = ({ ids, fixed = [], name, onChange, children }: {
|
||||
<SortableContext items={ids} strategy={horizontalListSortingStrategy}>
|
||||
{children (ids)}
|
||||
</SortableContext>
|
||||
{typeof document !== 'undefined' && createPortal (<DragOverlay
|
||||
{typeof document !== 'undefined' && createPortal (<DragOverlay adjustScale={false}
|
||||
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
|
||||
dropAnimation={motion.reduced ? null : {
|
||||
duration: motion.duration ('drop'), easing: motion.easing }}>
|
||||
duration: motion.duration ('drop'), easing: motion.easing,
|
||||
keyframes: ({ transform }) => [
|
||||
{ transform: CSS.Translate.toString (transform.initial) },
|
||||
{ transform: CSS.Translate.toString (transform.final) }] }}>
|
||||
{active != null && <div lang={name (active).language}
|
||||
className="rounded border border-blue-500 bg-white p-2 text-slate-900 shadow-lg
|
||||
className="w-max max-w-[min(20rem,calc(100vw_-_2rem))] truncate rounded
|
||||
border border-blue-500 bg-white px-2 py-1 text-sm text-slate-900 shadow-lg
|
||||
dark:border-blue-400 dark:bg-slate-800 dark:text-slate-100">{name (active).text}</div>}
|
||||
</DragOverlay>, document.body)}
|
||||
</DndContext>
|
||||
|
||||
@@ -40,7 +40,7 @@ const SortableItem = ({ id, name, fixed, children, moveGroup, handleId, renderSh
|
||||
}
|
||||
else item.listeners?.onKeyDown?.(event)
|
||||
}}><span aria-hidden>⠿</span></button>
|
||||
const style = { transform: CSS.Transform.toString (item.transform), transition: item.transition }
|
||||
const style = { transform: CSS.Translate.toString (item.transform), transition: item.transition }
|
||||
if (renderShell != null)
|
||||
return renderShell ({ ref: item.setNodeRef, style, handle, dragging: item.isDragging })
|
||||
return <div ref={item.setNodeRef} style={style}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SortableContext, arrayMove, rectSortingStrategy,
|
||||
sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
||||
import { useId, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
|
||||
import SortableItem from '@/components/common/SortableItem'
|
||||
import { useClientMotion } from '@/lib/useClientMotion'
|
||||
@@ -29,8 +30,10 @@ const Group = ({ id, children }: { id: string; children: ReactNode }) => {
|
||||
}
|
||||
|
||||
|
||||
const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed = [] }: {
|
||||
const SortableList = (
|
||||
{ groups, name, handleName, renderItem, renderGroup, onChange, fixed = [] }: {
|
||||
groups: Groups; name: (id: string) => Name
|
||||
handleName?: (id: string) => Name
|
||||
renderItem: (id: string, group: string) => ReactNode
|
||||
renderGroup: (id: string, content: ReactNode) => ReactNode
|
||||
onChange: (groups: Groups) => void; fixed?: string[] },
|
||||
@@ -39,6 +42,7 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
|
||||
const scope = useId ()
|
||||
const [active, setActive] = useState<string | null> (null)
|
||||
const motion = useClientMotion ()
|
||||
const accessibleName = handleName ?? name
|
||||
const sensors = useSensors (
|
||||
useSensor (MouseSensor, { activationConstraint: { distance: 4 } }),
|
||||
useSensor (TouchSensor, { activationConstraint: { delay: 350, tolerance: 8 } }),
|
||||
@@ -84,13 +88,14 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
|
||||
}
|
||||
return <DndContext sensors={sensors} collisionDetection={closestCenter}
|
||||
accessibility={{ announcements: {
|
||||
onDragStart: event => name (String (event.active.id)).text,
|
||||
onDragStart: event => accessibleName (String (event.active.id)).text,
|
||||
onDragOver: event => event.over == null ? undefined : (
|
||||
event.over.id.toString ().startsWith ('group:') ? undefined
|
||||
: name (String (event.over.id)).text),
|
||||
: accessibleName (String (event.over.id)).text),
|
||||
onDragMove: () => undefined, onDragEnd: () => undefined,
|
||||
onDragCancel: () => undefined },
|
||||
screenReaderInstructions: { draggable: active == null ? '' : name (active).text } }}
|
||||
screenReaderInstructions: {
|
||||
draggable: active == null ? '' : accessibleName (active).text } }}
|
||||
onDragStart={event => {
|
||||
const id = String (event.active.id)
|
||||
drag.current = { id, groups }
|
||||
@@ -108,17 +113,23 @@ const SortableList = ({ groups, name, renderItem, renderGroup, onChange, fixed =
|
||||
{Object.entries (groups).map (([group, ids]) => <div key={group}>
|
||||
{renderGroup (group, <SortableContext items={ids} strategy={rectSortingStrategy}>
|
||||
<Group id={group}>{ids.map (id => <SortableItem key={id} id={id}
|
||||
name={name (id)} fixed={fixed.includes (id)} handleId={`${scope}-${id}`}
|
||||
name={accessibleName (id)} fixed={fixed.includes (id)} handleId={`${scope}-${id}`}
|
||||
moveGroup={direction => moveGroup (id, direction)}>
|
||||
{renderItem (id, group)}
|
||||
</SortableItem>)}</Group>
|
||||
</SortableContext>)}
|
||||
</div>)}
|
||||
{typeof document !== 'undefined' && createPortal (
|
||||
<DragOverlay dropAnimation={motion.reduced ? null : {
|
||||
duration: motion.duration ('drop'), easing: motion.easing }}>
|
||||
<DragOverlay adjustScale={false}
|
||||
style={{ width: 'max-content', height: 'auto', maxWidth: 'calc(100vw - 2rem)' }}
|
||||
dropAnimation={motion.reduced ? null : {
|
||||
duration: motion.duration ('drop'), easing: motion.easing,
|
||||
keyframes: ({ transform }) => [
|
||||
{ transform: CSS.Translate.toString (transform.initial) },
|
||||
{ transform: CSS.Translate.toString (transform.final) }] }}>
|
||||
{active == null ? null : <div lang={name (active).language}
|
||||
className="rounded border border-blue-500 bg-white px-3 py-2 text-sm
|
||||
className="w-max max-w-[min(20rem,calc(100vw_-_2rem))] truncate rounded
|
||||
border border-blue-500 bg-white px-2 py-1 text-sm
|
||||
text-slate-900 shadow-lg dark:border-blue-400 dark:bg-slate-800 dark:text-slate-100">
|
||||
{name (active).text}</div>}
|
||||
</DragOverlay>, document.body)}
|
||||
|
||||
@@ -48,7 +48,8 @@ const ConditionEditor = (
|
||||
const operator = fields.find (item => item.key === field)?.operators[0]
|
||||
if (operator != null) setCondition ({ field, operator })
|
||||
}}>
|
||||
{fields.filter (field => field.operators.length > 0).map (field => (
|
||||
{fields.filter (item => item.operators.length > 0
|
||||
&& (!item.legacy || item.key === field)).map (field => (
|
||||
<option key={field.key} value={field.key}>{fieldName (field.key)}</option>))}
|
||||
</select>
|
||||
<select value={operator} aria-label={message ('operator').text}
|
||||
@@ -98,7 +99,7 @@ const CountsAdditionalConditions = ({ params, update }: {
|
||||
const dialogue = useDialogue ()
|
||||
const conditions = readCountState<CountsCondition[]> (params, 'conditions', [])
|
||||
const metadata = definition.filterFields ?? definition.fields
|
||||
const fields = metadata.filter (field => field.operators.length > 0)
|
||||
const fields = metadata.filter (field => field.operators.length > 0 && !field.legacy)
|
||||
const fieldName = (key: string) => (metadata.find (item => item.key === key)?.labels
|
||||
?? [countsFieldLabel (key)]).map (label => message (
|
||||
label, metadata.find (item => item.key === key)?.labelValues).text).join (' › ')
|
||||
|
||||
@@ -69,7 +69,9 @@ const FieldGroup = ({ fields, path, params, enabled, selected, select, update }:
|
||||
const prefix = path ? `${path}.` : ''
|
||||
return <SortableList groups={{ items: ordered.map (field => field.key) }}
|
||||
fixed={path === '' ? ['thumbnail'] : []}
|
||||
name={key => message ('reorder', {
|
||||
name={key => message (fields.find (field => field.key === key)!.label,
|
||||
fields.find (field => field.key === key)!.labelValues)}
|
||||
handleName={key => message ('reorder', {
|
||||
column: message (fields.find (field => field.key === key)!.label,
|
||||
fields.find (field => field.key === key)!.labelValues).text })}
|
||||
renderGroup={(_, content) => content}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { countsFieldLabel } from '@/lib/countsDefinitions'
|
||||
import { apiDownload, apiGet } from '@/lib/api'
|
||||
import { currentViewingLocale } from '@/lib/locales'
|
||||
import { currentViewingLocale, localeLanguage } from '@/lib/locales'
|
||||
|
||||
import { readCountState, encodeCountState } from '@/lib/queryState'
|
||||
|
||||
@@ -99,7 +99,7 @@ export type CountSort = { key: string; direction: 'asc' | 'desc' }
|
||||
export type CountField = { key: string; label: string; children?: CountField[]
|
||||
labelValues?: Record<string, string>
|
||||
multipleValues?: boolean
|
||||
nameLocale?: { code: string; languageCode: string; scriptCode: string } }
|
||||
nameLocale?: { code: string; languageCode: string; scriptCode?: string } }
|
||||
export type CountRecord = Record<string, unknown>
|
||||
|
||||
export const countColumns: Record<string, string> = {
|
||||
@@ -233,7 +233,8 @@ export const selectedCountColumns = (
|
||||
const defaults: Record<CountsResource, string[]> = {
|
||||
posts: ['title', 'url', 'tags', 'parents', 'video_ms'],
|
||||
tags: [...(definition?.nameColumns ?? []).filter (field =>
|
||||
field.nameLocale?.code === currentViewingLocale ()).map (field => field.key),
|
||||
field.nameLocale?.languageCode === localeLanguage (currentViewingLocale ()))
|
||||
.map (field => field.key),
|
||||
'category', 'parents'],
|
||||
nico_tags: ['name', 'linked_tags'],
|
||||
tag_descriptions: ['language_code', 'body', 'source_language_code'],
|
||||
@@ -356,14 +357,31 @@ export const normaliseCountsParams = (
|
||||
definition?: CountsDefinition,
|
||||
): URLSearchParams => {
|
||||
const params = new URLSearchParams (source)
|
||||
const nameScope = params.get ('name_scope')
|
||||
if (definition?.conditionAliases != null && (nameScope == null || nameScope === 'locale'))
|
||||
{
|
||||
const predicates = new Map ((definition.conditionAliases ?? [])
|
||||
.map (entry => [entry.key, entry.value]))
|
||||
for (const [key, value] of params)
|
||||
if (/^conditions\.\d+\.field$/.test (key)) params.set (key, value.replace (
|
||||
/(?:name|aliases)_[a-z0-9_]+/g, field => predicates.get (field) ?? field))
|
||||
params.set ('name_scope', 'language')
|
||||
}
|
||||
const aliasesByKey = new Map ((definition?.columnAliases ?? [])
|
||||
.map (entry => [entry.key, entry.value]))
|
||||
for (const key of ['columns', 'children', 'order', 'sort', 'child_sort'])
|
||||
if (params.has (key)) params.set (key, params.get (key)!.replace (
|
||||
/(?:name|aliases)_[a-z0-9_]+/g, field => aliasesByKey.get (field) ?? field))
|
||||
if (definition?.nameColumns != null)
|
||||
{
|
||||
const names = definition.nameColumns.filter (field => !field.multipleValues)
|
||||
.map (field => field.key)
|
||||
const current = definition.nameColumns.find (field =>
|
||||
!field.multipleValues && field.nameLocale?.code === currentViewingLocale ())?.key
|
||||
!field.multipleValues
|
||||
&& field.nameLocale?.languageCode === localeLanguage (currentViewingLocale ()))?.key
|
||||
const aliases = definition.nameColumns.find (field =>
|
||||
field.multipleValues && field.nameLocale?.code === currentViewingLocale ())?.key
|
||||
field.multipleValues
|
||||
&& field.nameLocale?.languageCode === localeLanguage (currentViewingLocale ()))?.key
|
||||
const rootKey = (key: string) => (key === 'name' && current != null) ? current
|
||||
: ((key === 'aliases' && aliases != null) ? aliases : key)
|
||||
const childKeys = (key: string): string[] => {
|
||||
|
||||
@@ -27,11 +27,14 @@ export type CountsFieldMetadata = {
|
||||
setType?: 'tag_reference'; inputTypes?: string[]
|
||||
labelValues?: Record<string, string>
|
||||
nameLocale?: CountField['nameLocale']
|
||||
legacy?: boolean
|
||||
choices?: { value: string; label: string }[] }
|
||||
export type CountsDefinition = {
|
||||
fields: CountsFieldMetadata[]
|
||||
outputFields?: CountField[]
|
||||
nameColumns?: CountField[]
|
||||
columnAliases?: { key: string; value: string }[]
|
||||
conditionAliases?: { key: string; value: string }[]
|
||||
filterFields?: CountsFieldMetadata[]
|
||||
defaults?: { definition: string; mode: string; params: { key: string; value: string }[] }[]
|
||||
axes: { latest: string[]; history: string[] }
|
||||
@@ -53,12 +56,9 @@ const localiseCountsDefinition = (
|
||||
const heading = (name: CountField['nameLocale'], aliasesFlg: boolean) => {
|
||||
if (name == null) return null
|
||||
const language = languageUIMessage (name.languageCode, locale).text
|
||||
const scripts = new Set ((definition.nameColumns ?? [])
|
||||
.filter (field => field.nameLocale?.languageCode === name.languageCode)
|
||||
.map (field => field.nameLocale!.scriptCode.toLowerCase ()))
|
||||
const label = aliasesFlg ? 'counts:localeAliases' : 'counts:localeName'
|
||||
return { label: scripts.size > 1 ? `${label}WithScript` : label,
|
||||
labelValues: { language, script: name.scriptCode } }
|
||||
return { label: name.scriptCode == null ? label : `${label}WithScript`,
|
||||
labelValues: { language, script: name.scriptCode ?? '' } }
|
||||
}
|
||||
const columns = (fields: CountField[]): CountField[] => fields.map (field => ({
|
||||
...field, ...heading (field.nameLocale, field.multipleValues === true),
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする