1import { css } from '@emotion/react';
2import { borderRadius, breakpoints, spacing, theme, typography } from '@expo/styleguide';
3import ReactMarkdown from 'react-markdown';
4
5import { InlineCode } from '../base/code';
6
7import { createPermalinkedComponent } from '~/common/create-permalinked-component';
8import { HeadingType } from '~/common/headingManager';
9import { PDIVHEADER } from '~/components/base/paragraph';
10import { APIBox } from '~/components/plugins/APIBox';
11import { mdComponents, mdInlineComponents } from '~/components/plugins/api/APISectionUtils';
12import { Collapsible } from '~/ui/components/Collapsible';
13import { CALLOUT } from '~/ui/components/Text';
14
15type PropertyMeta = {
16  regexHuman?: string;
17  deprecated?: boolean;
18  hidden?: boolean;
19  expoKit?: string;
20  bareWorkflow?: string;
21};
22
23export type Property = {
24  description?: string;
25  type?: string | string[];
26  meta?: PropertyMeta;
27  pattern?: string;
28  enum?: string[];
29  example?: any;
30  exampleString?: string;
31  host?: object;
32  properties?: Record<string, Property>;
33  items?: {
34    properties?: Record<string, Property>;
35    [key: string]: any;
36  };
37  uniqueItems?: boolean;
38  additionalProperties?: boolean;
39};
40
41type FormattedProperty = {
42  name: string;
43  description: string;
44  type?: string;
45  example?: string;
46  expoKit?: string;
47  bareWorkflow?: string;
48  subproperties: FormattedProperty[];
49  parent?: string;
50};
51
52type AppConfigSchemaProps = {
53  schema: Record<string, Property>;
54};
55
56const Anchor = createPermalinkedComponent(PDIVHEADER, {
57  baseNestingLevel: 3,
58  sidebarType: HeadingType.InlineCode,
59});
60
61const PropertyName = ({ name, nestingLevel }: { name: string; nestingLevel: number }) => (
62  <Anchor level={nestingLevel} data-testid={name} data-heading="true">
63    <InlineCode css={typography.fontSizes[16]}>{name}</InlineCode>
64  </Anchor>
65);
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    <div>
144      {formattedSchema.map((formattedProperty, index) => (
145        <AppConfigProperty
146          {...formattedProperty}
147          key={`${formattedProperty.name}-${index}`}
148          nestingLevel={0}
149        />
150      ))}
151    </div>
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">
169      Type: <InlineCode>{type || 'undefined'}</InlineCode>
170      {nestingLevel > 0 && (
171        <>
172          &emsp;&bull;&emsp;Path:{' '}
173          <code css={secondaryCodeLineStyle}>
174            {parent}.{name}
175          </code>
176        </>
177      )}
178    </CALLOUT>
179    <br />
180    <ReactMarkdown components={mdComponents}>{description}</ReactMarkdown>
181    {expoKit && (
182      <Collapsible summary="ExpoKit">
183        <ReactMarkdown components={mdComponents}>{expoKit}</ReactMarkdown>
184      </Collapsible>
185    )}
186    {bareWorkflow && (
187      <Collapsible summary="Bare Workflow">
188        <ReactMarkdown components={mdComponents}>{bareWorkflow}</ReactMarkdown>
189      </Collapsible>
190    )}
191    {example && <ReactMarkdown components={mdInlineComponents}>{`> ${example}`}</ReactMarkdown>}
192    <div>
193      {subproperties.length > 0 &&
194        subproperties.map((formattedProperty, index) => (
195          <AppConfigProperty
196            {...formattedProperty}
197            key={`${name}-${index}`}
198            nestingLevel={nestingLevel + 1}
199          />
200        ))}
201    </div>
202  </APIBox>
203);
204
205const boxStyle = css({
206  boxShadow: 'none',
207  marginBottom: 0,
208  borderRadius: 0,
209  borderBottomWidth: 0,
210
211  '&:first-of-type': {
212    borderTopLeftRadius: borderRadius.medium,
213    borderTopRightRadius: borderRadius.medium,
214  },
215
216  '&:last-of-type': {
217    borderBottomLeftRadius: borderRadius.medium,
218    borderBottomRightRadius: borderRadius.medium,
219    marginBottom: spacing[4],
220    borderBottomWidth: 1,
221  },
222
223  [`@media screen and (max-width: ${breakpoints.medium + 124}px)`]: {
224    paddingTop: spacing[4],
225  },
226});
227
228const secondaryCodeLineStyle = css({
229  fontFamily: typography.fontStacks.mono,
230  color: theme.text.secondary,
231  padding: `0 ${spacing[1]}px`,
232  wordBreak: 'break-word',
233});
234
235export default AppConfigSchemaPropertiesTable;
236