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