-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
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
Changes from 6 commits
7a04d93
4a74e29
1ae9c7b
de93837
d647677
f29c6c7
a4ecdad
2ce4e3c
5c3031d
ba5e578
90aa5bf
a66049e
712dd52
61d3c93
9d7d779
ed7b774
fdc9139
8b87a0b
9853d68
2138bda
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -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' | ||
|
@@ -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'), | ||
].filter(Boolean); | ||
|
||
// The `ref` is forwarded to the root element. | ||
|
@@ -272,9 +260,11 @@ export default function ApiPage(props: ApiPageProps) { | |
</React.Fragment> | ||
) : null} | ||
<PropertiesSection | ||
properties={componentProps} | ||
propertiesDescriptions={propDescriptions} | ||
componentName={pageContent.name} | ||
properties={propsApiProcessor({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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} | ||
|
@@ -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} | ||
|
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, | ||
|
@@ -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 } = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generating the |
||
classDefinition; | ||
|
||
let note = isGlobal ? t('api-docs.state') : ''; | ||
|
@@ -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}`} | ||
|
@@ -114,6 +109,6 @@ export default function ClassesList(props: ClassesListProps) { | |
</StyledApiItem> | ||
); | ||
})} | ||
</ApiItemContaier> | ||
</ApiItemContainer> | ||
); | ||
} |
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[] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Effectively I was not super happy with those namings About |
||
const { componentClasses, classDescriptions, componentName } = params; | ||
|
||
return componentClasses.map((classDefinition) => { | ||
return { | ||
...classDefinition, | ||
description: | ||
classDescriptions[classDefinition.key]?.description | ||
?.replace(/{{conditions}}/, classDescriptions[classDefinition.key].conditions!) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 // (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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}`, | ||
}; | ||
}); | ||
} |
There was a problem hiding this comment.
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