Skip to content

Commit

Permalink
Merge pull request #820 from interval/table-cell-image
Browse files Browse the repository at this point in the history
Table cell image
  • Loading branch information
jacobmischka authored Sep 16, 2022
2 parents 08d5861 + e25e64e commit f30f24d
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 115 deletions.
5 changes: 5 additions & 0 deletions src/classes/IntervalError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default class IntervalError extends Error {
constructor(message: string) {
super(message)
}
}
37 changes: 2 additions & 35 deletions src/components/displayImage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { T_IO_PROPS, ImageSize } from '../ioSchema'
import { IntervalError } from '..'

const MAX_BUFFER_SIZE_MB = 50
import { bufferToDataUrl } from '../utils/image'

export default function displayImage(
props: {
Expand All @@ -24,41 +22,10 @@ export default function displayImage(
props.height = size ? size : props.height

if ('buffer' in props) {
if (Buffer.byteLength(props.buffer) > MAX_BUFFER_SIZE_MB * 1000 * 1000) {
throw new IntervalError(
`Buffer for io.display.image is too large, must be under ${MAX_BUFFER_SIZE_MB} MB`
)
}

const data = props.buffer.toString('base64')

// using first character as a simple check for common image formats:
// https://stackoverflow.com/questions/27886677/javascript-get-extension-from-base64-image/50111377#50111377
// image/unknown actually seems to just work for all the types I've
// encountered, but we can expand this switch if we need to
let mime
switch (data[0]) {
case 'i':
mime = 'image/png'
break
case 'R':
mime = 'image/gif'
break
case '/':
mime = 'image/jpeg'
break
case 'U':
mime = 'image/webp'
break
default:
mime = 'image/unknown'
break
}

return {
props: {
...props,
url: `data:${mime};base64,${data}`,
url: bufferToDataUrl(props.buffer),
},
}
} else {
Expand Down
1 change: 1 addition & 0 deletions src/components/displayTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default function displayTable(logger: Logger) {
row,
columns,
menuBuilder: props.rowMenuItems,
logger,
})
)

Expand Down
1 change: 1 addition & 0 deletions src/components/selectTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default function selectTable(logger: Logger) {
row,
columns,
menuBuilder: props.rowMenuItems,
logger,
})
)

