Merge pull request #25908 from backstage/mob/nfs/finish-bleuprints
NFS: Support merging of `inputs` in `Blueprints`
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
Support merging of `inputs` in extension blueprints, but stop merging `output`. In addition, the original factory in extension blueprints now returns a data container that both provides access to the returned data, but can also be forwarded as output.
|
||||
@@ -603,7 +603,6 @@ export function createExtensionBlueprint<
|
||||
}
|
||||
>;
|
||||
},
|
||||
UExtraOutput extends AnyExtensionDataRef,
|
||||
TConfigSchema extends {
|
||||
[key in string]: (zImpl: typeof z) => z.ZodType;
|
||||
},
|
||||
@@ -624,7 +623,6 @@ export function createExtensionBlueprint<
|
||||
TParams,
|
||||
UOutput,
|
||||
TInputs,
|
||||
UExtraOutput,
|
||||
string extends keyof TConfigSchema
|
||||
? {}
|
||||
: {
|
||||
@@ -1090,7 +1088,6 @@ export interface ExtensionBlueprint<
|
||||
}
|
||||
>;
|
||||
},
|
||||
UExtraOutput extends AnyExtensionDataRef,
|
||||
TConfig extends {
|
||||
[key in string]: unknown;
|
||||
},
|
||||
@@ -1108,6 +1105,16 @@ export interface ExtensionBlueprint<
|
||||
[key in string]: (zImpl: typeof z) => z.ZodType;
|
||||
},
|
||||
UFactoryOutput extends ExtensionDataValue<any, any>,
|
||||
UNewOutput extends AnyExtensionDataRef,
|
||||
TExtraInputs extends {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{
|
||||
optional: boolean;
|
||||
singleton: boolean;
|
||||
}
|
||||
>;
|
||||
},
|
||||
>(
|
||||
args: {
|
||||
namespace?: string;
|
||||
@@ -1117,8 +1124,11 @@ export interface ExtensionBlueprint<
|
||||
input: string;
|
||||
};
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
output?: Array<UExtraOutput>;
|
||||
inputs?: TExtraInputs & {
|
||||
[KName in keyof TInputs]?: `Error: Input '${KName &
|
||||
string}' is already defined in parent definition`;
|
||||
};
|
||||
output?: Array<UNewOutput>;
|
||||
config?: {
|
||||
schema: TExtensionConfigSchema & {
|
||||
[KName in keyof TConfig]?: `Error: Config key '${KName &
|
||||
@@ -1134,7 +1144,7 @@ export interface ExtensionBlueprint<
|
||||
config?: TConfig;
|
||||
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
},
|
||||
) => Iterable<ExtensionDataRefToValue<UOutput>>,
|
||||
) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
node: AppNode;
|
||||
config: TConfig & {
|
||||
@@ -1142,11 +1152,11 @@ export interface ExtensionBlueprint<
|
||||
ReturnType<TExtensionConfigSchema[key]>
|
||||
>;
|
||||
};
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs & TExtraInputs>>;
|
||||
},
|
||||
): Iterable<UFactoryOutput>;
|
||||
} & VerifyExtensionFactoryOutput<
|
||||
UOutput & UExtraOutput,
|
||||
AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput,
|
||||
UFactoryOutput
|
||||
>)
|
||||
| {
|
||||
@@ -1186,7 +1196,17 @@ export interface ExtensionBoundaryProps {
|
||||
|
||||
// @public (undocumented)
|
||||
export type ExtensionDataContainer<UExtensionData extends AnyExtensionDataRef> =
|
||||
{
|
||||
Iterable<
|
||||
UExtensionData extends ExtensionDataRef<
|
||||
infer IData,
|
||||
infer IId,
|
||||
infer IConfig
|
||||
>
|
||||
? IConfig['optional'] extends true
|
||||
? never
|
||||
: ExtensionDataValue<IData, IId>
|
||||
: never
|
||||
> & {
|
||||
get<TId extends UExtensionData['id']>(
|
||||
ref: ExtensionDataRef<any, TId, any>,
|
||||
): UExtensionData extends ExtensionDataRef<infer IData, TId, infer IConfig>
|
||||
@@ -1356,7 +1376,6 @@ export const IconBundleBlueprint: ExtensionBlueprint<
|
||||
}
|
||||
>;
|
||||
},
|
||||
AnyExtensionDataRef,
|
||||
{
|
||||
icons: string;
|
||||
test: string;
|
||||
|
||||
@@ -65,7 +65,17 @@ export type ExtensionDataValues<TExtensionData extends AnyExtensionDataMap> = {
|
||||
|
||||
/** @public */
|
||||
export type ExtensionDataContainer<UExtensionData extends AnyExtensionDataRef> =
|
||||
{
|
||||
Iterable<
|
||||
UExtensionData extends ExtensionDataRef<
|
||||
infer IData,
|
||||
infer IId,
|
||||
infer IConfig
|
||||
>
|
||||
? IConfig['optional'] extends true
|
||||
? never
|
||||
: ExtensionDataValue<IData, IId>
|
||||
: never
|
||||
> & {
|
||||
get<TId extends UExtensionData['id']>(
|
||||
ref: ExtensionDataRef<any, TId, any>,
|
||||
): UExtensionData extends ExtensionDataRef<infer IData, TId, infer IConfig>
|
||||
|
||||
@@ -21,9 +21,21 @@ import { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
import { createExtensionDataRef } from './createExtensionDataRef';
|
||||
import { createExtensionInput } from './createExtensionInput';
|
||||
import { RouteRef } from '../routing';
|
||||
import {
|
||||
ExtensionDefinition,
|
||||
toInternalExtensionDefinition,
|
||||
} from './createExtension';
|
||||
|
||||
function unused(..._any: any[]) {}
|
||||
|
||||
function factoryOutput(ext: ExtensionDefinition<any, any>) {
|
||||
const int = toInternalExtensionDefinition(ext);
|
||||
if (int.version !== 'v2') {
|
||||
throw new Error('Expected v2 extension');
|
||||
}
|
||||
return Array.from(int.factory({} as any));
|
||||
}
|
||||
|
||||
describe('createExtensionBlueprint', () => {
|
||||
it('should allow creation of extension blueprints', () => {
|
||||
const TestExtensionBlueprint = createExtensionBlueprint({
|
||||
@@ -384,4 +396,203 @@ describe('createExtensionBlueprint', () => {
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow merging of inputs', () => {
|
||||
const blueprint = createExtensionBlueprint({
|
||||
kind: 'test-extension',
|
||||
attachTo: { id: 'test', input: 'default' },
|
||||
inputs: {
|
||||
test: createExtensionInput([coreExtensionData.routeRef], {
|
||||
singleton: true,
|
||||
}),
|
||||
},
|
||||
output: [coreExtensionData.reactElement.optional()],
|
||||
factory(_params: { x?: string }, { inputs }) {
|
||||
const ref: RouteRef = inputs.test.get(coreExtensionData.routeRef);
|
||||
|
||||
unused(ref);
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
blueprint.make({
|
||||
inputs: {
|
||||
test2: createExtensionInput([coreExtensionData.reactElement], {
|
||||
singleton: true,
|
||||
}),
|
||||
},
|
||||
factory(origFactory, { inputs }) {
|
||||
const ref: RouteRef = inputs.test.get(coreExtensionData.routeRef);
|
||||
|
||||
const el: JSX.Element = inputs.test2.get(
|
||||
coreExtensionData.reactElement,
|
||||
);
|
||||
|
||||
unused(ref, el);
|
||||
|
||||
return origFactory({});
|
||||
},
|
||||
});
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow overriding inputs', () => {
|
||||
const blueprint = createExtensionBlueprint({
|
||||
kind: 'test-extension',
|
||||
attachTo: { id: 'test', input: 'default' },
|
||||
inputs: {
|
||||
test: createExtensionInput([coreExtensionData.routeRef]),
|
||||
},
|
||||
output: [coreExtensionData.reactElement.optional()],
|
||||
factory() {
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
blueprint.make({
|
||||
inputs: {
|
||||
// @ts-expect-error
|
||||
test: createExtensionInput([]), // Overrides are not allowed
|
||||
},
|
||||
factory(origFactory) {
|
||||
return origFactory({});
|
||||
},
|
||||
});
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('should replace the outputs when provided through make', () => {
|
||||
const testDataRef1 = createExtensionDataRef<string>().with({ id: 'test1' });
|
||||
const testDataRef2 = createExtensionDataRef<string>().with({ id: 'test2' });
|
||||
|
||||
const blueprint = createExtensionBlueprint({
|
||||
kind: 'test-extension',
|
||||
attachTo: { id: 'test', input: 'default' },
|
||||
output: [testDataRef1],
|
||||
factory() {
|
||||
return [testDataRef1('foo')];
|
||||
},
|
||||
});
|
||||
|
||||
const ext = toInternalExtensionDefinition(
|
||||
blueprint.make({
|
||||
output: [testDataRef2],
|
||||
factory(origFactory) {
|
||||
const parent = origFactory({});
|
||||
return [testDataRef2(`${parent.get(testDataRef1)}bar`)];
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(ext.output).toEqual([testDataRef2]);
|
||||
|
||||
expect(factoryOutput(ext)).toEqual([testDataRef2('foobar')]);
|
||||
});
|
||||
|
||||
it('should allow returning of the parent data container', () => {
|
||||
const testDataRef1 = createExtensionDataRef<string>().with({ id: 'test1' });
|
||||
const testDataRef2 = createExtensionDataRef<string>().with({ id: 'test2' });
|
||||
|
||||
const blueprint = createExtensionBlueprint({
|
||||
kind: 'test-extension',
|
||||
attachTo: { id: 'test', input: 'default' },
|
||||
output: [testDataRef1],
|
||||
factory() {
|
||||
return [testDataRef1('foo')];
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
factoryOutput(
|
||||
blueprint.make({
|
||||
output: [testDataRef1, testDataRef2],
|
||||
*factory(origFactory) {
|
||||
yield* origFactory({});
|
||||
yield testDataRef2('bar');
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toEqual([testDataRef1('foo'), testDataRef2('bar')]);
|
||||
|
||||
expect(
|
||||
factoryOutput(
|
||||
blueprint.make({
|
||||
output: [testDataRef1, testDataRef2],
|
||||
factory(origFactory) {
|
||||
return [...origFactory({}), testDataRef2('bar')];
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toEqual([testDataRef1('foo'), testDataRef2('bar')]);
|
||||
});
|
||||
|
||||
it('should not allow returning parent output if outputs are overridden', () => {
|
||||
const testDataRef1 = createExtensionDataRef<string>().with({ id: 'test1' });
|
||||
const testDataRef2 = createExtensionDataRef<string>().with({ id: 'test2' });
|
||||
|
||||
const blueprint = createExtensionBlueprint({
|
||||
kind: 'test-extension',
|
||||
attachTo: { id: 'test', input: 'default' },
|
||||
output: [testDataRef1.optional()],
|
||||
factory() {
|
||||
return [testDataRef1('foo')];
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
blueprint.make({
|
||||
output: [testDataRef2.optional()],
|
||||
*factory() {
|
||||
yield testDataRef1('foo');
|
||||
yield testDataRef2('bar');
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
factoryOutput(
|
||||
blueprint.make({
|
||||
output: [testDataRef2.optional()],
|
||||
*factory() {
|
||||
yield testDataRef2('bar');
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toEqual([testDataRef2('bar')]);
|
||||
});
|
||||
|
||||
it('should not rely on optional outputs when forwarding from parent', () => {
|
||||
const testDataRef1 = createExtensionDataRef<string>().with({ id: 'test1' });
|
||||
const testDataRef2 = createExtensionDataRef<string>().with({ id: 'test2' });
|
||||
|
||||
const blueprint = createExtensionBlueprint({
|
||||
kind: 'test-extension',
|
||||
attachTo: { id: 'test', input: 'default' },
|
||||
output: [testDataRef1, testDataRef2.optional()],
|
||||
factory() {
|
||||
return [testDataRef1('foo')];
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
blueprint.make({
|
||||
output: [testDataRef1, testDataRef2],
|
||||
*factory(origFactory) {
|
||||
yield* origFactory({});
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
factoryOutput(
|
||||
blueprint.make({
|
||||
output: [testDataRef1, testDataRef2],
|
||||
*factory(origFactory) {
|
||||
yield* origFactory({});
|
||||
yield testDataRef2('bar');
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toEqual([testDataRef1('foo'), testDataRef2('bar')]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AppNode } from '../apis';
|
||||
import { Expand } from '../types';
|
||||
import {
|
||||
CreateExtensionOptions,
|
||||
ExtensionDataContainer,
|
||||
ExtensionDefinition,
|
||||
ResolvedExtensionInputs,
|
||||
VerifyExtensionFactoryOutput,
|
||||
@@ -27,7 +28,7 @@ import { z } from 'zod';
|
||||
import { ExtensionInput } from './createExtensionInput';
|
||||
import {
|
||||
AnyExtensionDataRef,
|
||||
ExtensionDataRefToValue,
|
||||
ExtensionDataRef,
|
||||
ExtensionDataValue,
|
||||
} from './createExtensionDataRef';
|
||||
|
||||
@@ -83,7 +84,6 @@ export interface ExtensionBlueprint<
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
},
|
||||
UExtraOutput extends AnyExtensionDataRef,
|
||||
TConfig extends { [key in string]: unknown },
|
||||
TConfigInput extends { [key in string]: unknown },
|
||||
TDataRefs extends { [name in string]: AnyExtensionDataRef },
|
||||
@@ -101,14 +101,24 @@ export interface ExtensionBlueprint<
|
||||
[key in string]: (zImpl: typeof z) => z.ZodType;
|
||||
},
|
||||
UFactoryOutput extends ExtensionDataValue<any, any>,
|
||||
UNewOutput extends AnyExtensionDataRef,
|
||||
TExtraInputs extends {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
},
|
||||
>(
|
||||
args: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
output?: Array<UExtraOutput>;
|
||||
inputs?: TExtraInputs & {
|
||||
[KName in keyof TInputs]?: `Error: Input '${KName &
|
||||
string}' is already defined in parent definition`;
|
||||
};
|
||||
output?: Array<UNewOutput>;
|
||||
config?: {
|
||||
schema: TExtensionConfigSchema & {
|
||||
[KName in keyof TConfig]?: `Error: Config key '${KName &
|
||||
@@ -124,7 +134,7 @@ export interface ExtensionBlueprint<
|
||||
config?: TConfig;
|
||||
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
},
|
||||
) => Iterable<ExtensionDataRefToValue<UOutput>>,
|
||||
) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
node: AppNode;
|
||||
config: TConfig & {
|
||||
@@ -132,11 +142,11 @@ export interface ExtensionBlueprint<
|
||||
ReturnType<TExtensionConfigSchema[key]>
|
||||
>;
|
||||
};
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs & TExtraInputs>>;
|
||||
},
|
||||
): Iterable<UFactoryOutput>;
|
||||
} & VerifyExtensionFactoryOutput<
|
||||
UOutput & UExtraOutput,
|
||||
AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput,
|
||||
UFactoryOutput
|
||||
>)
|
||||
| {
|
||||
@@ -160,6 +170,30 @@ export interface ExtensionBlueprint<
|
||||
>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
function createDataContainer<UData extends AnyExtensionDataRef>(
|
||||
values: Iterable<
|
||||
UData extends ExtensionDataRef<infer IData, infer IId>
|
||||
? ExtensionDataValue<IData, IId>
|
||||
: never
|
||||
>,
|
||||
): ExtensionDataContainer<UData> {
|
||||
const container = new Map<string, ExtensionDataValue<any, any>>();
|
||||
|
||||
for (const output of values) {
|
||||
container.set(output.id, output);
|
||||
}
|
||||
|
||||
return {
|
||||
get(ref) {
|
||||
return container.get(ref.id)?.value;
|
||||
},
|
||||
[Symbol.iterator]() {
|
||||
return container.values();
|
||||
},
|
||||
} as ExtensionDataContainer<UData>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -172,7 +206,6 @@ class ExtensionBlueprintImpl<
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
},
|
||||
UExtraOutput extends AnyExtensionDataRef,
|
||||
TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType },
|
||||
TDataRefs extends { [name in string]: AnyExtensionDataRef },
|
||||
> {
|
||||
@@ -196,13 +229,23 @@ class ExtensionBlueprintImpl<
|
||||
[key in string]: (zImpl: typeof z) => z.ZodType;
|
||||
},
|
||||
UFactoryOutput extends ExtensionDataValue<any, any>,
|
||||
UNewOutput extends AnyExtensionDataRef,
|
||||
TExtraInputs extends {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
},
|
||||
>(args: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
output?: Array<UExtraOutput>;
|
||||
inputs?: TExtraInputs & {
|
||||
[KName in keyof TInputs]?: `Error: Input '${KName &
|
||||
string}' is already defined in parent definition`;
|
||||
};
|
||||
output?: Array<UNewOutput>;
|
||||
params?: TParams;
|
||||
config?: {
|
||||
schema: TExtensionConfigSchema;
|
||||
@@ -218,7 +261,7 @@ class ExtensionBlueprintImpl<
|
||||
};
|
||||
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
},
|
||||
) => Iterable<ExtensionDataRefToValue<UOutput>>,
|
||||
) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
node: AppNode;
|
||||
config: {
|
||||
@@ -228,7 +271,7 @@ class ExtensionBlueprintImpl<
|
||||
} & {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
};
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs & TExtraInputs>>;
|
||||
},
|
||||
): Iterable<UFactoryOutput>;
|
||||
}): ExtensionDefinition<
|
||||
@@ -277,8 +320,8 @@ class ExtensionBlueprintImpl<
|
||||
name: args.name ?? name,
|
||||
attachTo: args.attachTo ?? this.options.attachTo,
|
||||
disabled: args.disabled ?? this.options.disabled,
|
||||
inputs: args.inputs ?? this.options.inputs,
|
||||
output: [...(args.output ?? []), ...this.options.output],
|
||||
inputs: { ...args.inputs, ...this.options.inputs },
|
||||
output: args.output ?? this.options.output,
|
||||
config: Object.keys(schema).length === 0 ? undefined : { schema },
|
||||
factory: ({ node, config, inputs }) => {
|
||||
if (args.factory) {
|
||||
@@ -293,12 +336,14 @@ class ExtensionBlueprintImpl<
|
||||
};
|
||||
inputs?: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
},
|
||||
): Iterable<ExtensionDataRefToValue<UOutput>> => {
|
||||
return this.options.factory(innerParams, {
|
||||
node,
|
||||
config: innerContext?.config ?? config,
|
||||
inputs: innerContext?.inputs ?? inputs,
|
||||
});
|
||||
): ExtensionDataContainer<UOutput> => {
|
||||
return createDataContainer<UOutput>(
|
||||
this.options.factory(innerParams, {
|
||||
node,
|
||||
config: innerContext?.config ?? config,
|
||||
inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden
|
||||
}),
|
||||
);
|
||||
},
|
||||
{
|
||||
node,
|
||||
@@ -310,14 +355,17 @@ class ExtensionBlueprintImpl<
|
||||
return this.options.factory(args.params, {
|
||||
node,
|
||||
config,
|
||||
inputs,
|
||||
// TODO: Figure out types once legacy data map input type is gone
|
||||
inputs: inputs as unknown as Expand<
|
||||
ResolvedExtensionInputs<TInputs>
|
||||
>,
|
||||
});
|
||||
}
|
||||
throw new Error('Either params or factory must be provided');
|
||||
},
|
||||
} as CreateExtensionOptions<
|
||||
UOutput,
|
||||
TInputs,
|
||||
TInputs & TExtraInputs,
|
||||
{
|
||||
[key in keyof TExtensionConfigSchema]: z.infer<
|
||||
ReturnType<TExtensionConfigSchema[key]>
|
||||
@@ -357,7 +405,6 @@ export function createExtensionBlueprint<
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
},
|
||||
UExtraOutput extends AnyExtensionDataRef,
|
||||
TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType },
|
||||
UFactoryOutput extends ExtensionDataValue<any, any>,
|
||||
TDataRefs extends { [name in string]: AnyExtensionDataRef } = never,
|
||||
@@ -374,7 +421,6 @@ export function createExtensionBlueprint<
|
||||
TParams,
|
||||
UOutput,
|
||||
TInputs,
|
||||
UExtraOutput,
|
||||
string extends keyof TConfigSchema
|
||||
? {}
|
||||
: { [key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>> },
|
||||
@@ -391,7 +437,6 @@ export function createExtensionBlueprint<
|
||||
TParams,
|
||||
UOutput,
|
||||
TInputs,
|
||||
UExtraOutput,
|
||||
string extends keyof TConfigSchema
|
||||
? {}
|
||||
: {
|
||||
|
||||
Reference in New Issue
Block a user