Skip to content

Commit

Permalink
Details display
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin committed Nov 28, 2023
1 parent 05fc47c commit dd5ddaa
Show file tree
Hide file tree
Showing 18 changed files with 349 additions and 222 deletions.
16 changes: 15 additions & 1 deletion packages/fastui-bootstrap/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,21 @@ export const classNameGenerator: ClassNameGenerator = ({ props, fullPath, subEle
case 'Button':
return 'btn btn-primary'
case 'Table':
return 'table table-striped table-bordered'
switch (subElement) {
case 'no-data-message':
return 'text-center mt-2'
default:
return 'table table-striped table-bordered'
}
case 'Details':
switch (subElement) {
case 'dt':
return 'col-sm-3 col-md-2 text-sm-end'
case 'dd':
return 'col-sm-9 col-md-10'
default:
return 'row'
}
case 'Form':
case 'ModelForm':
return formClassName(subElement)
Expand Down
35 changes: 35 additions & 0 deletions packages/fastui/src/components/details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { FC } from 'react'

import { asTitle } from '../tools'
import { ClassName, useClassName } from '../hooks/className'

import { DisplayComp, DisplayLookupProps, ModelData, renderEvent } from './display'

export interface DetailsProps {
type: 'Details'
data: ModelData
fields: DisplayLookupProps[]
className?: ClassName
}

export const DetailsComp: FC<DetailsProps> = (props) => (
<dl className={useClassName(props)}>
{props.fields.map((field, id) => (
<FieldDetail key={id} props={props} fieldDisplay={field} />
))}
</dl>
)

const FieldDetail: FC<{ props: DetailsProps; fieldDisplay: DisplayLookupProps }> = ({ props, fieldDisplay }) => {
const { field, title, onClick, ...rest } = fieldDisplay
const value = props.data[field]
const renderedOnClick = renderEvent(onClick, props.data)
return (
<>
<dt className={useClassName(props, { el: 'dt' })}>{title ?? asTitle(field)}</dt>
<dd className={useClassName(props, { el: 'dd' })}>
<DisplayComp type="Display" onClick={renderedOnClick} value={value} {...rest} />
</dd>
</>
)
}
127 changes: 99 additions & 28 deletions packages/fastui/src/components/display.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
import { FC } from 'react'

import { useCustomRender } from '../hooks/config'
import { DisplayChoices, asTitle } from '../display'
import { unreachable } from '../tools'
import { unreachable, asTitle } from '../tools'
import { AnyEvent } from '../events'

import { JsonComp, JsonData } from './Json'
import { LinkRender } from './link'

interface DisplayProps {
export enum DisplayMode {
auto = 'auto',
plain = 'plain',
datetime = 'datetime',
date = 'date',
duration = 'duration',
as_title = 'as_title',
markdown = 'markdown',
json = 'json',
inline_code = 'inline_code',
}

export interface DisplayProps {
type: 'Display'
display?: DisplayChoices
value?: JsonData
mode?: DisplayMode
title?: string
onClick?: AnyEvent
}

export const DisplayComp: FC<DisplayProps> = (props) => {
Expand All @@ -18,22 +33,35 @@ export const DisplayComp: FC<DisplayProps> = (props) => {
return <CustomRenderComp />
}

const display = props.display ?? DisplayChoices.auto
const { onClick } = props
if (onClick) {
return (
<LinkRender onClick={onClick}>
<DisplayRender {...props} />
</LinkRender>
)
} else {
return <DisplayRender {...props} />
}
}

const DisplayRender: FC<DisplayProps> = (props) => {
const mode = props.mode ?? DisplayMode.auto
const value = props.value ?? null
if (display === DisplayChoices.json) {
if (mode === DisplayMode.json) {
return <JsonComp type="JSON" value={value} />
} else if (Array.isArray(value)) {
return <DisplayArray type="DisplayArray" display={display} value={value} />
return <DisplayArray type="DisplayArray" mode={mode} value={value} />
} else if (typeof value === 'object' && value !== null) {
return <DisplayObject type="DisplayObject" display={display} value={value} />
return <DisplayObject type="DisplayObject" mode={mode} value={value} />
} else {
return <DisplayPrimitive type="DisplayPrimitive" display={display} value={value} />
return <DisplayPrimitive type="DisplayPrimitive" mode={mode} value={value} />
}
}

interface DisplayArrayProps {
value: JsonData[]
display?: DisplayChoices
mode?: DisplayMode
type: 'DisplayArray'
}

Expand All @@ -42,13 +70,13 @@ export const DisplayArray: FC<DisplayArrayProps> = (props) => {
if (CustomRenderComp) {
return <CustomRenderComp />
}
const { display, value } = props
const { mode, value } = props

return (
<>
{value.map((v, i) => (
<span key={i}>
<DisplayComp type="Display" display={display} value={v} />,{' '}
<DisplayComp type="Display" mode={mode} value={v} />,{' '}
</span>
))}
</>
Expand All @@ -57,7 +85,7 @@ export const DisplayArray: FC<DisplayArrayProps> = (props) => {

interface DisplayObjectProps {
value: { [key: string]: JsonData }
display?: DisplayChoices
mode?: DisplayMode
type: 'DisplayObject'
}

Expand All @@ -66,13 +94,13 @@ export const DisplayObject: FC<DisplayObjectProps> = (props) => {
if (CustomRenderComp) {
return <CustomRenderComp />
}
const { display, value } = props
const { mode, value } = props

return (
<>
{Object.entries(value).map(([key, v], i) => (
<span key={key}>
{key}: <DisplayComp type="Display" display={display} key={i} value={v} />,{' '}
{key}: <DisplayComp type="Display" mode={mode} key={i} value={v} />,{' '}
</span>
))}
</>
Expand All @@ -83,7 +111,7 @@ type JSONPrimitive = string | number | boolean | null

export interface DisplayPrimitiveProps {
value: JSONPrimitive
display: DisplayChoices
mode: DisplayMode
type: 'DisplayPrimitive'
}

Expand All @@ -92,28 +120,28 @@ export const DisplayPrimitive: FC<DisplayPrimitiveProps> = (props) => {
if (CustomRenderComp) {
return <CustomRenderComp />
}
const { display, value } = props
const { mode, value } = props

switch (display) {
case DisplayChoices.auto:
switch (mode) {
case DisplayMode.auto:
return <DisplayAuto value={value} />
case DisplayChoices.plain:
case DisplayMode.plain:
return <DisplayPlain value={value} />
case DisplayChoices.datetime:
case DisplayMode.datetime:
return <DisplayDateTime value={value} />
case DisplayChoices.date:
case DisplayMode.date:
return <DisplayDate value={value} />
case DisplayChoices.duration:
case DisplayMode.duration:
return <DisplayDuration value={value} />
case DisplayChoices.as_title:
case DisplayMode.as_title:
return <DisplayAsTitle value={value} />
case DisplayChoices.markdown:
case DisplayMode.markdown:
return <DisplayMarkdown value={value} />
case DisplayChoices.json:
case DisplayChoices.inline_code:
case DisplayMode.json:
case DisplayMode.inline_code:
return <DisplayInlineCode value={value} />
default:
unreachable('Unexpected display type', display, props)
unreachable('Unexpected display type', mode, props)
}
}

Expand Down Expand Up @@ -198,3 +226,46 @@ const DisplayInlineCode: FC<{ value: JSONPrimitive }> = ({ value }) => {
return <code className="fu-inline-code">{value.toString()}</code>
}
}

export type ModelData = Record<string, JsonData>

export interface DisplayLookupProps extends Omit<DisplayProps, 'type' | 'value'> {
field: string
tableWidthPercent?: number
}

export function renderEvent(event: AnyEvent | undefined, data: ModelData): AnyEvent | undefined {
let newEvent: AnyEvent | undefined = event ? { ...event } : undefined
if (newEvent) {
if (newEvent.type === 'go-to' && newEvent.url) {
// for go-to events with a URL, substitute the row values into the url
const url = subKeys(newEvent.url, data)
if (url === null) {
newEvent = undefined
} else {
newEvent.url = url
}
}
}
return newEvent
}

const subKeys = (template: string, row: ModelData): string | null => {
let returnNull = false
const r = template.replace(/{(.+?)}/g, (_, key: string): string => {
const v: JsonData | undefined = row[key]
if (v === undefined) {
throw new Error(`field "${key}" not found in ${JSON.stringify(row)}`)
} else if (v === null) {
returnNull = true
return 'null'
} else {
return v.toString()
}
})
if (returnNull) {
return null
} else {
return r
}
}
5 changes: 5 additions & 0 deletions packages/fastui/src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { NavbarProps, NavbarComp } from './navbar'
import { ModalComp, ModalProps } from './modal'
import { TableComp, TableProps } from './table'
import { PaginationProps, PaginationComp } from './pagination'
import { DetailsProps, DetailsComp } from './details'
import {
AllDisplayProps,
DisplayArray,
Expand Down Expand Up @@ -54,6 +55,7 @@ export type {
ModalProps,
TableProps,
PaginationProps,
DetailsProps,
LinkProps,
LinkListProps,
NavbarProps,
Expand Down Expand Up @@ -82,6 +84,7 @@ export type FastProps =
| ModalProps
| TableProps
| PaginationProps
| DetailsProps
| LinkProps
| LinkListProps
| NavbarProps
Expand Down Expand Up @@ -152,6 +155,8 @@ export const AnyComp: FC<FastProps> = (props) => {
return <TableComp {...props} />
case 'Pagination':
return <PaginationComp {...props} />
case 'Details':
return <DetailsComp {...props} />
case 'Display':
return <DisplayComp {...props} />
case 'DisplayArray':
Expand Down
Loading

0 comments on commit dd5ddaa

Please sign in to comment.