Skip to content

POC: AI Photo Edit #55

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

Merged
merged 9 commits into from
Jun 5, 2025
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add order provider configuration and test for getProperties
maerch committed Jun 3, 2025
commit 01dcbb3a219d49e81a7e7f4b92ec322ef16fe5ed
466 changes: 466 additions & 0 deletions packages/plugin-ai-generation-web/src/__tests__/getProperties.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,466 @@
import { describe, expect, it } from '@jest/globals';
import { OpenAPIV3 } from 'openapi-types';
import getProperties from '../generation/openapi/getProperties';
import { PanelInputSchema } from '../generation/provider';

describe('getProperties', () => {
const mockInputSchema: OpenAPIV3.SchemaObject = {
type: 'object',
properties: {
prompt: {
type: 'string',
description: 'Text prompt'
},
width: {
type: 'integer',
description: 'Image width'
},
height: {
type: 'integer',
description: 'Image height'
}
}
};

const mockPanelInput: PanelInputSchema<'image', any> = {
type: 'schema',
document: {} as OpenAPIV3.Document,
inputReference: '#/components/schemas/Input',
getBlockInput: async () => ({ image: { width: 1024, height: 1024 } })
};

it('should return properties in default order when no custom order is specified', () => {
const result = getProperties(mockInputSchema, mockPanelInput);

expect(result).toHaveLength(3);
expect(result[0]).toEqual({
id: 'prompt',
schema: mockInputSchema.properties!.prompt
});
expect(result[1]).toEqual({
id: 'width',
schema: mockInputSchema.properties!.width
});
expect(result[2]).toEqual({
id: 'height',
schema: mockInputSchema.properties!.height
});
});

it('should return properties in custom order when order array is specified', () => {
const panelInputWithOrder = {
...mockPanelInput,
order: ['height', 'prompt', 'width']
};

const result = getProperties(mockInputSchema, panelInputWithOrder);

expect(result).toHaveLength(3);
expect(result[0]).toEqual({
id: 'height',
schema: mockInputSchema.properties!.height
});
expect(result[1]).toEqual({
id: 'prompt',
schema: mockInputSchema.properties!.prompt
});
expect(result[2]).toEqual({
id: 'width',
schema: mockInputSchema.properties!.width
});
});

it('should return properties in order returned by order function', () => {
const panelInputWithOrderFn = {
...mockPanelInput,
order: (order: string[]) => order.reverse()
};

const result = getProperties(mockInputSchema, panelInputWithOrderFn);

expect(result).toHaveLength(3);
expect(result[0]).toEqual({
id: 'height',
schema: mockInputSchema.properties!.height
});
expect(result[1]).toEqual({
id: 'width',
schema: mockInputSchema.properties!.width
});
expect(result[2]).toEqual({
id: 'prompt',
schema: mockInputSchema.properties!.prompt
});
});

it('should use extension keyword order when specified', () => {
const schemaWithExtension = {
...mockInputSchema,
'x-order': ['width', 'height', 'prompt']
};

const panelInputWithExtensionKeyword = {
...mockPanelInput,
orderExtensionKeyword: 'x-order'
};

const result = getProperties(
schemaWithExtension,
panelInputWithExtensionKeyword
);

expect(result).toHaveLength(3);
expect(result[0]).toEqual({
id: 'width',
schema: mockInputSchema.properties!.width
});
expect(result[1]).toEqual({
id: 'height',
schema: mockInputSchema.properties!.height
});
expect(result[2]).toEqual({
id: 'prompt',
schema: mockInputSchema.properties!.prompt
});
});

it('should prioritize order array over extension keyword', () => {
const schemaWithExtension = {
...mockInputSchema,
'x-order': ['width', 'height', 'prompt']
};

const panelInputWithBoth = {
...mockPanelInput,
orderExtensionKeyword: 'x-order',
order: ['prompt', 'width', 'height']
};

const result = getProperties(schemaWithExtension, panelInputWithBoth);

expect(result).toHaveLength(3);
expect(result[0]).toEqual({
id: 'prompt',
schema: mockInputSchema.properties!.prompt
});
expect(result[1]).toEqual({
id: 'width',
schema: mockInputSchema.properties!.width
});
expect(result[2]).toEqual({
id: 'height',
schema: mockInputSchema.properties!.height
});
});

it('should handle properties with undefined schema', () => {
const schemaWithLimitedProperties = {
...mockInputSchema,
properties: {
prompt: mockInputSchema.properties!.prompt
// width and height are missing from properties but might be in order
}
};

const panelInputWithMissingProperty = {
...mockPanelInput,
order: ['width', 'prompt']
};

const result = getProperties(
schemaWithLimitedProperties,
panelInputWithMissingProperty
);

expect(result).toHaveLength(2);
expect(result[0]).toEqual({ id: 'width', schema: undefined });
expect(result[1]).toEqual({
id: 'prompt',
schema: mockInputSchema.properties!.prompt
});
});

it('should throw error when input schema has no properties', () => {
const schemaWithoutProperties: OpenAPIV3.SchemaObject = {
type: 'object'
};

expect(() => {
getProperties(schemaWithoutProperties, mockPanelInput);
}).toThrow('Input schema must have properties');
});

it('should remove duplicates from order when using function', () => {
const panelInputWithDuplicates = {
...mockPanelInput,
order: () => ['prompt', 'width', 'prompt', 'height', 'width']
};

const result = getProperties(mockInputSchema, panelInputWithDuplicates);

expect(result).toHaveLength(3);
expect(result[0]).toEqual({
id: 'prompt',
schema: mockInputSchema.properties!.prompt
});
expect(result[1]).toEqual({
id: 'width',
schema: mockInputSchema.properties!.width
});
expect(result[2]).toEqual({
id: 'height',
schema: mockInputSchema.properties!.height
});
});

it('should handle multiple extension keywords', () => {
const schemaWithMultipleExtensions = {
...mockInputSchema,
'x-custom-order': ['height', 'prompt', 'width']
};

const panelInputWithMultipleKeywords = {
...mockPanelInput,
orderExtensionKeyword: ['x-order', 'x-custom-order']
};

const result = getProperties(
schemaWithMultipleExtensions,
panelInputWithMultipleKeywords
);

expect(result).toHaveLength(3);
expect(result[0]).toEqual({
id: 'height',
schema: mockInputSchema.properties!.height
});
expect(result[1]).toEqual({
id: 'prompt',
schema: mockInputSchema.properties!.prompt
});
expect(result[2]).toEqual({
id: 'width',
schema: mockInputSchema.properties!.width
});
});

it('should throw error for invalid orderExtensionKeyword type', () => {
const panelInputWithInvalidKeyword = {
...mockPanelInput,
orderExtensionKeyword: 123 as any
};

expect(() => {
getProperties(mockInputSchema, panelInputWithInvalidKeyword);
}).toThrow('orderExtensionKeyword must be a string or an array of strings');
});
});

