chore: enforce loader format for component refs
Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
+2
-3
@@ -20,15 +20,14 @@ describe('ComponentImplementationBlueprint', () => {
|
||||
it('should allow defining a component override for sync component ref', () => {
|
||||
const componentRef = createComponentRef({
|
||||
id: 'test.component',
|
||||
mode: 'sync',
|
||||
defaultComponent: (props: { hello: string }) => <div>{props.hello}</div>,
|
||||
loader: () => (props: { hello: string }) => <div>{props.hello}</div>,
|
||||
});
|
||||
|
||||
const extension = ComponentImplementationBlueprint.make({
|
||||
params: define =>
|
||||
define({
|
||||
ref: componentRef,
|
||||
component: props => {
|
||||
loader: () => props => {
|
||||
// @ts-expect-error
|
||||
const t: number = props.hello;
|
||||
|
||||
|
||||
@@ -22,10 +22,9 @@ import {
|
||||
|
||||
export const componentDataRef = createExtensionDataRef<{
|
||||
ref: ComponentRef;
|
||||
component:
|
||||
| ((props: {}) => JSX.Element | null)
|
||||
loader:
|
||||
| (() => (props: {}) => JSX.Element | null)
|
||||
| (() => Promise<(props: {}) => JSX.Element | null>);
|
||||
type: 'sync' | 'async';
|
||||
}>().with({ id: 'core.component.component' });
|
||||
|
||||
export const ComponentImplementationBlueprint = createExtensionBlueprint({
|
||||
@@ -37,16 +36,10 @@ export const ComponentImplementationBlueprint = createExtensionBlueprint({
|
||||
},
|
||||
defineParams<Ref extends ComponentRef<any>>(params: {
|
||||
ref: Ref;
|
||||
component: Ref extends ComponentRef<
|
||||
infer IInnerComponentProps,
|
||||
any,
|
||||
infer IMode
|
||||
>
|
||||
? IMode extends 'sync'
|
||||
? (props: IInnerComponentProps) => JSX.Element
|
||||
: IMode extends 'async'
|
||||
? () => Promise<(props: IInnerComponentProps) => JSX.Element>
|
||||
: never
|
||||
loader: Ref extends ComponentRef<infer IInnerComponentProps, any>
|
||||
?
|
||||
| (() => (props: IInnerComponentProps) => JSX.Element | null)
|
||||
| (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>)
|
||||
: never;
|
||||
}) {
|
||||
return createExtensionBlueprintParams(params);
|
||||
@@ -54,8 +47,7 @@ export const ComponentImplementationBlueprint = createExtensionBlueprint({
|
||||
*factory(params) {
|
||||
yield componentDataRef({
|
||||
ref: params.ref,
|
||||
component: params.component,
|
||||
type: params.ref.mode,
|
||||
loader: params.loader,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import { createComponentRef } from './createComponentRef';
|
||||
|
||||
describe('createComponentRef', () => {
|
||||
it('can be created and read', () => {
|
||||
const ref = createComponentRef({ id: 'foo', mode: 'sync' });
|
||||
const ref = createComponentRef({ id: 'foo' });
|
||||
expect(ref.id).toBe('foo');
|
||||
expect(String(ref)).toBe('ComponentRef{id=foo}');
|
||||
});
|
||||
@@ -28,14 +28,15 @@ describe('createComponentRef', () => {
|
||||
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
mode: 'sync',
|
||||
defaultComponent: ({ foo }) => <Test key={foo} />,
|
||||
loader:
|
||||
() =>
|
||||
({ foo }) =>
|
||||
<Test key={foo} />,
|
||||
});
|
||||
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
mode: 'async',
|
||||
defaultComponent:
|
||||
loader:
|
||||
async () =>
|
||||
({ foo }) =>
|
||||
<Test key={foo} />,
|
||||
@@ -43,21 +44,6 @@ describe('createComponentRef', () => {
|
||||
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
mode: 'sync',
|
||||
// @ts-expect-error - this should be an error as mode is sync
|
||||
defaultComponent: async ({ foo }) => <Test key={foo} />,
|
||||
});
|
||||
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
mode: 'async',
|
||||
// @ts-expect-error - this should be an error as mode is async
|
||||
defaultComponent: ({ bar }) => <Test key={bar} />,
|
||||
});
|
||||
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
mode: 'sync',
|
||||
});
|
||||
|
||||
expect(Test).toBeDefined();
|
||||
@@ -66,23 +52,15 @@ describe('createComponentRef', () => {
|
||||
it('should allow transformings props', () => {
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
mode: 'sync',
|
||||
transformProps: props => ({ foo: props.bar }),
|
||||
});
|
||||
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
mode: 'sync',
|
||||
// @ts-expect-error - this should be an error as foo is not a string
|
||||
transformProps: props => ({ foo: 1 }),
|
||||
});
|
||||
|
||||
createComponentRef<{ foo: string }, { bar: string }>({
|
||||
id: 'foo',
|
||||
mode: 'sync',
|
||||
transformProps: props => ({ foo: props.bar }),
|
||||
});
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,12 +20,10 @@ import { OpaqueComponentRef } from '@internal/frontend';
|
||||
export type ComponentRef<
|
||||
TInnerComponentProps extends {} = {},
|
||||
TExternalComponentProps extends {} = TInnerComponentProps,
|
||||
TMode extends 'sync' | 'async' = 'sync' | 'async',
|
||||
> = {
|
||||
id: string;
|
||||
TProps: TInnerComponentProps;
|
||||
TExternalProps: TExternalComponentProps;
|
||||
TMode: TMode;
|
||||
$$type: '@backstage/ComponentRef';
|
||||
};
|
||||
|
||||
@@ -34,66 +32,27 @@ export type ComponentRefOptions<
|
||||
TExternalComponentProps extends {} = TInnerComponentProps,
|
||||
> = {
|
||||
id: string;
|
||||
componentAsync: TMode extends 'async'
|
||||
? () => Promise<(props: TInnerComponentProps) => JSX.Element | null>
|
||||
: TMode extends 'sync'
|
||||
? (props: TInnerComponentProps) => JSX.Element | null
|
||||
: never;
|
||||
loader?:
|
||||
| (() => (props: TInnerComponentProps) => JSX.Element | null)
|
||||
| (() => Promise<(props: TInnerComponentProps) => JSX.Element | null>);
|
||||
transformProps?: (props: TExternalComponentProps) => TInnerComponentProps;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new component ref that is synchronous.
|
||||
* @public
|
||||
*/
|
||||
export function createComponentRef<
|
||||
TInnerComponentProps extends {},
|
||||
TExternalComponentProps extends {} = TInnerComponentProps,
|
||||
>(
|
||||
options: ComponentRefOptions<
|
||||
TInnerComponentProps,
|
||||
TExternalComponentProps,
|
||||
'sync'
|
||||
>,
|
||||
): ComponentRef<TInnerComponentProps, TExternalComponentProps, 'sync'>;
|
||||
|
||||
/**
|
||||
* Creates a new component ref that is asynchronous.
|
||||
* @public
|
||||
*/
|
||||
export function createComponentRef<
|
||||
TInnerComponentProps extends {},
|
||||
TExternalComponentProps extends {} = TInnerComponentProps,
|
||||
>(
|
||||
options: ComponentRefOptions<
|
||||
TInnerComponentProps,
|
||||
TExternalComponentProps,
|
||||
'async'
|
||||
>,
|
||||
): ComponentRef<TInnerComponentProps, TExternalComponentProps, 'async'>;
|
||||
|
||||
export function createComponentRef<
|
||||
TInnerComponentProps extends {},
|
||||
TExternalComponentProps extends {} = TInnerComponentProps,
|
||||
TMode extends 'sync' | 'async' = 'sync' | 'async',
|
||||
>(
|
||||
options: ComponentRefOptions<
|
||||
TInnerComponentProps,
|
||||
TExternalComponentProps,
|
||||
TMode
|
||||
>,
|
||||
options: ComponentRefOptions<TInnerComponentProps, TExternalComponentProps>,
|
||||
): ComponentRef<TInnerComponentProps, TExternalComponentProps> {
|
||||
return OpaqueComponentRef.createInstance('v1', {
|
||||
id: options.id,
|
||||
TProps: null as unknown as TInnerComponentProps,
|
||||
TExternalProps: null as unknown as TExternalComponentProps,
|
||||
TMode: null as unknown as TMode,
|
||||
toString() {
|
||||
return `ComponentRef{id=${options.id}}`;
|
||||
},
|
||||
options: {
|
||||
mode: options.mode,
|
||||
defaultComponent: options.defaultComponent,
|
||||
loader: options.loader,
|
||||
transformProps: options.transformProps,
|
||||
} as (typeof OpaqueComponentRef.TInternal)['options'],
|
||||
});
|
||||
|
||||
@@ -22,15 +22,16 @@ describe('makeComponentFromRef', () => {
|
||||
it('should create a component from a ref for sync component', () => {
|
||||
const ref = createComponentRef({
|
||||
id: 'random',
|
||||
mode: 'sync',
|
||||
defaultComponent: (props: { name: string }) => {
|
||||
loader: () => (props: { name: string }) => {
|
||||
return <div data-testid="test">{props.name}</div>;
|
||||
},
|
||||
transformProps: (props: { id: string }) => ({
|
||||
name: props.id,
|
||||
}),
|
||||
});
|
||||
|
||||
const Component = makeComponentFromRef({ ref });
|
||||
|
||||
render(<Component name="test" />);
|
||||
render(<Component id="test" />);
|
||||
|
||||
expect(screen.getByTestId('test')).toHaveTextContent('test');
|
||||
});
|
||||
@@ -38,7 +39,6 @@ describe('makeComponentFromRef', () => {
|
||||
it('should render a fallback when theres no default implementation provided', () => {
|
||||
const ref = createComponentRef({
|
||||
id: 'random',
|
||||
mode: 'sync',
|
||||
});
|
||||
|
||||
const Component = makeComponentFromRef({ ref });
|
||||
@@ -51,11 +51,10 @@ describe('makeComponentFromRef', () => {
|
||||
it('should map props from external to internal', () => {
|
||||
const ref = createComponentRef({
|
||||
id: 'random',
|
||||
mode: 'sync',
|
||||
transformProps: (props: { name: string }) => ({
|
||||
uppercase: props.name.toUpperCase(),
|
||||
}),
|
||||
defaultComponent: props => {
|
||||
loader: () => props => {
|
||||
// @ts-expect-error as uppercase is types as a string
|
||||
const test: number = props.uppercase;
|
||||
|
||||
@@ -75,8 +74,7 @@ describe('makeComponentFromRef', () => {
|
||||
it('should create a component from a ref for async component', async () => {
|
||||
const ref = createComponentRef({
|
||||
id: 'random',
|
||||
mode: 'async',
|
||||
defaultComponent: async () => (props: { name: string }) => {
|
||||
loader: async () => (props: { name: string }) => {
|
||||
return <div data-testid="test">{props.name}</div>;
|
||||
},
|
||||
});
|
||||
@@ -91,7 +89,6 @@ describe('makeComponentFromRef', () => {
|
||||
it('should render a fallback when theres no default implementation provided', async () => {
|
||||
const ref = createComponentRef({
|
||||
id: 'random',
|
||||
mode: 'async',
|
||||
});
|
||||
|
||||
const Component = makeComponentFromRef({ ref });
|
||||
@@ -104,11 +101,10 @@ describe('makeComponentFromRef', () => {
|
||||
it('should map props from external to internal', async () => {
|
||||
const ref = createComponentRef({
|
||||
id: 'random',
|
||||
mode: 'async',
|
||||
transformProps: (props: { name: string }) => ({
|
||||
uppercase: props.name.toUpperCase(),
|
||||
}),
|
||||
defaultComponent: async () => props => {
|
||||
loader: async () => props => {
|
||||
// @ts-expect-error as uppercase is types as a string
|
||||
const test: number = props.uppercase;
|
||||
|
||||
|
||||
@@ -33,29 +33,24 @@ export function makeComponentFromRef<
|
||||
const ComponentRefImpl = (props: ExternalComponentProps) => {
|
||||
const innerProps = options.transformProps?.(props) ?? props;
|
||||
|
||||
if (options.mode === 'sync') {
|
||||
const DefaultImplementation =
|
||||
options.defaultComponent ?? FallbackComponent;
|
||||
const ComponentOrPromise = options.loader?.() ?? FallbackComponent;
|
||||
|
||||
return <DefaultImplementation {...innerProps} />;
|
||||
if ('then' in ComponentOrPromise) {
|
||||
const DefaultImplementation = lazy(() =>
|
||||
ComponentOrPromise.then(c => {
|
||||
return { default: c };
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
// todo: is this necessary? can we remove this?
|
||||
<Suspense>
|
||||
<DefaultImplementation {...innerProps} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const DefaultImplementation = lazy(
|
||||
() =>
|
||||
options.defaultComponent?.().then(c => {
|
||||
return { default: c };
|
||||
}) ??
|
||||
Promise.resolve({
|
||||
default: FallbackComponent,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
// todo: is this necessary? can we remove this?
|
||||
<Suspense>
|
||||
<DefaultImplementation {...innerProps} />
|
||||
</Suspense>
|
||||
);
|
||||
return <ComponentOrPromise {...innerProps} />;
|
||||
};
|
||||
|
||||
return ComponentRefImpl;
|
||||
|
||||
Reference in New Issue
Block a user