177 lines
4.6 KiB
TypeScript
177 lines
4.6 KiB
TypeScript
import { formatNumber, t } from '@/i18n'
|
||
import type { BuiltinField } from '@/lib/contentTypes'
|
||
import { fieldTypeSpec, type FieldCellKind } from '@/lib/fieldTypes'
|
||
import { formatDayValue } from '@/lib/entryValue'
|
||
import { isMissingImage, type MediaSummary } from '@/lib/mediaField'
|
||
import { isMissingRelation, relationIdsOf, type RelationSummary } from '@/lib/relationField'
|
||
import { formatAbsoluteDateFromIso } from '@/lib/relativeDate'
|
||
|
||
export { formatDayValue }
|
||
|
||
export type CellKind = 'empty' | FieldCellKind
|
||
|
||
export interface RelationChip {
|
||
id: number
|
||
label: string
|
||
missing: boolean
|
||
}
|
||
|
||
export interface CellView {
|
||
kind: CellKind
|
||
text: string
|
||
yes?: boolean
|
||
image?: MediaSummary
|
||
chips?: RelationChip[]
|
||
more?: number
|
||
}
|
||
|
||
export const RELATION_CHIPS_SHOWN = 2
|
||
|
||
export const EMPTY_TEXT = '–'
|
||
|
||
const EMPTY: CellView = { kind: 'empty', text: '' }
|
||
const LINE_PREFIX = /^\s{0,3}(?:#{1,6}(?:\s+|$)|>\s?|[-*+]\s+|\d+[.)]\s+)+/
|
||
const IMAGE_OR_LINK = /!?\[([^\]]*)\]\([^)]*\)/g
|
||
const EMPHASIS = /(\*\*|__|\*|`|~~)/g
|
||
const UNDERSCORE_EMPHASIS = /(^|[\s(])_(?=\S)([^_]*?\S)_(?=$|[\s).,;:!?])/g
|
||
|
||
export function richTextFirstLine(markdown: string) {
|
||
for (const line of markdown.split(/\r?\n/)) {
|
||
const plain = line
|
||
.replace(LINE_PREFIX, '')
|
||
.replace(IMAGE_OR_LINK, '$1')
|
||
.replace(EMPHASIS, '')
|
||
.replace(UNDERSCORE_EMPHASIS, '$1$2')
|
||
.trim()
|
||
if (plain) {
|
||
return plain
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function thumbnailCell(value: unknown, image: MediaSummary | undefined): CellView {
|
||
const shown = image ?? { id: Number(value), missing: true as const }
|
||
return {
|
||
kind: 'thumbnail',
|
||
text: isMissingImage(shown) ? '' : shown.filename,
|
||
image: shown,
|
||
}
|
||
}
|
||
|
||
function summaryField(first: { key: string; type: string }): BuiltinField {
|
||
return {
|
||
key: first.key,
|
||
label: '',
|
||
type: first.type,
|
||
required: false,
|
||
unique: false,
|
||
default: null,
|
||
min: null,
|
||
max: null,
|
||
options: null,
|
||
}
|
||
}
|
||
|
||
export function relationLabel(summary: RelationSummary) {
|
||
if (isMissingRelation(summary)) {
|
||
return t('Entry no longer exists')
|
||
}
|
||
const first = summary.first_field
|
||
const cell = describeCell(summaryField(first), first.value)
|
||
return cell.kind === 'empty' || cell.kind === 'thumbnail' || cell.kind === 'relation'
|
||
? t('Entry {0}', [summary.id])
|
||
: plainCellText(cell)
|
||
}
|
||
|
||
function relationCell(value: unknown, summaries: RelationSummary[]): CellView {
|
||
const ids = relationIdsOf(value)
|
||
if (ids.length === 0) {
|
||
return EMPTY
|
||
}
|
||
const chips = ids.map((id): RelationChip => {
|
||
const summary = summaries.find((candidate) => candidate.id === id) ?? {
|
||
id,
|
||
missing: true as const,
|
||
}
|
||
return { id, label: relationLabel(summary), missing: isMissingRelation(summary) }
|
||
})
|
||
return {
|
||
kind: 'relation',
|
||
text: chips.map((chip) => chip.label).join(', '),
|
||
chips: chips.slice(0, RELATION_CHIPS_SHOWN),
|
||
more: Math.max(0, chips.length - RELATION_CHIPS_SHOWN),
|
||
}
|
||
}
|
||
|
||
export function describeCell(
|
||
field: BuiltinField,
|
||
value: unknown,
|
||
image?: MediaSummary,
|
||
relations: RelationSummary[] = [],
|
||
): CellView {
|
||
if (value === undefined || value === null || value === '') {
|
||
return EMPTY
|
||
}
|
||
const kind = fieldTypeSpec(field.type).cell
|
||
switch (kind) {
|
||
case 'thumbnail':
|
||
return thumbnailCell(value, image)
|
||
case 'relation':
|
||
return relationCell(value, relations)
|
||
case 'boolean':
|
||
return { kind, text: '', yes: value === true }
|
||
case 'number':
|
||
return { kind, text: typeof value === 'number' ? formatNumber(value) : String(value) }
|
||
case 'date':
|
||
return { kind, text: formatDayValue(String(value)) }
|
||
case 'choice':
|
||
return { kind, text: String(value) }
|
||
case 'richtext': {
|
||
const line = richTextFirstLine(String(value))
|
||
return line ? { kind, text: line } : EMPTY
|
||
}
|
||
default:
|
||
return { kind, text: String(value) }
|
||
}
|
||
}
|
||
|
||
export function formatEntryDate(iso: string) {
|
||
return formatAbsoluteDateFromIso(iso)
|
||
}
|
||
|
||
export function isEmptyCell(cell: CellView) {
|
||
return cell.kind === 'empty'
|
||
}
|
||
|
||
export function plainCellText(cell: CellView) {
|
||
if (cell.kind === 'empty') {
|
||
return EMPTY_TEXT
|
||
}
|
||
if (cell.kind === 'boolean') {
|
||
return cell.yes ? t('Yes') : t('No')
|
||
}
|
||
return cell.text || EMPTY_TEXT
|
||
}
|
||
|
||
export interface CardFields {
|
||
first?: BuiltinField
|
||
second?: BuiltinField
|
||
choice?: BuiltinField
|
||
}
|
||
|
||
function isThumbnail(field: BuiltinField) {
|
||
return fieldTypeSpec(field.type).cell === 'thumbnail'
|
||
}
|
||
|
||
export function thumbnailColumn(columns: BuiltinField[]) {
|
||
return columns.find(isThumbnail)
|
||
}
|
||
|
||
export function cardFields(fields: BuiltinField[]): CardFields {
|
||
const [first, ...rest] = fields
|
||
const second = rest.find((field) => !isThumbnail(field))
|
||
const choice = fields.find((field) => field.type === 'select')
|
||
return { first, second, choice: choice === first ? undefined : choice }
|
||
}
|