describe('getOrder (internal function)', () => {
const mockInputSchema: OpenAPIV3.SchemaObject = {
type: 'object',
properties: {
prompt: { type: 'string' },
width: { type: 'integer' },
height: { type: 'integer' }
}
};

const mockPanelInput: PanelInputSchema<'image', any> = {
type: 'schema',
document: {} as OpenAPIV3.Document,
inputReference: '#/components/schemas/Input',
getBlockInput: async () => ({ image: { width: 1024, height: 1024 } })
};

it('should return default order from schema properties when no order specified', () => {
// Test default order by checking getProperties result ordering
const result = getProperties(mockInputSchema, mockPanelInput);
const propertyIds = result.map((p) => p.id);

expect(propertyIds).toEqual(['prompt', 'width', 'height']);
});

it('should return order from panelInput.order array when specified', () => {
const panelInputWithOrder = {
...mockPanelInput,
order: ['height', 'prompt', 'width']
};

const result = getProperties(mockInputSchema, panelInputWithOrder);
const propertyIds = result.map((p) => p.id);

expect(propertyIds).toEqual(['height', 'prompt', 'width']);
});

it('should apply order function to default order', () => {
const panelInputWithOrderFn = {
...mockPanelInput,
order: (order: string[]) => order.slice().reverse()
};

const result = getProperties(mockInputSchema, panelInputWithOrderFn);
const propertyIds = result.map((p) => p.id);

expect(propertyIds).toEqual(['height', 'width', 'prompt']);
});

it('should use extension keyword order when available', () => {
const schemaWithExtension = {
...mockInputSchema,
'x-order': ['width', 'height', 'prompt']
};

const panelInputWithExtension = {
...mockPanelInput,
orderExtensionKeyword: 'x-order'
};

const result = getProperties(schemaWithExtension, panelInputWithExtension);
const propertyIds = result.map((p) => p.id);

expect(propertyIds).toEqual(['width', 'height', 'prompt']);
});

it('should apply order function to extension keyword order', () => {
const schemaWithExtension = {
...mockInputSchema,
'x-order': ['width', 'height', 'prompt']
};

const panelInputWithBoth = {
...mockPanelInput,
orderExtensionKeyword: 'x-order',
order: (order: string[]) => order.filter((id) => id !== 'height')
};

const result = getProperties(schemaWithExtension, panelInputWithBoth);
const propertyIds = result.map((p) => p.id);

expect(propertyIds).toEqual(['width', 'prompt']);
});

it('should remove duplicates from final order', () => {
const panelInputWithOrderFn = {
...mockPanelInput,
order: (order: string[]) => [...order, ...order] // Create duplicates
};

const result = getProperties(mockInputSchema, panelInputWithOrderFn);
const propertyIds = result.map((p) => p.id);

expect(propertyIds).toEqual(['prompt', 'width', 'height']);
expect(propertyIds.length).toBe(3); // No duplicates
});

it('should throw error when schema has no properties', () => {
const schemaWithoutProperties: OpenAPIV3.SchemaObject = {
type: 'object'
};

expect(() => {
getProperties(schemaWithoutProperties, mockPanelInput);
}).toThrow('Input schema must have properties');
});
});

