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