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

Display add new property as the last option only for reference dropdown #642

Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions app/src/client/Main.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ body {
-moz-osx-font-smoothing: grayscale;
}

/* Preload Material Symbols Outlined font */
davorinrusevljan marked this conversation as resolved.
Show resolved Hide resolved
body::after {
content: '\ue147';
font-family: 'Material Symbols Outlined';
position: absolute;
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
}

code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
Expand Down
34 changes: 32 additions & 2 deletions app/src/client/components/buildPage/FormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ import Select, { StylesConfig } from 'react-select';
import { TextInput } from '../form/TextInput';
import { SECRETS_TO_MASK } from '../../utils/constants';
import { TextArea } from '../form/TextArea';
import { SelectOption } from './PropertySchemaParser';

const markAddPropertyOption: StylesConfig<SelectOption, false> = {
control: (baseStyles) => ({
...baseStyles,
borderColor: '#003257',
}),
option: (styles, { data }) => ({
...styles,
display: 'flex',
alignItems: 'center',
'::before': data.isAddPropertyOption
? {
fontFamily: '"Material Symbols Outlined"',
content: '"\ue147"',
marginRight: '5px',
}
: {},
}),
};

interface FormFieldProps {
field: FieldApi<any, any, any, any>;
Expand Down Expand Up @@ -60,6 +80,16 @@ export const FormField: React.FC<FormFieldProps> = ({ field, property, fieldKey,
}
}, [property.enum]);

const handleSelectOnchange = (selectedOption: any) => {
const value = selectedOption?.value || null;
const isAddOption = selectedOption?.isAddPropertyOption;

field.handleChange(value);
davorinrusevljan marked this conversation as resolved.
Show resolved Hide resolved
if (isAddOption) {
console.log('Add option selected for: ', value);
}
};

