1import * as React from 'react';
2import ReactMarkdown from 'react-markdown';
3
4import { InlineCode } from '../base/code';
5
6import { createPermalinkedComponent } from '~/common/create-permalinked-component';
7import { HeadingType } from '~/common/headingManager';
8import { PDIV } from '~/components/base/paragraph';
9import { mdComponents, mdInlineComponents } from '~/components/plugins/api/APISectionUtils';
10import { Collapsible } from '~/ui/components/Collapsible';
11import { Cell, HeaderCell, Row, Table, TableHead } from '~/ui/components/Table';
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  nestingLevel: number;
43  type?: string;
44  example?: string;
45  expoKit?: string;
46  bareWorkflow?: string;
47};
48
49type AppConfigSchemaProps = {
50  schema: Record<string, Property>;
51};
52
53const Anchor = createPermalinkedComponent(PDIV, {
54  baseNestingLevel: 3,
55  sidebarType: HeadingType.InlineCode,
56});
57
58const PropertyName = ({ name, nestingLevel }: Pick<FormattedProperty, 'name' | 'nestingLevel'>) => (
59  <Anchor level={nestingLevel}>
60    <InlineCode>{name}</InlineCode>
61  </Anchor>
62);
63
64export function formatSchema(rawSchema: [string, Property][]) {
65  const formattedSchema: FormattedProperty[] = [];
66
67  rawSchema.map(property => {
68    appendProperty(formattedSchema, property, 0);
69  });
70
71  return formattedSchema;
72}
73
74// Appends a property and recursively appends sub-properties
75function appendProperty(
76  formattedSchema: FormattedProperty[],
77  property: [string, Property],
78  _nestingLevel: number
79) {
80  let nestingLevel = _nestingLevel;
81  const propertyKey = property[0];
82  const propertyValue = property[1];
83
84  if (propertyValue.meta && (propertyValue.meta.deprecated || propertyValue.meta.hidden)) {
85    return;
86  }
87
88  formattedSchema.push({
89    name: propertyKey,
90    description: createDescription(property),
91    nestingLevel,
92    type: _getType(propertyValue),
93    example: propertyValue.exampleString,
94    expoKit: propertyValue?.meta?.expoKit,
95    bareWorkflow: propertyValue?.meta?.bareWorkflow,
96  });
97
98  nestingLevel++;
99
100  if (propertyValue.properties) {
101    Object.entries(propertyValue.properties).forEach(subproperty => {
102      appendProperty(formattedSchema, subproperty, nestingLevel);
103    });
104  } //Note: sub-properties are sometimes nested within "items"
105  else if (propertyValue.items && propertyValue.items.properties) {
106    Object.entries(propertyValue.items.properties).forEach(subproperty => {
107      appendProperty(formattedSchema, subproperty, nestingLevel);
108    });
109  }
110}
111
112export function _getType(propertyValue: Property) {
113  if (propertyValue.enum) {
114    return 'enum';
115  } else {
116    return propertyValue.type?.toString().replace(',', ' || ');
117  }
118}
119
120export function createDescription(propertyEntry: [string, Property]) {
121  const propertyValue = propertyEntry[1];
122
123  let propertyDescription = `**(${_getType(propertyValue)})**`;
124  if (propertyValue.description) {
125    propertyDescription += ` - ` + propertyValue.description;
126  }
127  if (propertyValue.meta && propertyValue.meta.regexHuman) {
128    propertyDescription += `\n\n` + propertyValue.meta.regexHuman;
129  }
130
131  return propertyDescription;
132}
133
134const AppConfigSchemaPropertiesTable = ({ schema }: AppConfigSchemaProps) => {
135  const rawSchema = Object.entries(schema);
136  const formattedSchema = formatSchema(rawSchema);
137
138  return (
139    <Table>
140      <TableHead>
141        <Row>
142          <HeaderCell>Property</HeaderCell>
143          <HeaderCell>Description</HeaderCell>
144        </Row>
145      </TableHead>
146      <tbody>
147        {formattedSchema.map(
148          ({ name, description, nestingLevel, type, example, expoKit, bareWorkflow }, index) => (
149            <Row key={index}>
150              <Cell fitContent>
151                <div
152                  data-testid={name}
153                  style={{
154                    marginLeft: `${nestingLevel * 32}px`,
155                    display: nestingLevel ? 'list-item' : 'block',
156                    listStyleType: nestingLevel % 2 ? 'default' : 'circle',
157                    overflowX: 'visible',
158                  }}>
159                  <PropertyName name={name} nestingLevel={nestingLevel} />
160                </div>
161              </Cell>
162              <Cell>
163                <ReactMarkdown components={mdComponents}>{description}</ReactMarkdown>
164                {expoKit && (
165                  <Collapsible summary="ExpoKit">
166                    <ReactMarkdown components={mdComponents}>{expoKit}</ReactMarkdown>
167                  </Collapsible>
168                )}
169                {bareWorkflow && (
170                  <Collapsible summary="Bare Workflow">
171                    <ReactMarkdown components={mdComponents}>{bareWorkflow}</ReactMarkdown>
172                  </Collapsible>
173                )}
174                {example && (
175                  <ReactMarkdown components={mdInlineComponents}>
176                    {`> ${example.replaceAll('\n', '')}`}
177                  </ReactMarkdown>
178                )}
179              </Cell>
180            </Row>
181          )
182        )}
183      </tbody>
184    </Table>
185  );
186};
187
188export default AppConfigSchemaPropertiesTable;
189