Expand Down
143 changes: 143 additions & 0 deletions src/examples/basic/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function generateRows(count: number) {
.fill(null)
.map((_, i) => ({
id: i,
name: `${faker.name.firstName()} ${faker.name.lastName()}`,
email: faker.internet.email(),
description: faker.helpers.arrayElement([
faker.random.word(),
Expand All @@ -16,6 +17,12 @@ function generateRows(count: number) {
number: faker.datatype.number(100),
boolean: faker.datatype.boolean(),
date: faker.datatype.datetime(),
image: faker.image.imageUrl(
480,
Math.random() < 0.25 ? 300 : 480,
undefined,
true
),
}))
}

Expand Down Expand Up @@ -57,6 +64,17 @@ export const display_table: IntervalActionHandler = async io => {
defaultPageSize: 50,
columns: [
'id',
{
label: 'User',
renderCell: row => ({
label: row.name,
image: {
alt: 'Alt tag',
url: row.image,
size: 'small',
},
}),
},
'description',
'boolean',
'date',
Expand Down Expand Up @@ -231,3 +249,128 @@ export const table_custom: IntervalActionHandler = async io => {
await io.display.object('Selected', { data: selections })
}
}

export const image_viewer: IntervalActionHandler = async io => {
const data = Array(50)
.fill(null)
.map((_, i) => {
const [width, height, crazyW, crazyH] = [
faker.datatype.number({ min: 500, max: 700 }),
faker.datatype.number({ min: 200, max: 400 }),
faker.datatype.number({ min: 100, max: 999 }),
faker.datatype.number({ min: 100, max: 999 }),
]

return {
id: i,
name: faker.name.findName(),
square: faker.image.avatar(),
width,
height,
crazyW,
crazyH,
wide: faker.image.imageUrl(width, height, undefined, true),
tall: faker.image.imageUrl(height, width, undefined, true),
crazy: faker.image.imageUrl(crazyW, crazyH, undefined, true),
}
})

await io.display.table('Images', {
data,
defaultPageSize: 50,
columns: [
'id',
{
label: 'Square',
renderCell: row => ({
image: {
alt: 'Alt tag',
url: row.square,
size: 'small',
},
}),
},
{
label: 'Tall',
renderCell: row => ({
label: `${row.height} x ${row.width}`,
image: {
alt: 'Alt tag',
url: row.tall,
size: 'small',
},
}),
},
{
label: 'Wide',
renderCell: row => ({
label: `${row.width} x ${row.height}`,
image: {
alt: 'Alt tag',
url: row.wide,
size: 'small',
},
}),
},
{
label: 'Crazy',
renderCell: row => ({
label: `${row.crazyW} x ${row.crazyH}`,
image: {
alt: 'Alt tag',
url: row.crazy,
size: 'small',
},
}),
},
],
})

await io.display.table('Image sizes', {
data,
defaultPageSize: 50,
columns: [
'id',
{
label: 'Thumbnail',
renderCell: row => ({
image: {
alt: 'Alt tag',
url: row.wide,
size: 'thumbnail',
},
}),
},
{
label: 'Small',
renderCell: row => ({
image: {
alt: 'Alt tag',
url: row.wide,
size: 'small',
},
}),
},
{
label: 'Medium',
renderCell: row => ({
image: {
alt: 'Alt tag',
url: row.wide,
size: 'medium',
},
}),
},
{
label: 'Large',
renderCell: row => ({
image: {
alt: 'Alt tag',
url: row.wide,
size: 'large',
},
}),
},
],
})
}
9 changes: 2 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
NotifyConfig,
IntervalActionDefinitions,
} from './types'
import IntervalError from './classes/IntervalError'
import IntervalClient, {
DEFAULT_WEBSOCKET_ENDPOINT,
getHttpEndpoint,
Expand Down Expand Up @@ -47,12 +48,6 @@ export interface QueuedAction {
params?: SerializableRecord
}

export class IntervalError extends Error {
constructor(message: string) {
super(message)
}
}

export function getActionStore(): IntervalActionStore {
const store = actionLocalStorage.getStore()
if (!store) {
Expand Down Expand Up @@ -193,4 +188,4 @@ export default class Interval {
}
}

export { Interval, IOError }
export { Interval, IOError, IntervalError }
70 changes: 13 additions & 57 deletions src/ioSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ export type Serializable = z.infer<typeof serializableSchema>
export const serializableRecord = z.record(serializableSchema)
export type SerializableRecord = z.infer<typeof serializableRecord>

export const imageSize = z.enum(['thumbnail', 'small', 'medium', 'large'])
export type ImageSize = z.infer<typeof imageSize>

export const tableRowValue = z.union([
z.string(),
z.number(),
Expand All @@ -128,7 +131,7 @@ export const tableRowValue = z.union([
z.undefined(),
z.bigint(),
z.object({
label: z.string(),
label: z.string().optional(),
value: z
.union([
z.string(),
Expand All @@ -141,6 +144,14 @@ export const tableRowValue = z.union([
.optional(),
href: z.string().optional(),
url: z.string().optional(),
image: z
.object({
alt: z.string().optional(),
width: imageSize.optional(),
height: imageSize.optional(),
url: z.string(),
})
.optional(),
action: z.string().optional(),
params: serializableRecord.optional(),
}),
Expand All @@ -150,7 +161,7 @@ export const tableRow = z
.record(tableRowValue)
// Allow arbitrary objects/interfaces with specified column mappings.
// If no columns specified, we'll just serialize any nested objects.
.or(z.object({}).passthrough())
.or(z.record(z.any()))

export const menuItem = z.intersection(
z.object({
Expand Down Expand Up @@ -195,58 +206,6 @@ export const internalTableRow = z.object({
filterValue: z.string().optional(),
})

export const tableColumn = z.object({
label: z.string(),
renderCell: z
.function()
.args(z.any())
.returns(
z.union([
z.intersection(
z.object({
label: z.union([
z.string(),
z.number(),
z.boolean(),
z.date(),
z.null(),
z.undefined(),
]),
value: z
.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.date(),
z.undefined(),
])
.optional(),
}),
z.union([
z.object({
url: z.string(),
}),
z.object({
href: z.string(),
}),
z.object({
action: z.string(),
params: serializableRecord.optional(),
}),
z.object({}),
])
),
z.string(),
z.number(),
z.boolean(),
z.date(),
z.null(),
z.undefined(),
])
),
})

export const internalTableColumn = z.object({
label: z.string(),
})
Expand All @@ -263,9 +222,6 @@ export const CURRENCIES = [
export const currencyCode = z.enum(CURRENCIES)
export type CurrencyCode = z.infer<typeof currencyCode>

export const imageSize = z.enum(['thumbnail', 'small', 'medium', 'large'])
export type ImageSize = z.infer<typeof imageSize>

export const dateObject = z.object({
year: z.number(),
month: z.number(),
Expand Down
Loading

0 comments on commit f30f24d

Please sign in to comment.