import * as React from 'react' import * as types from 'notion-types' import formatNumber from 'format-number' import format from 'date-fns/format/index.js' import { cs } from '../utils' import { useNotionContext } from '../context' import { Checkbox } from '../components/checkbox' import { Text } from '../components/text' import { PageTitle } from '../components/page-title' import { GracefulImage } from '../components/graceful-image' import { evalFormula } from './eval-formula' export interface IPropertyProps { propertyId?: string schema?: types.CollectionPropertySchema data?: types.Decoration[] block?: types.Block collection?: types.Collection inline?: boolean linkToTitlePage?: boolean pageHeader?: boolean } /** * Renders a single value of structured Notion data according to its schema. * * This corresponds to rendering the content of a single cell in a table. * Property rendering is re-used across all the different types of collection views. */ export const Property: React.FC = (props) => { const { components } = useNotionContext() if (components.Property) { return } else { return } } export const PropertyImpl: React.FC = (props) => { const { components, mapImageUrl, mapPageUrl } = useNotionContext() const { schema, data, block, collection, inline = false, linkToTitlePage = true } = props const renderTextValue = React.useMemo( () => function TextProperty() { return }, [block, data] ) const renderDateValue = React.useMemo( () => function DateProperty() { return }, [block, data] ) const renderRelationValue = React.useMemo( () => function RelationProperty() { return }, [block, data] ) const renderFormulaValue = React.useMemo( () => function FormulaProperty() { let content: string try { let content = evalFormula(schema.formula, { schema: collection?.schema, properties: block?.properties }) if (isNaN(content as number)) { // console.log('NaN', schema.formula) } if (content instanceof Date) { content = format(content, 'MMM d, YYY hh:mm aa') } } catch (err) { // console.log('error evaluating formula', schema.formula, err) content = null } return content }, [block?.properties, collection?.schema, schema] ) const renderTitleValue = React.useMemo( () => function FormulaTitle() { if (block && linkToTitlePage) { return ( ) } else { return } }, [block, components, data, linkToTitlePage, mapPageUrl] ) const renderPersonValue = React.useMemo( () => function PersonProperty() { // console.log('person', schema, data) return }, [block, data] ) const renderFileValue = React.useMemo( () => function FileProperty() { // TODO: assets should be previewable via image-zoom const files = data .filter((v) => v.length === 2) .map((f) => f.flat().flat()) return files.map((file, i) => ( )) }, [block, components, data, mapImageUrl] ) const renderCheckboxValue = React.useMemo( () => function CheckboxProperty() { const isChecked = data && data[0][0] === 'Yes' return (
{schema.name}
) }, [data, schema] ) const renderUrlValue = React.useMemo( () => function UrlProperty() { // TODO: refactor to less hacky solution const d = JSON.parse(JSON.stringify(data)) if (inline) { try { const url = new URL(d[0][0]) d[0][0] = url.hostname.replace(/^www\./, '') } catch (err) { // ignore invalid urls } } return ( ) }, [block, data, inline] ) const renderEmailValue = React.useMemo( () => function EmailProperty() { return }, [block, data] ) const renderPhoneNumberValue = React.useMemo( () => function PhoneNumberProperty() { return }, [block, data] ) const renderNumberValue = React.useMemo( () => function NumberProperty() { const value = parseFloat(data[0][0] || '0') let output = '' if (isNaN(value)) { return } else { switch (schema.number_format) { case 'number_with_commas': output = formatNumber()(value) break case 'percent': output = formatNumber({ suffix: '%' })(value * 100) break case 'dollar': output = formatNumber({ prefix: '$', round: 2, padRight: 2 })( value ) break case 'euro': output = formatNumber({ prefix: '€', round: 2, padRight: 2 })( value ) break case 'pound': output = formatNumber({ prefix: '£', round: 2, padRight: 2 })( value ) break case 'yen': output = formatNumber({ prefix: '¥', round: 0 })(value) break case 'rupee': output = formatNumber({ prefix: '₹', round: 2, padRight: 2 })( value ) break case 'won': output = formatNumber({ prefix: '₩', round: 0 })(value) break case 'yuan': output = formatNumber({ prefix: 'CN¥', round: 2, padRight: 2 })( value ) break default: return } return } }, [block, data, schema] ) const renderCreatedTimeValue = React.useMemo( () => function CreatedTimeProperty() { return format(new Date(block?.created_time), 'MMM d, YYY hh:mm aa') }, [block?.created_time] ) const renderLastEditedTimeValue = React.useMemo( () => function LastEditedTimeProperty() { return format(new Date(block?.last_edited_time), 'MMM d, YYY hh:mm aa') }, [block?.last_edited_time] ) if (!schema) { return null } let content = null if ( data || schema.type === 'checkbox' || schema.type === 'title' || schema.type === 'formula' || schema.type === 'created_by' || schema.type === 'last_edited_by' || schema.type === 'created_time' || schema.type === 'last_edited_time' ) { switch (schema.type) { case 'relation': content = components.propertyRelationValue(props, renderRelationValue) break case 'formula': // TODO // console.log('formula', schema.formula, { // schema: collection?.schema, // properties: block?.properties // }) content = components.propertyFormulaValue(props, renderFormulaValue) break case 'title': content = components.propertyTitleValue(props, renderTitleValue) break case 'select': // intentional fallthrough case 'multi_select': { const values = (data[0][0] || '').split(',') content = values.map((value, index) => { const option = schema.options?.find( (option) => value === option.value ) const color = option?.color return components.propertySelectValue( { ...props, key: index, value, option, color }, () => (
{value}
) ) }) break } case 'person': content = components.propertyPersonValue(props, renderPersonValue) break case 'file': content = components.propertyFileValue(props, renderFileValue) break case 'checkbox': content = components.propertyCheckboxValue(props, renderCheckboxValue) break case 'url': content = components.propertyUrlValue(props, renderUrlValue) break case 'email': content = components.propertyEmailValue(props, renderEmailValue) break case 'phone_number': content = components.propertyPhoneNumberValue( props, renderPhoneNumberValue ) break case 'number': content = components.propertyNumberValue(props, renderNumberValue) break case 'created_time': content = components.propertyCreatedTimeValue( props, renderCreatedTimeValue ) break case 'last_edited_time': content = components.propertyLastEditedTimeValue( props, renderLastEditedTimeValue ) break case 'created_by': // TODO // console.log('created_by', schema, data) break case 'last_edited_by': // TODO // console.log('last_edited_by', schema, data) break case 'text': content = components.propertyTextValue(props, renderTextValue) break case 'date': content = components.propertyDateValue(props, renderDateValue) break default: content = break } } return ( {content} ) } export const PropertyImplMemo = React.memo(PropertyImpl)