1import { css } from '@emotion/react';
2import { borderRadius, breakpoints, spacing, theme, typography } from '@expo/styleguide';
3import ReactMarkdown from 'react-markdown';
4
5import { createPermalinkedComponent } from '~/common/create-permalinked-component';
6import { HeadingType } from '~/common/headingManager';
7import { APIBox } from '~/components/plugins/APIBox';
8import { mdComponents, mdInlineComponents } from '~/components/plugins/api/APISectionUtils';
9import { Collapsible } from '~/ui/components/Collapsible';
10import { P, CALLOUT, CODE } 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">
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    <br />
179    <ReactMarkdown components={mdComponents}>{description}</ReactMarkdown>
180    {expoKit && (
181      <Collapsible summary="ExpoKit">
182        <ReactMarkdown components={mdComponents}>{expoKit}</ReactMarkdown>
183      </Collapsible>
184    )}
185    {bareWorkflow && (
186      <Collapsible summary="Bare Workflow">
187        <ReactMarkdown components={mdComponents}>{bareWorkflow}</ReactMarkdown>
188      </Collapsible>
189    )}
190    {example && <ReactMarkdown components={mdInlineComponents}>{`> ${example}`}</ReactMarkdown>}
191    <div>
192      {subproperties.length > 0 &&
193        subproperties.map((formattedProperty, index) => (
194          <AppConfigProperty
195            {...formattedProperty}
196            key={`${name}-${index}`}
197            nestingLevel={nestingLevel + 1}
198          />
199        ))}
200    </div>
201  </APIBox>
202);
203
204const boxStyle = css({
205  boxShadow: 'none',
206  marginBottom: 0,
207  borderRadius: 0,
208  borderBottomWidth: 0,
209
210  '&:first-of-type': {
211    borderTopLeftRadius: borderRadius.medium,
212    borderTopRightRadius: borderRadius.medium,
213  },
214
215  '&:last-of-type': {
216    borderBottomLeftRadius: borderRadius.medium,
217    borderBottomRightRadius: borderRadius.medium,
218    marginBottom: spacing[4],
219    borderBottomWidth: 1,
220  },
221
222  [`@media screen and (max-width: ${breakpoints.medium + 124}px)`]: {
223    paddingTop: spacing[4],
224  },
225});
226
227const secondaryCodeLineStyle = css({
228  fontFamily: typography.fontStacks.mono,
229  color: theme.text.secondary,
230  padding: `0 ${spacing[1]}px`,
231  wordBreak: 'break-word',
232});
233
234export default AppConfigSchemaPropertiesTable;
235