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