このコミットが含まれているのは:
@@ -199,3 +199,28 @@ export const apiDelete = async <T = void> (
|
||||
|
||||
export const isApiError = <T = unknown> (err: unknown): err is AxiosError<T> =>
|
||||
axios.isAxiosError (err)
|
||||
|
||||
|
||||
// Native downloads let the browser stream to disk without buffering a Blob in app memory.
|
||||
export const apiDownload = async (
|
||||
path: string,
|
||||
body: Record<string, string>,
|
||||
locale: LocaleCode,
|
||||
): Promise<void> => {
|
||||
const token = await csrfToken ()
|
||||
const form = document.createElement ('form')
|
||||
form.method = 'post'
|
||||
form.action = `${API_BASE_URL}${path}?${new URLSearchParams ({ locale })}`
|
||||
form.hidden = true
|
||||
for (const [name, value] of Object.entries ({ ...body, authenticity_token: token }))
|
||||
{
|
||||
const input = document.createElement ('input')
|
||||
input.type = 'hidden'
|
||||
input.name = name
|
||||
input.value = value
|
||||
form.append (input)
|
||||
}
|
||||
document.body.append (form)
|
||||
form.submit ()
|
||||
form.remove ()
|
||||
}
|
||||
|
||||
+61
-34
@@ -1,9 +1,11 @@
|
||||
import { apiGet, apiPost } from '@/lib/api'
|
||||
import { countsAttributes, countsFieldLabel } from '@/lib/countsDefinitions'
|
||||
import { apiDownload, apiGet } from '@/lib/api'
|
||||
|
||||
import type { LocaleCode } from '@/lib/locales'
|
||||
import type { TagNameSnapshot } from '@/types'
|
||||
|
||||
export type CountsResource = 'posts' | 'tags'
|
||||
export type CountsResource = 'posts' | 'tags' | 'nico_tags'
|
||||
| 'tag_descriptions' | 'tag_groups' | 'materials'
|
||||
export type CountsName = TagNameSnapshot & {
|
||||
languageCode: string; languageName?: string | null; scriptName?: string | null }
|
||||
export type CountsTag = {
|
||||
@@ -17,6 +19,8 @@ export type CountsTag = {
|
||||
sections: { beginMs: number; endMs: number | null }[]
|
||||
names: CountsName[] }
|
||||
export type CountsVersion = {
|
||||
[key: string]: unknown
|
||||
eventType?: string
|
||||
id: number
|
||||
resourceId: number
|
||||
versionNo: number
|
||||
@@ -51,6 +55,12 @@ export type CountsStateTotals = {
|
||||
deprecatedTags: number | null
|
||||
externalAssignments: number | null }
|
||||
export type CountsResult = {
|
||||
executionAt?: string
|
||||
columnValues?: (string | number | null)[][]
|
||||
cells?: { values: (string | number | null)[]; count: number }[]
|
||||
axisLabels?: Record<string, Record<string, string | null>>
|
||||
rows?: { field: string; grain?: string }[]
|
||||
cols?: { field: string; grain?: string }[]
|
||||
count: number
|
||||
page: number
|
||||
limit: number
|
||||
@@ -69,7 +79,8 @@ export type CountSort = { key: string; direction: 'asc' | 'desc' }
|
||||
export type CountField = { key: string; label: string; children?: CountField[] }
|
||||
export type CountRecord = Record<string, unknown>
|
||||
|
||||
export const countColumns = {
|
||||
export const countColumns: Record<string, string> = {
|
||||
event_type: 'counts:eventType',
|
||||
thumbnail: 'counts:columns.thumbnailDisplay', resource_id: 'counts:columns.resourceId',
|
||||
version_no: 'history:version', is_latest: 'counts:columns.latest',
|
||||
title: 'counts:columns.postTitle', url: 'counts:columns.postURL',
|
||||
@@ -82,7 +93,7 @@ export const countColumns = {
|
||||
category: 'common:categoryLabel', aliases: 'tags:aliases',
|
||||
deprecated: 'counts:columns.deprecated', user: 'history:actor',
|
||||
created_at: 'counts:columns.changedAt' }
|
||||
export type CountColumn = keyof typeof countColumns
|
||||
export type CountColumn = string
|
||||
|
||||
const aliasFields: CountField[] = [
|
||||
{ key: 'name', label: 'history:name' },
|
||||
@@ -93,7 +104,24 @@ const nameFields: CountField[] = [
|
||||
{ key: 'name', label: 'history:name' },
|
||||
{ key: 'aliases', label: 'tags:aliases', children: aliasFields },
|
||||
{ key: 'autoGenerated', label: 'history:nameChanges.automatic' }]
|
||||
const placementFields = (depth: number): CountField[] => [
|
||||
{ key: 'sectionName', label: 'counts:attributes.section_name' },
|
||||
{ key: 'description', label: 'counts:attributes.description' },
|
||||
{ key: 'tagId', label: 'counts:attributes.tag_id' },
|
||||
{ key: 'tagGroupId', label: 'counts:resources.tag_groups' },
|
||||
...(depth > 0 ? [{ key: 'children', label: 'counts:attributes.children',
|
||||
children: placementFields (depth - 1) }] : [])]
|
||||
|
||||
|
||||
const nested: Partial<Record<CountColumn, CountField[]>> = {
|
||||
placements: placementFields (4),
|
||||
export_paths_json: [
|
||||
{ key: 'profile', label: 'counts:attributes.profile' },
|
||||
{ key: 'path', label: 'counts:attributes.path' }],
|
||||
members: [
|
||||
{ key: 'tagId', label: 'counts:columns.resourceId' },
|
||||
{ key: 'tagGroupId', label: 'counts:resources.tag_groups' },
|
||||
{ key: 'versionNo', label: 'history:version' }],
|
||||
tags: [
|
||||
{ key: 'id', label: 'counts:columns.resourceId' },
|
||||
{ key: 'versionNo', label: 'history:version' },
|
||||
@@ -108,19 +136,22 @@ const nested: Partial<Record<CountColumn, CountField[]>> = {
|
||||
|
||||
|
||||
export const columnsForCounts = (resource: CountsResource): CountColumn[] => [
|
||||
...(resource === 'posts' ? ['thumbnail'] as const : []),
|
||||
...(resource === 'posts' ? ['thumbnail'] : []),
|
||||
'resource_id', 'version_no', 'is_latest',
|
||||
...(resource === 'posts'
|
||||
? ['title', 'url', 'thumbnail_base', 'tags', 'parents', 'video_ms',
|
||||
'original_created_at', 'original_created_from', 'original_created_before'] as const
|
||||
: ['name', 'names', 'category', 'aliases', 'parents', 'deprecated'] as const),
|
||||
'user', 'created_at']
|
||||
? ['title', 'url', 'thumbnail_base', 'tags', 'parents', 'video_ms',
|
||||
'original_created_at', 'original_created_from', 'original_created_before']
|
||||
: (resource === 'tags'
|
||||
? ['name', 'names', 'category', 'aliases', 'parents', 'deprecated']
|
||||
: Object.keys (countsAttributes[resource]))),
|
||||
'event_type', 'user', 'created_at']
|
||||
|
||||
|
||||
export const fieldsForCounts = (resource: CountsResource): CountField[] =>
|
||||
columnsForCounts (resource).map (key => ({
|
||||
key,
|
||||
label: (key === 'parents' && resource === 'tags') ? 'tags:parents' : countColumns[key],
|
||||
label: (key === 'parents' && resource === 'tags') ? 'tags:parents'
|
||||
: (countColumns[key] ?? countsFieldLabel (key)),
|
||||
children: nested[key] }))
|
||||
|
||||
|
||||
@@ -185,7 +216,7 @@ 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)))
|
||||
countChildPaths (fields ?? [], key)))
|
||||
|
||||
|
||||
export const readCountSort = (value: string | null): CountSort[] =>
|
||||
@@ -234,24 +265,22 @@ export const normaliseCountsParams = (
|
||||
source: URLSearchParams,
|
||||
): URLSearchParams => {
|
||||
const params = new URLSearchParams (source)
|
||||
params.delete ('event_type')
|
||||
if (!params.has ('definition'))
|
||||
params.set ('definition', params.get ('basis') === 'history' ? 'history' : 'latest')
|
||||
const legacy = params.get ('mode')
|
||||
if (['state', 'trend', 'snapshot'].includes (legacy ?? ''))
|
||||
{
|
||||
params.set ('mode', legacy === 'snapshot' ? 'details' : 'summary')
|
||||
if (legacy !== 'snapshot') params.set ('axis', resource === 'posts' ? 'tags' : 'month')
|
||||
if (legacy === 'trend')
|
||||
{
|
||||
if (params.get ('before')) params.set ('at', params.get ('before')!)
|
||||
params.delete ('from')
|
||||
params.delete ('before')
|
||||
}
|
||||
}
|
||||
if (!params.has ('mode')) params.set ('mode', 'summary')
|
||||
const axes = (resource === 'posts' && params.get ('basis') !== 'history')
|
||||
? ['tags', 'month', 'user'] : ['month', 'user']
|
||||
if (!axes.includes (params.get ('axis') ?? '')) params.set ('axis', axes[0])
|
||||
params.delete ('grain')
|
||||
if (!params.has ('rows') && params.has ('axis'))
|
||||
{
|
||||
const field = params.get ('axis') === 'month' ? 'created_at' : params.get ('axis')!
|
||||
params.set ('rows', JSON.stringify ([{
|
||||
field, ...(field === 'created_at' ? { grain: 'month' } : { }) }]))
|
||||
}
|
||||
if (params.has ('before') && !params.has ('through'))
|
||||
params.set ('through', params.get ('before')!)
|
||||
for (const key of ['basis', 'axis', 'latest', 'grain', 'before', 'summary_columns'])
|
||||
params.delete (key)
|
||||
if (params.has ('columns'))
|
||||
{
|
||||
const selected = new Set (params.get ('columns')!.split (','))
|
||||
@@ -263,14 +292,13 @@ export const normaliseCountsParams = (
|
||||
|
||||
|
||||
export const isCountsState = (params: URLSearchParams): boolean =>
|
||||
params.get ('basis') !== 'history'
|
||||
params.get ('definition') !== 'history'
|
||||
|
||||
|
||||
export const countsRequestParams = (params: URLSearchParams): URLSearchParams => {
|
||||
const result = new URLSearchParams (params)
|
||||
const hidden = isCountsState (params) ? ['latest'] : ['kind']
|
||||
for (const key of ['columns', 'summary_columns', 'children', 'child_sort',
|
||||
'nested_json', 'order', 'basis', 'grain', ...hidden])
|
||||
'nested_json', 'order', 'basis', 'axis', 'latest', 'grain'])
|
||||
result.delete (key)
|
||||
return result
|
||||
}
|
||||
@@ -280,10 +308,9 @@ export const fetchCounts = (
|
||||
resource: CountsResource,
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): Promise<CountsResult> => apiGet (
|
||||
isCountsState (params) ? '/counts/state' : `/counts/${resource}`, {
|
||||
locale, params: { ...Object.fromEntries (countsRequestParams (params)),
|
||||
...(isCountsState (params) ? { resource } : { }) } })
|
||||
): Promise<CountsResult> => apiGet ('/counts/query', {
|
||||
locale, params: { ...Object.fromEntries (countsRequestParams (params)), resource } })
|
||||
|
||||
|
||||
|
||||
export const exportCounts = (
|
||||
@@ -294,10 +321,10 @@ export const exportCounts = (
|
||||
columns: string[],
|
||||
headers: string[],
|
||||
labels: Record<string, string>,
|
||||
): Promise<Blob> => apiPost ('/counts/export', {
|
||||
): Promise<void> => apiDownload ('/counts/export', {
|
||||
...Object.fromEntries (params), resource, format,
|
||||
columns: columns.join (','), headers: JSON.stringify (headers),
|
||||
labels: JSON.stringify (labels) }, { locale, responseType: 'blob' })
|
||||
labels: JSON.stringify (labels) }, locale)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { CountField, CountsResource } from '@/lib/counts'
|
||||
|
||||
export type CountsAxis = { field: string; grain?: string }
|
||||
export type CountsCondition = {
|
||||
field: string; operator: string; value?: string; end?: string; blank?: boolean }
|
||||
export type CountsFieldType = 'string' | 'integer' | 'datetime' | 'enum' | 'json'
|
||||
|
||||
export const countsResources: CountsResource[] = [
|
||||
'posts', 'tags', 'nico_tags', 'tag_descriptions', 'tag_groups', 'materials']
|
||||
export const countsGrains = ['year', 'quarter', 'month', 'week', 'day', 'hour']
|
||||
export const countsOperators: Record<CountsFieldType, string[]> = {
|
||||
string: ['eq', 'ne', 'like'], enum: ['eq', 'ne'], json: [],
|
||||
integer: ['eq', 'ne', 'gt', 'lt', 'ge', 'le', 'range'],
|
||||
datetime: ['eq', 'ne', 'gt', 'lt', 'ge', 'le', 'range'] }
|
||||
|
||||
export const countsAttributes: Record<CountsResource, Record<string, CountsFieldType>> = {
|
||||
posts: { title: 'string', url: 'string', thumbnail_base: 'string', tags_json: 'json',
|
||||
parent_post_ids: 'string', video_ms: 'integer', original_created_from: 'datetime',
|
||||
original_created_before: 'datetime' },
|
||||
tags: { name: 'string', names: 'json', category: 'enum', aliases: 'string',
|
||||
parent_tag_ids: 'string', deprecated_at: 'datetime' },
|
||||
nico_tags: { name: 'string', linked_tags: 'string' },
|
||||
tag_descriptions: { language_code: 'enum', body: 'string', source_language_code: 'enum',
|
||||
source_version_no: 'integer' },
|
||||
tag_groups: { name: 'string', description: 'string', members: 'json', placements: 'json' },
|
||||
materials: { url: 'string', parent_id: 'integer', tag_id: 'integer', tag_name: 'string',
|
||||
tag_category: 'enum', discarded_at: 'datetime', updated_at: 'datetime',
|
||||
updated_by_user_id: 'integer', export_paths_json: 'json', file_blob_id: 'integer',
|
||||
file_filename: 'string', file_content_type: 'string', file_byte_size: 'integer',
|
||||
file_checksum: 'string', file_sha256: 'string', source_kind: 'enum', source_uri: 'string',
|
||||
source_path: 'string', source_file_id: 'string', normalized_source_key: 'string' } }
|
||||
|
||||
|
||||
export const countsFilterFields = (
|
||||
resource: CountsResource,
|
||||
): Record<string, CountsFieldType> => ({
|
||||
resource_id: 'integer' as const, version_no: 'integer' as const,
|
||||
event_type: 'enum' as const, user: 'integer' as const, created_at: 'datetime' as const,
|
||||
...countsAttributes[resource] })
|
||||
|
||||
|
||||
export const countsFieldLabel = (key: string): string => {
|
||||
const labels: Record<string, string> = {
|
||||
resource_id: 'columns.resourceId', version_no: 'history:version',
|
||||
user: 'history:actor', created_at: 'columns.changedAt', name: 'history:name',
|
||||
title: 'columns.postTitle', url: 'columns.postURL', thumbnail_base: 'columns.thumbnailURL',
|
||||
tags: 'columns.tags', tags_json: 'columns.tags', names: 'columns.names',
|
||||
aliases: 'tags:aliases', category: 'common:categoryLabel',
|
||||
language_code: 'columns.language', source_language_code: 'attributes.sourceLanguage',
|
||||
changed_field: 'changedField' }
|
||||
return labels[key] ?? `attributes.${key}`
|
||||
}
|
||||
|
||||
|
||||
export const countsAxisFields = (resource: CountsResource, history: boolean): CountField[] => [
|
||||
...Object.entries (countsFilterFields (resource))
|
||||
.filter (([, type]) => type !== 'json')
|
||||
.map (([key]) => ({ key, label: countsFieldLabel (key) })),
|
||||
...(resource === 'posts' ? [{ key: 'tags', label: 'columns.tags' }] : []),
|
||||
...(history ? [{ key: 'changed_field', label: 'changedField' }] : [])]
|
||||
|
||||
|
||||
export const readCountsJSON = <T,> (params: URLSearchParams, key: string, fallback: T): T => {
|
||||
try
|
||||
{
|
||||
return JSON.parse (params.get (key) ?? JSON.stringify (fallback)) as T
|
||||
}
|
||||
catch
|
||||
{
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,10 @@ export const countsCellText = (
|
||||
row: CountsVersion,
|
||||
column: CountColumn,
|
||||
locale: LocaleCode,
|
||||
params = new URLSearchParams (),
|
||||
): string => {
|
||||
const timezone = params.get ('timezone') ?? 'UTC'
|
||||
const date = (value: string) => localisedDateString (value, locale, null, timezone)
|
||||
switch (column)
|
||||
{
|
||||
case 'resource_id': return String (row.resourceId)
|
||||
@@ -28,6 +31,10 @@ export const countsCellText = (
|
||||
return translateUIMessage (`common:categories.${row.category}`, 'counts', locale).text
|
||||
case 'video_ms': return row.videoMs == null ? '' : msToTime (row.videoMs)
|
||||
case 'original_created_at':
|
||||
if (timezone !== 'Asia/Tokyo')
|
||||
return translateUIMessage ('sections.range', 'counts', locale, {
|
||||
begin: row.originalCreatedFrom == null ? '' : date (row.originalCreatedFrom),
|
||||
end: row.originalCreatedBefore == null ? '' : date (row.originalCreatedBefore) }).text
|
||||
return originalCreatedAtString (
|
||||
row.originalCreatedFrom ?? null, row.originalCreatedBefore ?? null, locale)
|
||||
case 'original_created_from':
|
||||
@@ -35,10 +42,10 @@ export const countsCellText = (
|
||||
{
|
||||
const value = column === 'original_created_from'
|
||||
? row.originalCreatedFrom : row.originalCreatedBefore
|
||||
return value == null ? '' : localisedDateString (value, locale)
|
||||
return value == null ? '' : date (value)
|
||||
}
|
||||
case 'deprecated':
|
||||
return row.deprecated == null ? '' : localisedDateString (row.deprecated, locale)
|
||||
return row.deprecated == null ? '' : date (row.deprecated)
|
||||
case 'user':
|
||||
if (row.createdByUser == null)
|
||||
return translateUIMessage ('history:bot', 'counts', locale).text
|
||||
@@ -46,12 +53,33 @@ export const countsCellText = (
|
||||
? `${row.createdByUser.name} (#${row.createdByUser.id})`
|
||||
: translateUIMessage ('history:anonymous', 'counts', locale, {
|
||||
id: row.createdByUser.id }).text
|
||||
case 'created_at': return localisedDateString (row.createdAt, locale)
|
||||
default: return ''
|
||||
case 'created_at': return date (row.createdAt)
|
||||
default:
|
||||
{
|
||||
const key = column.replace (/_([a-z])/g, (_, letter: string) => letter.toUpperCase ())
|
||||
const value = row[key]
|
||||
if (['discarded_at', 'updated_at'].includes (column) && typeof value === 'string')
|
||||
return date (value)
|
||||
if (column === 'event_type')
|
||||
return translateUIMessage (`events.${value}`, 'counts', locale).text
|
||||
if (column === 'language_code' || column === 'source_language_code')
|
||||
return countsLogicalName ({ languageCode: value }, 'languageCode', locale).text
|
||||
return value == null ? '' : (typeof value === 'object'
|
||||
? JSON.stringify (value, null, 2) : String (value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const countsNestedRows = (row: CountsVersion, column: string): CountRecord[] => {
|
||||
const key = column.replace (/_([a-z])/g, (_, letter: string) => letter.toUpperCase ())
|
||||
const value = row[key]
|
||||
if (column === 'export_paths_json' && value != null && typeof value === 'object')
|
||||
return Object.entries (value).map (([profile, path]) => ({ profile, path }))
|
||||
return Array.isArray (value) ? value as CountRecord[] : []
|
||||
}
|
||||
|
||||
|
||||
export const countsCSVCell = (
|
||||
row: CountsVersion,
|
||||
column: CountColumn,
|
||||
@@ -62,10 +90,10 @@ export const countsCSVCell = (
|
||||
const field = fieldsForCounts (resource).find (field => field.key === column)!
|
||||
if (field.children != null)
|
||||
{
|
||||
const rows = row[column as keyof CountsVersion] as CountRecord[] ?? []
|
||||
const rows = countsNestedRows (row, column)
|
||||
return JSON.stringify (countsNestedExport (rows, field.children, column, params, locale))
|
||||
}
|
||||
return countsCellText (row, column, locale)
|
||||
return countsCellText (row, column, locale, params)
|
||||
}
|
||||
|
||||
|
||||
@@ -189,8 +217,8 @@ export const countsCellCopy = (
|
||||
params: URLSearchParams,
|
||||
locale: LocaleCode,
|
||||
): string => {
|
||||
if (field.children == null) return countsCellText (row, field.key as CountColumn, locale)
|
||||
const rows = row[field.key as keyof CountsVersion] as CountRecord[] ?? []
|
||||
if (field.children == null) return countsCellText (row, field.key as CountColumn, locale, params)
|
||||
const rows = countsNestedRows (row, field.key)
|
||||
return params.get ('nested_json') === '1'
|
||||
? JSON.stringify (
|
||||
countsNestedExport (rows, field.children, field.key, params, locale), null, 2)
|
||||
@@ -240,6 +268,10 @@ export const countsExportLabels = (
|
||||
for (const key of ['ja', 'ain'])
|
||||
labels[`language:${key}`] = translateUIMessage (
|
||||
`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'])
|
||||
labels[`event_type:${event}`] = translateUIMessage (`events.${event}`, 'counts', locale).text
|
||||
labels.latest = translateUIMessage ('latestValue', 'counts', locale).text
|
||||
labels.notRegistered = translateUIMessage ('notRegistered', 'counts', locale).text
|
||||
return labels
|
||||
|
||||
+15
-11
@@ -17,24 +17,26 @@ export const cn = (...inputs: ClassValue[]) => twMerge (clsx (...inputs))
|
||||
|
||||
type DatePrecision = 'month' | 'day' | 'hour' | 'minute' | 'second' | null
|
||||
|
||||
const dateFormatters = new Map<DatePrecision, Intl.DateTimeFormat> ()
|
||||
let ainuDateFormatter: Intl.DateTimeFormat | null = null
|
||||
const dateFormatters = new Map<string, Intl.DateTimeFormat> ()
|
||||
const ainuDateFormatters = new Map<string, Intl.DateTimeFormat> ()
|
||||
|
||||
|
||||
const formatDateString = (
|
||||
date: Date,
|
||||
unknown: DatePrecision,
|
||||
locale: LocaleCode,
|
||||
timeZone = 'Asia/Tokyo',
|
||||
): string => {
|
||||
const key = `${timeZone}/${unknown}`
|
||||
if (Number.isNaN (date.getTime ()))
|
||||
return date.toString ()
|
||||
if (locale === 'ja')
|
||||
{
|
||||
if (!dateFormatters.has (unknown))
|
||||
if (!dateFormatters.has (key))
|
||||
{
|
||||
dateFormatters.set (unknown, new Intl.DateTimeFormat ('ja-JP-u-ca-japanese', {
|
||||
dateFormatters.set (key, new Intl.DateTimeFormat ('ja-JP-u-ca-japanese', {
|
||||
era: 'long', year: 'numeric',
|
||||
timeZone: 'Asia/Tokyo',
|
||||
timeZone,
|
||||
month: unknown === 'month' ? undefined : 'long',
|
||||
day: unknown != null && ['month', 'day'].includes (unknown) ? undefined : 'numeric',
|
||||
weekday: unknown != null && ['month', 'day'].includes (unknown) ? undefined : 'short',
|
||||
@@ -42,14 +44,15 @@ const formatDateString = (
|
||||
minute: unknown == null || unknown === 'second' ? 'numeric' : undefined,
|
||||
second: unknown == null ? 'numeric' : undefined }))
|
||||
}
|
||||
return dateFormatters.get (unknown)!.format (date)
|
||||
return dateFormatters.get (key)!.format (date)
|
||||
}
|
||||
ainuDateFormatter ??= new Intl.DateTimeFormat ('en-CA', {
|
||||
if (!ainuDateFormatters.has (timeZone))
|
||||
ainuDateFormatters.set (timeZone, new Intl.DateTimeFormat ('en-CA', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
timeZone: 'Asia/Tokyo',
|
||||
timeZone,
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hourCycle: 'h23' })
|
||||
const parts = ainuDateFormatter.formatToParts (date)
|
||||
hourCycle: 'h23' }))
|
||||
const parts = ainuDateFormatters.get (timeZone)!.formatToParts (date)
|
||||
const part = (type: string) => parts.find (entry => entry.type === type)?.value ?? ''
|
||||
const omitDay = unknown != null && ['month', 'day'].includes (unknown)
|
||||
const dateParts = [part ('year'),
|
||||
@@ -76,7 +79,8 @@ export const localisedDateString = (
|
||||
value: string | Date,
|
||||
locale: LocaleCode,
|
||||
unknown: DatePrecision = null,
|
||||
): string => dateString (value, unknown, locale)
|
||||
timeZone?: string,
|
||||
): string => formatDateString (toDate (value), unknown, locale, timeZone)
|
||||
|
||||
|
||||
export const originalCreatedAtString = (
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする