このコミットが含まれているのは:
2026-10-26 21:24:32 +09:00
コミット 2563b3fef3
21個のファイルの変更、703行の追加、54行の削除
+9
ファイルの表示
@@ -211,6 +211,15 @@ export const apiDownload = async (
const { token } = await apiPost<{ token: string }> (`${path}/prepare`, body, { locale })
if (typeof token !== 'string' || token.length === 0)
throw new Error ('Missing download token')
startAPIDownload (path, token, onError)
}
export const startAPIDownload = (
path: string,
token: string,
onError?: () => void,
): void => {
const frame = document.createElement ('iframe')
frame.hidden = true
frame.addEventListener ('load', () => {
+33 -11
ファイルの表示
@@ -206,10 +206,16 @@ export const selectedCountColumns = (
params: URLSearchParams,
definition?: CountsDefinition,
): CountColumn[] => {
const selected = params.has ('columns') ? params.get ('columns')!.split (',') :
columnsForCounts (resource, definition).filter (key => ![
'thumbnail', 'original_created_from', 'original_created_before']
.includes (key))
const common = ['resource_id', 'version_no', 'event_type', 'user', 'created_at']
const defaults: Record<CountsResource, string[]> = {
posts: ['title', 'url', 'tags', 'parents', 'video_ms'],
tags: ['name', 'category', 'aliases', 'parents'],
nico_tags: ['name', 'linked_tags'],
tag_descriptions: ['language_code', 'body', 'source_language_code'],
tag_groups: ['name', 'description', 'members'],
materials: ['url', 'source_kind', 'tag_id'] }
const selected = params.has ('columns') ? params.get ('columns')!.split (',')
: [...common, ...defaults[resource]]
return orderedCountFields (fieldsForCounts (resource, definition), params)
.map (field => field.key as CountColumn).filter (key => selected.includes (key))
}
@@ -223,9 +229,8 @@ export const countChildPaths = (fields: CountField[], prefix = ''): string[] =>
export const selectedCountChildren = (params: URLSearchParams): Set<string> =>
new Set (params.has ('children') ? params.get ('children')!.split (',') :
Object.entries (nested).flatMap (([key, fields]) =>
countChildPaths (fields ?? [], key)))
new Set (params.has ('children') ? params.get ('children')!.split (',')
: ['tags.name', 'parents.id', 'aliases.name', 'members.tagId'])
export const readCountSort = (value: string | null): CountSort[] =>
@@ -342,15 +347,32 @@ export const exportCounts = (
headers: string[],
labels: Record<string, string>,
onError?: () => void,
): Promise<void> => {
): Promise<void> => apiDownload ('/counts/export', countsExportPayload (
resource, params, format, columns, headers, labels), locale, onError)
export const countsExportPayload = (
resource: CountsResource,
params: URLSearchParams,
format: 'json' | 'csv',
columns: string[],
headers: string[],
labels: Record<string, string>,
definition?: CountsDefinition,
): Record<string, string> => {
const payload = new URLSearchParams (params)
if (!payload.has ('children'))
{
const selected = selectedCountChildren (params)
payload.set ('children', countChildPaths (fieldsForCounts (resource, definition))
.filter (path => selected.has (path)).join (','))
}
payload.delete ('current')
payload.delete ('row_numbers')
if (isCountsState (params) && countsUseLatest (params)) payload.delete ('at')
return apiDownload ('/counts/export', {
...Object.fromEntries (payload), resource, format,
return { ...Object.fromEntries (payload), resource, format,
columns: columns.join (','), headers: JSON.stringify (headers),
labels: JSON.stringify (labels) }, locale, onError)
labels: JSON.stringify (labels) }
}
+53 -7
ファイルの表示
@@ -194,6 +194,38 @@ export const countsNestedExport = (
}
// A chain with one selected scalar leaf is displayed and copied as one cell.
export const countsNestedIsScalar = (
fields: CountField[],
path: string,
params: URLSearchParams,
): boolean => {
const selected = selectedCountChildren (params)
const columns = fields.filter (field => selected.has (`${path}.${field.key}`))
if (columns.length !== 1) return false
const field = columns[0]
return field.children == null
|| countsNestedIsScalar (field.children, `${path}.${field.key}`, params)
}
export const countsNestedScalarText = (
rows: CountRecord[],
fields: CountField[],
path: string,
params: URLSearchParams,
locale: LocaleCode,
): string => {
const selected = selectedCountChildren (params)
const field = fields.find (field => selected.has (`${path}.${field.key}`))
if (field == null) return ''
return countsNestedOrder (rows, path, params, locale).map (row => field.children == null
? countsLogicalName (row, field.key, locale).text
: countsNestedScalarText (row[field.key] as CountRecord[] ?? [], field.children,
`${path}.${field.key}`, params, locale)).join (' ')
}
export const countsNestedText = (
rows: CountRecord[],
fields: CountField[],
@@ -201,6 +233,8 @@ export const countsNestedText = (
params: URLSearchParams,
locale: LocaleCode,
): string => {
if (countsNestedIsScalar (fields, path, params))
return countsNestedScalarText (rows, fields, path, params, locale)
const selected = selectedCountChildren (params)
const columns = orderedCountFields (fields, params, path)
.filter (field => selected.has (`${path}.${field.key}`))
@@ -224,9 +258,11 @@ export const countsCellCopy = (
): string => {
if (field.children == null) return countsCellText (row, field.key as CountColumn, locale, params)
const rows = countsNestedRows (row, field.key)
if (countsNestedIsScalar (field.children, field.key, params))
return countsNestedScalarText (rows, field.children, field.key, params, locale)
return params.get ('nested_json') === '1'
? JSON.stringify (
countsNestedExport (rows, field.children, field.key, params, locale), null, 2)
countsNestedExport (rows, field.children, field.key, params, locale))
: countsNestedText (rows, field.children, field.key, params, locale)
}
@@ -297,7 +333,9 @@ const clipboardColumns = (
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 (
children: (field.children == null
|| countsNestedIsScalar (field.children, `${path}.${field.key}`, params))
? undefined : clipboardColumns (
field.children, `${path}.${field.key}`, params, locale) }))
}
@@ -316,8 +354,11 @@ export const countsNestedClipboardTable = (
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))) }
: (countsNestedIsScalar (field.children, `${path}.${field.key}`, params)
? countsNestedScalarText (row[field.key] as CountRecord[] ?? [],
field.children, `${path}.${field.key}`, params, locale)
: countsNestedClipboardTable (row[field.key] as CountRecord[] ?? [],
field.children, `${path}.${field.key}`, params, locale)))) }
}
@@ -330,7 +371,9 @@ export const countsDetailClipboardTable = (
): CountsClipboardTable => {
const columns = fields.map (field => ({
label: translateUIMessage (field.label, 'counts', locale).text,
children: field.children == null ? undefined : clipboardColumns (
children: (field.children == null
|| countsNestedIsScalar (field.children, field.key, params))
? undefined : clipboardColumns (
field.children, field.key, params, locale) }))
if (rowStart != null)
columns.unshift ({ label: translateUIMessage ('rowNumbers', 'counts', locale).text,
@@ -338,8 +381,11 @@ export const countsDetailClipboardTable = (
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))
: (countsNestedIsScalar (field.children, field.key, params)
? countsNestedScalarText (countsNestedRows (row, field.key), field.children,
field.key, params, locale)
: countsNestedClipboardTable (countsNestedRows (row, field.key), field.children,
field.key, params, locale)))
return rowStart == null ? cells : [String (rowStart + index), ...cells]
}) }
}
+1
ファイルの表示
@@ -41,6 +41,7 @@ type DialogueFormOptions = { title: ReactNode
description?: ReactNode
body: (controls: DialogueFormControls) => ReactNode
cancelText?: ReactNode
onCancel?: () => Promise<boolean | void> | boolean | void
footerLayout?: 'inline'
size?: 'default' | 'large' | 'viewport' }