Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[docs-infra] Simplify API sections typing #43128

Merged
merged 20 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 17 additions & 25 deletions docs/src/modules/components/ApiPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { ComponentApiContent, PropsTranslations } from '@mui-internal/api-docs-builder';
import Typography from '@mui/material/Typography';
import Alert from '@mui/material/Alert';
import { TableOfContentsEntry } from '@mui/internal-markdown';
import { Ad, AdGuest } from '@mui/docs/Ad';
import VerifiedRoundedIcon from '@mui/icons-material/VerifiedRounded';
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
Expand All @@ -13,18 +14,15 @@ import { BrandingProvider } from '@mui/docs/branding';
import { SectionTitle, SectionTitleProps } from '@mui/docs/SectionTitle';
import { MarkdownElement } from '@mui/docs/MarkdownElement';
import AppLayoutDocs from 'docs/src/modules/components/AppLayoutDocs';
import PropertiesSection, {
getPropsToC,
} from 'docs/src/modules/components/ApiPage/sections/PropertiesSection';
import ClassesSection, {
getClassesToC,
} from 'docs/src/modules/components/ApiPage/sections/ClassesSection';
import PropertiesSection from 'docs/src/modules/components/ApiPage/sections/PropertiesSection';
import ClassesSection from 'docs/src/modules/components/ApiPage/sections/ClassesSection';
import SlotsSection from 'docs/src/modules/components/ApiPage/sections/SlotsSection';
import {
ApiDisplayOptions,
DEFAULT_API_LAYOUT_STORAGE_KEYS,
} from 'docs/src/modules/components/ApiPage/sections/ToggleDisplayOption';
import { TableOfContentsEntry } from '@mui/internal-markdown';
import { propsApiProcessor } from 'docs/src/modules/components/ApiPage/processors/properties';
import { classesApiProcessor } from 'docs/src/modules/components/ApiPage/processors/classes';

type ApiHeaderKeys =
| 'demos'
Expand Down Expand Up @@ -169,19 +167,9 @@ export default function ApiPage(props: ApiPageProps) {
createTocEntry('demos'),
createTocEntry('import'),
...componentDescriptionToc,
getPropsToC({
t,
componentName: pageContent.name,
componentProps,
inheritance,
themeDefaultProps: pageContent.themeDefaultProps,
}),
createTocEntry('props'),
componentSlots?.length > 0 && createTocEntry('slots'),
...getClassesToC({
t,
componentName: pageContent.name,
componentClasses,
}),
createTocEntry('classes'),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to bring that feature back. I initially removed it because I remembered that with the table layout it was useless, but did not manage to find back this conclusion on github

].filter(Boolean);

// The `ref` is forwarded to the root element.
Expand Down Expand Up @@ -272,9 +260,11 @@ export default function ApiPage(props: ApiPageProps) {
</React.Fragment>
) : null}
<PropertiesSection
properties={componentProps}
propertiesDescriptions={propDescriptions}
componentName={pageContent.name}
properties={propsApiProcessor({
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the new way of defining API pages, such that if someone need to pass other kind of data, they can do it

The previous one should still work the time of the migration

componentName: pageContent.name,
properties: componentProps,
propertiesDescriptions: propDescriptions,
})}
spreadHint={spreadHint}
defaultLayout={defaultLayout}
layoutStorageKey={layoutStorageKey.props}
Expand Down Expand Up @@ -339,9 +329,11 @@ export default function ApiPage(props: ApiPageProps) {
/>
)
<ClassesSection
componentClasses={componentClasses}
componentName={pageContent.name}
classDescriptions={classDescriptions}
classes={classesApiProcessor({
componentClasses,
componentName: pageContent.name,
classDescriptions,
})}
spreadHint={t('api-docs.classesDescription')}
styleOverridesLink={styleOverridesLink}
defaultLayout={defaultLayout}
Expand Down
27 changes: 11 additions & 16 deletions docs/src/modules/components/ApiPage/list/ClassesList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/* eslint-disable react/no-danger */
import * as React from 'react';
import { styled } from '@mui/material/styles';
import kebabCase from 'lodash/kebabCase';
import { ComponentClassDefinition } from '@mui/internal-docs-utils';
import { useTranslate } from '@mui/docs/i18n';
import ExpandableApiItem, {
ApiItemContaier,
ApiItemContainer,
} from 'docs/src/modules/components/ApiPage/list/ExpandableApiItem';
import { ClassDefinition } from 'docs/src/modules/components/ApiPage/common/classes';
import {
brandingLightTheme as lightTheme,
brandingDarkTheme as darkTheme,
Expand Down Expand Up @@ -49,27 +48,23 @@ const StyledApiItem = styled(ExpandableApiItem)(
}),
);