describe('getOrderFromExtensionKeyword (internal function)', () => {
const mockInputSchema: OpenAPIV3.SchemaObject = {
type: 'object',
properties: {
prompt: { type: 'string' },
width: { type: 'integer' }
}
};

const mockPanelInput: PanelInputSchema<'image', any> = {
type: 'schema',
document: {} as OpenAPIV3.Document,
inputReference: '#/components/schemas/Input',
getBlockInput: async () => ({ image: { width: 1024, height: 1024 } })
};

it('should return undefined when orderExtensionKeyword is not specified', () => {
const result = getProperties(mockInputSchema, mockPanelInput);
const propertyIds = result.map((p) => p.id);

// Should use default order from schema properties
expect(propertyIds).toEqual(['prompt', 'width']);
});

it('should return undefined when extension keyword is not found in schema', () => {
const panelInputWithMissingKeyword = {
...mockPanelInput,
orderExtensionKeyword: 'x-missing-order'
};

const result = getProperties(mockInputSchema, panelInputWithMissingKeyword);
const propertyIds = result.map((p) => p.id);

// Should fall back to default order
expect(propertyIds).toEqual(['prompt', 'width']);
});

it('should return order from extension keyword when found', () => {
const schemaWithExtension = {
...mockInputSchema,
'x-custom-order': ['width', 'prompt']
};

const panelInputWithExtension = {
...mockPanelInput,
orderExtensionKeyword: 'x-custom-order'
};

const result = getProperties(schemaWithExtension, panelInputWithExtension);
const propertyIds = result.map((p) => p.id);

expect(propertyIds).toEqual(['width', 'prompt']);
});

it('should handle array of extension keywords and use first found', () => {
const schemaWithMultipleExtensions = {
...mockInputSchema,
'x-second-order': ['width', 'prompt']
};

const panelInputWithMultipleKeywords = {
...mockPanelInput,
orderExtensionKeyword: [
'x-first-order',
'x-second-order',
'x-third-order'
]
};

const result = getProperties(
schemaWithMultipleExtensions,
panelInputWithMultipleKeywords
);
const propertyIds = result.map((p) => p.id);

expect(propertyIds).toEqual(['width', 'prompt']);
});

it('should throw error for invalid orderExtensionKeyword type', () => {
const panelInputWithInvalidKeyword = {
...mockPanelInput,
orderExtensionKeyword: 123 as any
};

expect(() => {
getProperties(mockInputSchema, panelInputWithInvalidKeyword);
}).toThrow('orderExtensionKeyword must be a string or an array of strings');
});

it('should throw error for invalid orderExtensionKeyword object type', () => {
const panelInputWithInvalidKeyword = {
...mockPanelInput,
orderExtensionKeyword: {} as any
};

expect(() => {
getProperties(mockInputSchema, panelInputWithInvalidKeyword);
}).toThrow('orderExtensionKeyword must be a string or an array of strings');
});
});
Original file line number Diff line number Diff line change
@@ -9,31 +9,60 @@ function getProperties<K extends OutputKind, I>(
if (inputSchema.properties == null) {
throw new Error('Input schema must have properties');
}

const propertiesFromSchema = inputSchema.properties;
const properties: Property[] = [];

const orderedProperties = getOrderedProperties(inputSchema, panelInput);
if (orderedProperties != null) {
return orderedProperties;
} else {
Object.entries(inputSchema.properties).forEach((property) => {
const id = property[0];
const schema = property[1] as OpenAPIV3.SchemaObject;
properties.push({ id, schema });
});
}
const order = getOrder(inputSchema, panelInput);
order.forEach((propertyKey) => {
const id = propertyKey;
const schema =
(propertiesFromSchema[propertyKey] as OpenAPIV3.SchemaObject) ??
undefined;
properties.push({ id, schema });
});

return properties;
}

