1import { css } from '@emotion/react'; 2import { borderRadius, breakpoints, spacing, theme, typography } from '@expo/styleguide'; 3import ReactMarkdown from 'react-markdown'; 4 5import { HeadingType } from '~/common/headingManager'; 6import { APIBox } from '~/components/plugins/APIBox'; 7import { mdComponents } from '~/components/plugins/api/APISectionUtils'; 8import { Callout } from '~/ui/components/Callout'; 9import { Collapsible } from '~/ui/components/Collapsible'; 10import { P, CALLOUT, CODE, createPermalinkedComponent, BOLD } from '~/ui/components/Text'; 11 12type PropertyMeta = { 13 regexHuman?: string; 14 deprecated?: boolean; 15 hidden?: boolean; 16 expoKit?: string; 17 bareWorkflow?: string; 18}; 19 20export type Property = { 21 description?: string; 22 type?: string | string[]; 23 meta?: PropertyMeta; 24 pattern?: string; 25 enum?: string[]; 26 example?: any; 27 exampleString?: string; 28 host?: object; 29 properties?: Record<string, Property>; 30 items?: { 31 properties?: Record<string, Property>; 32 [key: string]: any; 33 }; 34 uniqueItems?: boolean; 35 additionalProperties?: boolean; 36}; 37 38type FormattedProperty = { 39 name: string; 40 description: string; 41 type?: string; 42 example?: string; 43 expoKit?: string; 44 bareWorkflow?: string; 45 subproperties: FormattedProperty[]; 46 parent?: string; 47}; 48 49type AppConfigSchemaProps = { 50 schema: Record<string, Property>; 51}; 52 53const Anchor = createPermalinkedComponent(P, { 54 baseNestingLevel: 3, 55 sidebarType: HeadingType.InlineCode, 56}); 57 58const PropertyName = ({ name, nestingLevel }: { name: string; nestingLevel: number }) => ( 59 <Anchor level={nestingLevel} data-testid={name} data-heading="true" css={propertyNameStyle}> 60 <CODE css={typography.fontSizes[16]}>{name}</CODE> 61 </Anchor> 62); 63 64const propertyNameStyle = css({ marginBottom: spacing[4] }); 65 66export function formatSchema(rawSchema: [string, Property][]) { 67 const formattedSchema: FormattedProperty[] = []; 68 69 rawSchema.map(property => { 70 appendProperty(formattedSchema, property); 71 }); 72 73 return formattedSchema; 74} 75 76function appendProperty(formattedSchema: FormattedProperty[], property: [string, Property]) { 77 const propertyValue = property[1]; 78 79 if (propertyValue.meta && (propertyValue.meta.deprecated || propertyValue.meta.hidden)) { 80 return; 81 } 82 83 formattedSchema.push(formatProperty(property)); 84} 85 86function formatProperty(property: [string, Property], parent?: string): FormattedProperty { 87 const propertyKey = property[0]; 88 const propertyValue = property[1]; 89 90 const subproperties: FormattedProperty[] = []; 91 92 if (propertyValue.properties) { 93 Object.entries(propertyValue.properties).forEach(subproperty => { 94 subproperties.push( 95 formatProperty(subproperty, parent ? `${parent}.${propertyKey}` : propertyKey) 96 ); 97 }); 98 } // note: sub-properties are sometimes nested within "items" 99 else if (propertyValue.items && propertyValue.items.properties) { 100 Object.entries(propertyValue.items.properties).forEach(subproperty => { 101 subproperties.push( 102 formatProperty(subproperty, parent ? `${parent}.${propertyKey}` : propertyKey) 103 ); 104 }); 105 } 106 107 return { 108 name: propertyKey, 109 description: createDescription(property), 110 type: _getType(propertyValue), 111 example: propertyValue.exampleString?.replaceAll('\n', ''), 112 expoKit: propertyValue?.meta?.expoKit, 113 bareWorkflow: propertyValue?.meta?.bareWorkflow, 114 subproperties, 115 parent, 116 }; 117} 118 119export function _getType({ enum: enm, type }: Partial<Property>) { 120 return enm ? 'enum' : type?.toString().replace(',', ' || '); 121} 122 123export function createDescription(propertyEntry: [string, Property]) { 124 const { description, meta } = propertyEntry[1]; 125 126 let propertyDescription = ``; 127 if (description) { 128 propertyDescription += description; 129 } 130 if (meta && meta.regexHuman) { 131 propertyDescription += `\n\n` + meta.regexHuman; 132 } 133 134 return propertyDescription; 135} 136 137const AppConfigSchemaPropertiesTable = ({ schema }: AppConfigSchemaProps) => { 138 const rawSchema = Object.entries(schema); 139 const formattedSchema = formatSchema(rawSchema); 140 141 return ( 142 <> 143 {formattedSchema.map((formattedProperty, index) => ( 144 <AppConfigProperty 145 {...formattedProperty} 146 key={`${formattedProperty.name}-${index}`} 147 nestingLevel={0} 148 /> 149 ))} 150 </> 151 ); 152}; 153 154const AppConfigProperty = ({ 155 name, 156 description, 157 example, 158 expoKit, 159 bareWorkflow, 160 type, 161 nestingLevel, 162 subproperties, 163 parent, 164}: FormattedProperty & { nestingLevel: number }) => ( 165 <APIBox css={boxStyle}> 166 <PropertyName name={name} nestingLevel={nestingLevel} /> 167 <CALLOUT theme="secondary" data-text="true" css={typeRow}> 168 Type: <CODE>{type || 'undefined'}</CODE> 169 {nestingLevel > 0 && ( 170 <> 171  • Path:{' '} 172 <code css={secondaryCodeLineStyle}> 173 {parent}.{name} 174 </code> 175 </> 176 )} 177 </CALLOUT> 178 <ReactMarkdown components={mdComponents}>{description}</ReactMarkdown> 179 {expoKit && ( 180 <Collapsible summary="ExpoKit"> 181 <ReactMarkdown components={mdComponents}>{expoKit}</ReactMarkdown> 182 </Collapsible> 183 )} 184 {bareWorkflow && ( 185 <Collapsible summary="Bare Workflow"> 186 <ReactMarkdown components={mdComponents}>{bareWorkflow}</ReactMarkdown> 187 </Collapsible> 188 )} 189 {example && ( 190 <Callout> 191 <BOLD>Example</BOLD> 192 <ReactMarkdown components={mdComponents}>{example}</ReactMarkdown> 193 </Callout> 194 )} 195 <div> 196 {subproperties.length > 0 && 197 subproperties.map((formattedProperty, index) => ( 198 <AppConfigProperty 199 {...formattedProperty} 200 key={`${name}-${index}`} 201 nestingLevel={nestingLevel + 1} 202 /> 203 ))} 204 </div> 205 </APIBox> 206); 207 208const boxStyle = css({ 209 boxShadow: 'none', 210 marginBottom: 0, 211 borderRadius: 0, 212 borderBottomWidth: 0, 213 paddingBottom: 0, 214 215 '&:first-of-type': { 216 borderTopLeftRadius: borderRadius.md, 217 borderTopRightRadius: borderRadius.md, 218 }, 219 220 '&:last-of-type': { 221 borderBottomLeftRadius: borderRadius.md, 222 borderBottomRightRadius: borderRadius.md, 223 marginBottom: spacing[4], 224 borderBottomWidth: 1, 225 }, 226 227 [`@media screen and (max-width: ${breakpoints.medium + 124}px)`]: { 228 paddingTop: spacing[4], 229 }, 230}); 231 232const secondaryCodeLineStyle = css({ 233 color: theme.text.secondary, 234 padding: `0 ${spacing[1]}px`, 235 wordBreak: 'break-word', 236}); 237 238const typeRow = css({ 239 margin: `${spacing[3]}px 0`, 240}); 241 242export default AppConfigSchemaPropertiesTable; 243