type HashParams = { componentName: string; className: string };

export function getHash({ componentName, className }: HashParams) {
return `${kebabCase(componentName)}-classes-${className}`;
}

type ClassesListProps = {
componentName: string;
classes: ComponentClassDefinition[];
classes: ClassDefinition[];
displayOption: 'collapsed' | 'expanded';
/**
* If `true` the the associated key in the classes object is visible.
*/
displayClassKeys?: boolean;
};

export default function ClassesList(props: ClassesListProps) {
const { classes, displayOption, componentName, displayClassKeys } = props;
const { classes, displayOption, displayClassKeys } = props;
const t = useTranslate();

return (
<ApiItemContaier>
<ApiItemContainer>
{classes.map((classDefinition) => {
const { className, key, description, isGlobal, isDeprecated, deprecationInfo } =
const { hash, className, key, description, isGlobal, isDeprecated, deprecationInfo } =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generating the hash is now the responsibility of the processors.

classDefinition;

let note = isGlobal ? t('api-docs.state') : '';
Expand All @@ -80,7 +75,7 @@ export default function ClassesList(props: ClassesListProps) {

return (
<StyledApiItem
id={getHash({ componentName, className: key })}
id={hash}
key={key}
note={note}
title={`.${className}`}
Expand Down Expand Up @@ -114,6 +109,6 @@ export default function ClassesList(props: ClassesListProps) {
</StyledApiItem>
);
})}
</ApiItemContaier>
</ApiItemContainer>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export default function ExpandableApiItem(props: ExpandableApiItemProps) {
);
}

export const ApiItemContaier = styled('div')({
export const ApiItemContainer = styled('div')({
width: '100%',
display: 'flex',
flexDirection: 'column',
Expand Down
60 changes: 8 additions & 52 deletions docs/src/modules/components/ApiPage/list/PropertiesList.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/* eslint-disable react/no-danger */
import * as React from 'react';
import { styled } from '@mui/material/styles';
import kebabCase from 'lodash/kebabCase';
import { useTranslate } from '@mui/docs/i18n';
import {
brandingDarkTheme as darkTheme,
brandingLightTheme as lightTheme,
} from '@mui/docs/branding';
import ExpandableApiItem, {
ApiItemContaier,
ApiItemContainer,
} from 'docs/src/modules/components/ApiPage/list/ExpandableApiItem';
import ApiWarningAlert from 'docs/src/modules/components/ApiPage/ApiWarningAlert';
import { PropertyDefinition } from 'docs/src/modules/components/ApiPage/common/properties';

const StyledApiItem = styled(ExpandableApiItem)(
({ theme }) => ({
Expand Down Expand Up @@ -111,61 +111,18 @@ function PropDescription(props: { description: string }) {
);
}

export function getHash({
componentName,
propName,
hooksParameters,
hooksReturnValue,
}: {
componentName: string;
propName: string;
hooksParameters?: boolean;
hooksReturnValue?: boolean;
}) {
let sectionName = 'prop';
if (hooksParameters) {
sectionName = 'parameters';
} else if (hooksReturnValue) {
sectionName = 'return-value';
}
return `${kebabCase(componentName)}-${sectionName}-${propName}`;
}

export interface Properties {
additionalInfo: string[];
componentName: string;
deprecationInfo?: string;
description?: string;
hooksParameters?: boolean;
hooksReturnValue?: boolean;
isDeprecated?: boolean;
isOptional?: boolean;
isRequired?: boolean;
isProPlan?: boolean;
isPremiumPlan?: boolean;
propDefault?: string;
propName: string;
requiresRef?: boolean;
seeMoreDescription?: string;
signature?: string;
signatureArgs?: { argName: string; argDescription?: string }[];
signatureReturnDescription?: string;
typeName: string;
}

interface PropertiesListProps {
properties: Properties[];
properties: PropertyDefinition[];
displayOption: 'collapsed' | 'expanded';
}

export default function PropertiesList(props: PropertiesListProps) {
const { properties, displayOption } = props;
const t = useTranslate();
return (
<ApiItemContaier>
<ApiItemContainer>
{properties.map((params) => {
const {
componentName,
propName,
seeMoreDescription,
description,
Expand All @@ -175,15 +132,14 @@ export default function PropertiesList(props: PropertiesListProps) {
isDeprecated,
isProPlan,
isPremiumPlan,
hooksParameters,
hooksReturnValue,
deprecationInfo,
typeName,
propDefault,
additionalInfo,
signature,
signatureArgs,
signatureReturnDescription,
hash,
} = params;

let note =
Expand All @@ -196,7 +152,7 @@ export default function PropertiesList(props: PropertiesListProps) {
return (
<StyledApiItem
key={propName}
id={getHash({ componentName, propName, hooksParameters, hooksReturnValue })}
id={hash}
title={
<React.Fragment>
{propName}
Expand Down Expand Up @@ -228,7 +184,7 @@ export default function PropertiesList(props: PropertiesListProps) {
/>
</ApiWarningAlert>
)}
{additionalInfo.map((key) => (
{additionalInfo?.map((key) => (
<p
className="prop-list-additional-description MuiApi-collapsible"
key={key}
Expand Down Expand Up @@ -310,6 +266,6 @@ export default function PropertiesList(props: PropertiesListProps) {
</StyledApiItem>
);
})}
</ApiItemContaier>
</ApiItemContainer>
);
}
27 changes: 7 additions & 20 deletions docs/src/modules/components/ApiPage/list/SlotsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {
} from '@mui/docs/branding';
import { useTranslate } from '@mui/docs/i18n';
import ExpandableApiItem, {
ApiItemContaier,
ApiItemContainer,
} from 'docs/src/modules/components/ApiPage/list/ExpandableApiItem';
import { SlotsDefinition } from 'docs/src/modules/components/ApiPage/common/slots';

const StyledApiItem = styled(ExpandableApiItem)(
({ theme }) => ({
Expand Down Expand Up @@ -43,22 +44,8 @@ const StyledApiItem = styled(ExpandableApiItem)(
}),
);

type HashParams = { componentName: string; className: string | null; name: string };

export function getHash({ componentName, className, name }: HashParams) {
return `${componentName}-css-${className ?? name}`;
}

export type SlotsFormattedParams = {
className: string | null;
componentName: string;
description?: string;
name: string;
defaultValue?: string;
};

interface SlotsListProps {
slots: SlotsFormattedParams[];
slots: SlotsDefinition[];
displayOption: 'collapsed' | 'expanded';
}

Expand All @@ -67,15 +54,15 @@ export default function SlotsList(props: SlotsListProps) {
const t = useTranslate();

return (
<ApiItemContaier className="MuiApi-slot-list">
<ApiItemContainer className="MuiApi-slot-list">
{slots.map((params) => {
const { description, className, name, defaultValue, componentName } = params;
const { description, className, name, defaultValue, hash } = params;

const isExtendable = description || defaultValue || className;

return (
<StyledApiItem
id={getHash({ componentName, className, name })}
id={hash}
key={name}
title={name}
note=""
Expand Down Expand Up @@ -108,6 +95,6 @@ export default function SlotsList(props: SlotsListProps) {
</StyledApiItem>
);
})}
</ApiItemContaier>
</ApiItemContainer>
);
}
35 changes: 35 additions & 0 deletions docs/src/modules/components/ApiPage/processors/classes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PropsTranslations, ComponentApiContent } from '@mui-internal/api-docs-builder';
import kebabCase from 'lodash/kebabCase';

export interface ClassDefinition {
className: string;
key: string;
hash: string;
description?: string;
isGlobal?: boolean;
isDeprecated?: boolean;
deprecationInfo?: string;
}

export interface ClassesApiProcessorParams {
componentClasses: ComponentApiContent['classes'];
classDescriptions: PropsTranslations['classDescriptions'];
componentName: string;
}

export function classesApiProcessor(params: ClassesApiProcessorParams): ClassDefinition[] {
Copy link
Member

@Janpot Janpot Aug 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker at all, just some reflections from reviewing this but I find the naming confusing. a "processor" is a noun, for a function I usually expect a verb. The meaning of the word "processor" is quite broad, it can mean anything.
For functions that transform one data type to another, I'd prefer to choose a name that includes both types. e.g. mapXtoY(x), xToY(x), yFromX(x). I like the latter as it mimics javascript API design (Object.fromEntries(), Array.from(), String.fromCharCode(), String.fromCodePoint()). I realize those aren't verbs neither, but the from/to clearly indicates a transformation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effectively I was not super happy with those namings

About mapXtoY naming, my struggle is that the X is not easy to name since it's a mix of descriptions and translations. So I went with a getter naming get[Yyy]ApiDefinitions

const { componentClasses, classDescriptions, componentName } = params;

return componentClasses.map((classDefinition) => {
return {
...classDefinition,
description:
classDescriptions[classDefinition.key]?.description
?.replace(/{{conditions}}/, classDescriptions[classDefinition.key].conditions!)
Copy link
Member

@Janpot Janpot Aug 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to get rid of these non-null assertion operators? It removes type safety and an upstream bug can be introduced that essentially turns this replacement into 'undefined' without a warning.
If it's hard to guarantee type-wise and have this as a compile time error, we could add an invariant() call to turn this into a build time error instead of quietly starting to replace strings with 'undefined' at run time.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with the following solution

    if (!conditions && description.search(/{{conditions}}/) !== -1) {
      throw Error(
        `In ${componentName} the class "${classDefinition.className}" description with "{{conditions}}" but without \`conditions\` to replace it.`,
      );
    }

It adds the runtime error, but does not fix the typing, because the fact that conditions is defined depends on the presence of {{conditions}} in the description, which might be hard to define with types

Copy link
Member

@Janpot Janpot Aug 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I would write this is to replace only within the check of whether the description contains {{conditions}}

// (use .includes())
if (!conditions && description.includes(/{{conditions}}/)) {
  throw new Error('...')
}

// This is suboptimal, a type checker can't relate the previous condition with the following replacement

return {
  // we're replacing, even when we know there is nothing to replace
  description: description.replace(/{{conditions}}/, conditions!),
}

The solution requires a refactor

// make description a mutable variable

if (description.includes(/{{conditions}}/)) {
  // Use an invariant to assert the condition, this would be a programming error
  // (don't use invariant when an error comes from user input)
  invariant(conditions, 'conditions should be defined if description includes {{conditions}}')
  // only replace when it makes sense
  descripton = description.replace(/{{conditions}}/, conditions)
}

return {
  description
}

It's cleaner and more defensive imo. non-null assertion is always a code smell. I have never seen a use-case that isn't better solved with a refactor or an invariant. But again, this is all polish. Doesn't have to be in scope for this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed explanation 👍

?.replace(/{{nodeName}}/, classDescriptions[classDefinition.key].nodeName!) ??
classDefinition.description,
deprecationInfo: classDescriptions[classDefinition.key]?.deprecationInfo,
hash: `${kebabCase(componentName)}-classes-${classDefinition.className}`,
};
});
}
Loading