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          &emsp;&bull;&emsp;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