function getOrderedProperties<K extends OutputKind, I>(
function getOrder<K extends OutputKind, I>(
inputSchema: OpenAPIV3.SchemaObject,
panelInput: PanelInputSchema<K, I>
): string[] {
const panelInputOrder = panelInput.order;
if (panelInputOrder != null && Array.isArray(panelInputOrder)) {
return panelInputOrder;
}

if (inputSchema.properties == null) {
throw new Error('Input schema must have properties');
}
const propertiesFromSchema = inputSchema.properties;
const orderFromKeys = Object.keys(propertiesFromSchema);
const orderFromExtensionKeyword = getOrderFromExtensionKeyword(
inputSchema,
panelInput
);

let order = orderFromExtensionKeyword ?? orderFromKeys;

if (panelInputOrder != null && typeof panelInputOrder === 'function') {
order = panelInputOrder(order);
}

// Return order with no duplicates
return [...new Set(order)];
}

/**
* Get the order from an extension keyword in the input schema (e.g. x-order) if it exists.
*/
function getOrderFromExtensionKeyword<K extends OutputKind, I>(
inputSchema: OpenAPIV3.SchemaObject,
panelInput: PanelInputSchema<K, I>
): Property[] | undefined {
): string[] | undefined {
if (panelInput.orderExtensionKeyword == null) {
return undefined;
}
const properties: Property[] = [];

if (
typeof panelInput.orderExtensionKeyword !== 'string' &&
@@ -43,7 +72,6 @@ function getOrderedProperties<K extends OutputKind, I>(
'orderExtensionKeyword must be a string or an array of strings'
);
}

const orderExtensionKeywords =
typeof panelInput.orderExtensionKeyword === 'string'
? [panelInput.orderExtensionKeyword]
@@ -62,30 +90,7 @@ function getOrderedProperties<K extends OutputKind, I>(
// @ts-ignore
inputSchema[orderExtensionKeyword] as string[];

if (order == null || Array.isArray(order) === false) {
throw new Error(
`Extension keyword ${orderExtensionKeyword} must be an array of strings`
);
}

[...new Set(order)].forEach((orderKey) => {
const property = inputSchema.properties?.[orderKey];
if (property != null) {
properties.push({
id: orderKey,
schema: property as OpenAPIV3.SchemaObject
});
}
});

if (properties.length === 0) {
throw new Error(
`Could not find any properties with order extension keyword(s) ${orderExtensionKeywords.join(
', '
)}`
);
}
return properties;
return order;
}

export default getProperties;
9 changes: 9 additions & 0 deletions packages/plugin-ai-generation-web/src/generation/provider.ts
Original file line number Diff line number Diff line change
@@ -234,6 +234,15 @@ export interface PanelInputSchema<K extends OutputKind, I>
*/
orderExtensionKeyword?: string | string[];

/**
* Defined the order of the properties in the panel. Takes precedence over
* the order defined in the schema (also see `orderExtensionKeyword`).
*
* If a function is provided, it receives the current order of properties from
* the schema and can return a new order.
*/
order?: string[] | ((order: string[]) => string[]);

/**
* Returns the necessary input for the creation of a block.
*