return (
<div className='w-full mt-2'>
<label htmlFor={fieldKey}>{`${property.title} ${isOptionalRefField ? ' (Optional)' : ''}`}</label>
Expand All @@ -70,12 +100,12 @@ export const FormField: React.FC<FormFieldProps> = ({ field, property, fieldKey,
classNamePrefix='react-select'
inputId={fieldKey}
options={selectOptions}
onChange={(e: any) => field.handleChange(e?.value || null)} // field.handleChange(e.value)
onChange={handleSelectOnchange}
className='pt-1 pb-1'
defaultValue={defaultValue}
isSearchable={true}
isClearable={isOptionalRefField}
// styles={customStyles}
styles={markAddPropertyOption}
/>
) : fieldKey === 'system_message' ? (
<TextArea
Expand Down
45 changes: 35 additions & 10 deletions app/src/client/components/buildPage/PropertySchemaParser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import _ from 'lodash';

import { ListOfSchemas, Schema } from '../../interfaces/BuildPageInterfaces';
import { ListOfSchemas, PropertiesSchema, Schema } from '../../interfaces/BuildPageInterfaces';
import { filerOutComponentData } from './buildPageUtils';

export enum Flow {
UPDATE_MODEL = 'update_model',
Expand All @@ -12,6 +13,7 @@ export type SetActiveModelType = (model: string | null) => void;
export interface SelectOption {
value: string;
label: string;
isAddPropertyOption?: boolean;
}

export interface UserProperties {
Expand Down Expand Up @@ -52,19 +54,21 @@ interface PropertySchemaParserInterface {
}

export class PropertySchemaParser implements PropertySchemaParserInterface {
private readonly propertiesSchema: PropertiesSchema;
private readonly propertyName: string;
private readonly propertySchemas: ListOfSchemas;
private flow: Flow;
private activeModel: string | null;
private readonly propertyName: string;
private activeModelObj: any;
private schema: Schema | undefined;
private userProperties: UserProperties[] | null;
private refFields: { [key: string]: any } = {};
private nonRefButDropdownFields: { [key: string]: any } = {};

constructor(propertySchemas: ListOfSchemas) {
this.propertySchemas = propertySchemas;
this.propertyName = propertySchemas.name;
constructor(propertiesSchema: PropertiesSchema, propertyName: string) {
this.propertiesSchema = propertiesSchema;
this.propertyName = propertyName;
this.propertySchemas = filerOutComponentData(propertiesSchema, propertyName);
this.flow = Flow.ADD_MODEL;
this.activeModel = null;
this.activeModelObj = null;
Expand Down Expand Up @@ -119,20 +123,41 @@ export class PropertySchemaParser implements PropertySchemaParserInterface {
return defaultValues;
}

private getCapitalizeTitle(key: string): string {
return key === 'llm' ? 'LLM' : this.capitalizeWords(key);
}

private createAddPropertyOption(refTypes: string[]): SelectOption[] {
const targetPropertyName: string = _.chain(this.propertiesSchema.list_of_schemas)
.find((schemaGroup) => _.some(schemaGroup.schemas, { name: refTypes[0] }))
.get('name', '')
.value();

return [
{
label: `Add new "${this.getCapitalizeTitle(targetPropertyName)}"`,
value: targetPropertyName,
isAddPropertyOption: true,
},
];
}

private handleReferenceField(key: string, property: any, defaultValues: { [key: string]: any }): void {
const refTypes = this.getRefTypes(property);
const matchingProperties = this.getMatchingProperties(refTypes);
const enumValues = this.createEnumValues(matchingProperties);
const userOptions = this.getUserOptions(matchingProperties);
const addPropertyOption = this.createAddPropertyOption(refTypes);
const isOptional = this.isOptionalField(property);
const defaultValue = this.getDefaultValueForRefField(key, enumValues, isOptional);
const defaultValue = this.getDefaultValueForRefField(key, userOptions, isOptional);
const options = [...userOptions, ...addPropertyOption];

this.refFields[key] = {
property: matchingProperties,
htmlForSelectBox: {
description: '',
enum: enumValues,
enum: options,
default: defaultValue,
title: this.capitalizeWords(key),
title: this.getCapitalizeTitle(key),
},
initialFormValue: defaultValue?.value ?? null,
isOptional: isOptional,
Expand Down Expand Up @@ -189,7 +214,7 @@ export class PropertySchemaParser implements PropertySchemaParserInterface {
defaultValues[key] = this.nonRefButDropdownFields[key].initialFormValue;
}

private createEnumValues(properties: UserProperties[]): SelectOption[] {
private getUserOptions(properties: UserProperties[]): SelectOption[] {
return properties.map((prop) => ({
value: prop.uuid,
label: prop.json_str.name,
Expand Down
7 changes: 4 additions & 3 deletions app/src/client/components/buildPage/UserProperty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ export const UserProperty = memo(({ activeProperty, propertiesSchema, sideNavIte
const propertyName = activeProperty === 'llm' ? 'LLM' : capitalizeFirstLetter(activeProperty);
const propertySchemasList = filerOutComponentData(propertiesSchema, activeProperty);

const { parser, activeModel, createParser } = usePropertySchemaParser(propertySchemasList);
const { parser, activeModel, createParser } = usePropertySchemaParser(propertiesSchema, activeProperty);

const { data: userProperties, refetch: refetchUserProperties, isLoading: isLoading } = useQuery(getModels);
const userPropertiesByType = (userProperties && filterPropertiesByType(userProperties, activeProperty)) || [];

const setActiveModel = (model: string | null, flow: Flow = Flow.ADD_MODEL) => {
createParser({ propertySchemasList, activeModel: model, flow: flow, userProperties });
createParser({ propertiesSchema, activeProperty, activeModel: model, flow: flow, userProperties });
};

const addProperty = (e: React.MouseEvent<HTMLButtonElement>) => {
Expand All @@ -44,7 +44,8 @@ export const UserProperty = memo(({ activeProperty, propertiesSchema, sideNavIte
const getSelectedUserProperty = (index: number) => {
const selectedProperty = userPropertiesByType[index];
createParser({
propertySchemasList,
propertiesSchema,
activeProperty,
activeModel: selectedProperty.model_name,
activeModelObj: selectedProperty,
flow: Flow.UPDATE_MODEL,
Expand Down
13 changes: 7 additions & 6 deletions app/src/client/components/buildPage/usePropertySchemaParser.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import { useState, useEffect, useCallback } from 'react';
import { PropertySchemaParser, Flow } from './PropertySchemaParser';
import { ListOfSchemas } from '../../interfaces/BuildPageInterfaces';
import { PropertiesSchema } from '../../interfaces/BuildPageInterfaces';

interface CustomInitOptions {
propertySchemasList: ListOfSchemas;
propertiesSchema: PropertiesSchema;
activeProperty: string;
activeModel: string | null;
flow: Flow;
activeModelObj?: any;
userProperties: any;
}

export function usePropertySchemaParser(propertySchemasList: ListOfSchemas) {
export function usePropertySchemaParser(propertiesSchema: PropertiesSchema, activeProperty: string) {
const [activeModel, setActiveModel] = useState<string | null>(null);
const [parser, setParser] = useState<PropertySchemaParser | null>(null);

// Recreate the parser when propertySchemasList or activeModel changes
useEffect(() => {
const newParser = new PropertySchemaParser(propertySchemasList);
const newParser = new PropertySchemaParser(propertiesSchema, activeProperty);
newParser.setActiveModel(activeModel);
setParser(newParser);
}, [propertySchemasList]);
}, [propertiesSchema, activeProperty]);

const createParser = useCallback((customOptions: CustomInitOptions) => {
const newParser = new PropertySchemaParser(customOptions.propertySchemasList);
const newParser = new PropertySchemaParser(customOptions.propertiesSchema, customOptions.activeProperty);
newParser.setActiveModel(customOptions.activeModel);

if (customOptions.flow) {
Expand Down
65 changes: 11 additions & 54 deletions app/src/client/tests/DynamicForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,66 +9,19 @@ import { renderInContext } from 'wasp/client/test';
import * as operations from 'wasp/client/operations';

import { DynamicForm } from '../components/buildPage/DynamicForm';
import { ListOfSchemas } from '../interfaces/BuildPageInterfaces';
import { PropertySchemaParser } from '../components/buildPage/PropertySchemaParser';
import { mockPropertieSchemas } from './mocks';

// Mock the operation
vi.mock('wasp/client/operations', () => ({
validateForm: vi.fn(),
addUserModels: vi.fn(),
}));

const mockPropertySchemasList: ListOfSchemas = {
name: 'secret',
schemas: [
{
name: 'AnthropicAPIKey',
json_schema: {
properties: {
name: {
description: 'The name of the item',
minLength: 1,
title: 'Name',
type: 'string',
},
api_key: {
description: 'The API Key from Anthropic',
title: 'Api Key',
type: 'string',
},
},
required: ['name', 'api_key'],
title: 'AnthropicAPIKey',
type: 'object',
},
},
{
name: 'AzureOAIAPIKey',
json_schema: {
properties: {
name: {
description: 'The name of the item',
minLength: 1,
title: 'Name',
type: 'string',
},
api_key: {
description: 'The API Key from Azure OpenAI',
title: 'Api Key',
type: 'string',
},
},
required: ['name', 'api_key'],
title: 'AzureOAIAPIKey',
type: 'object',
},
},
],
};

describe('DynamicForm', () => {
it('renders form fields based on the AnthropicAPIKey schema and handles submission', async () => {
const parser = new PropertySchemaParser(mockPropertySchemasList);
const activeProperty = 'secret';
const parser = new PropertySchemaParser(mockPropertieSchemas, activeProperty);
parser.setActiveModel('AnthropicAPIKey');
const mockSetActiveModel = vi.fn();
const mockRefetchUserProperties = vi.fn();
Expand Down Expand Up @@ -106,7 +59,8 @@ describe('DynamicForm', () => {
});

it('renders form fields and handles successful submission', async () => {
const parser = new PropertySchemaParser(mockPropertySchemasList);
const activeProperty = 'secret';
const parser = new PropertySchemaParser(mockPropertieSchemas, activeProperty);
parser.setActiveModel('AnthropicAPIKey');
const mockSetActiveModel = vi.fn();
const mockRefetchUserProperties = vi.fn();
Expand Down Expand Up @@ -142,7 +96,8 @@ describe('DynamicForm', () => {
});

it('handles form submission failure due to validation error', async () => {
const parser = new PropertySchemaParser(mockPropertySchemasList);
const activeProperty = 'secret';
const parser = new PropertySchemaParser(mockPropertieSchemas, activeProperty);
parser.setActiveModel('AnthropicAPIKey');
const mockSetActiveModel = vi.fn();
const mockRefetchUserProperties = vi.fn();
Expand Down Expand Up @@ -175,7 +130,8 @@ describe('DynamicForm', () => {
});

it('calls handleCancel when cancel button is clicked', async () => {
const parser = new PropertySchemaParser(mockPropertySchemasList);
const activeProperty = 'secret';
const parser = new PropertySchemaParser(mockPropertieSchemas, activeProperty);
parser.setActiveModel('AnthropicAPIKey');
const mockSetActiveModel = vi.fn();
const mockRefetchUserProperties = vi.fn();
Expand All @@ -202,7 +158,8 @@ describe('DynamicForm', () => {
});

it('masks the API key input', () => {
const parser = new PropertySchemaParser(mockPropertySchemasList);
const activeProperty = 'secret';
const parser = new PropertySchemaParser(mockPropertieSchemas, activeProperty);
parser.setActiveModel('AnthropicAPIKey');
const mockSetActiveModel = vi.fn();
const mockRefetchUserProperties = vi.fn();
Expand Down
Loading
Loading