Skip to content
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
140 changes: 140 additions & 0 deletions entities/parameter/lib/__tests__/parametersFilter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {IShapeDiverParameter} from "@AppBuilderLib/entities/parameter/config/parameter";
import {NotificationAction} from "@AppBuilderLib/features/notifications/config/notificationcontext";
import {ResParameterType} from "@shapediver/sdk.geometry-api-sdk-v2";
import {
filterAndValidateParameters,
generateParameterFeedback,
} from "../parametersFilter";
function createMockParameter(
overrides: Partial<IShapeDiverParameter<any>> & {
definition: IShapeDiverParameter<any>["definition"];
},
): IShapeDiverParameter<any> {
return {
state: {
uiValue: overrides.state?.uiValue,
execValue: overrides.state?.execValue,
dirty: false,
disableOtherParameters: false,
stringExecValue: () => "",
},
actions: {
setUiValue: () => true,
setUiAndExecValue: () => true,
execute: async () => "",
isValid: overrides.actions?.isValid ?? (() => true),
isUiValueDifferent: () => false,
resetToDefaultValue: () => undefined,
resetToExecValue: () => undefined,
},
acceptRejectMode: false,
...overrides,
} as IShapeDiverParameter<any>;
}

describe("filterAndValidateParameters", () => {
const widthParam = createMockParameter({
definition: {
id: "width",
name: "Width",
type: ResParameterType.FLOAT,
min: 0,
max: 100,
defval: 10,
} as IShapeDiverParameter<any>["definition"],
});

it("reports unknown parameter in invalidParameters", () => {
const result = filterAndValidateParameters(
[widthParam],
[{id: "missing", value: 1}],
);

expect(result.hasValidParameters).toBe(false);
expect(result.skippedParameters).toEqual(["missing"]);
expect(result.invalidParameters).toEqual([
{
name: "missing",
message:
'Parameter "missing" does not exist in the current model session.',
},
]);
});

it("reports invalid value in invalidParameters", () => {
const result = filterAndValidateParameters(
[
createMockParameter({
definition: widthParam.definition,
actions: {isValid: () => false},
}),
],
[{id: "width", value: 999}],
);

expect(result.hasValidParameters).toBe(false);
expect(result.skippedParameters).toEqual(["width"]);
expect(result.invalidParameters).toEqual([
{
name: "width",
message: 'Value is not valid for parameter "width"',
},
]);
});

it("keeps valid parameters and reports only failures", () => {
const result = filterAndValidateParameters(
[widthParam],
[
{id: "width", value: 42},
{id: "unknown", value: 1},
],
);

expect(result.hasValidParameters).toBe(true);
expect(result.validParameters).toEqual({width: 42});
expect(result.invalidParameters).toEqual([
{
name: "unknown",
message:
'Parameter "unknown" does not exist in the current model session.',
},
]);
});
});

describe("generateParameterFeedback", () => {
it("uses invalidParameters messages for partial success warnings", () => {
const feedback = generateParameterFeedback({
validParameters: {width: 42},
skippedParameters: ["unknown"],
invalidParameters: [
{
name: "unknown",
message:
'Parameter "unknown" does not exist in the current model session.',
},
],
hasValidParameters: true,
});

expect(feedback.type).toBe(NotificationAction.WARNING);
expect(feedback.message).toBe(
'Parameter "unknown" does not exist in the current model session.',
);
});

it("falls back to skipped parameter names when invalidParameters is empty", () => {
const feedback = generateParameterFeedback({
validParameters: {width: 42},
skippedParameters: ["foo", "bar"],
invalidParameters: [],
hasValidParameters: true,
});

expect(feedback.type).toBe(NotificationAction.WARNING);
expect(feedback.message).toBe(
"The following parameters could not be matched or ar invalid: foo, bar",
);
});
});
30 changes: 27 additions & 3 deletions entities/parameter/lib/parametersFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ import {NotificationAction} from "@AppBuilderLib/features/notifications/config/n
import {z} from "@AppBuilderLib/shared/lib/zod";
import {IShapeDiverParameter} from "../config/parameter";

export interface ParameterInvalidParameter {
name: string;
message: string;
}

export interface ParameterValidationResult {
/** Validated (id,value) pairs of parameters */
validParameters: {[key: string]: any};
/** Names or IDs of parameters present in the input that could not be matched */
skippedParameters: string[];
/** Per-parameter validation failures with user-facing messages */
invalidParameters: ParameterInvalidParameter[];
/** Whether any valid parameters were found */
hasValidParameters: boolean;
}
Expand Down Expand Up @@ -44,6 +51,7 @@ export function filterAndValidateParameters(
): ParameterValidationResult {
const validParameters: {[key: string]: any} = {};
const skippedParameters: string[] = [];
const invalidParameters: ParameterInvalidParameter[] = [];

for (const param of parameterArray) {
// Skip parameters without id or value
Expand All @@ -62,13 +70,23 @@ export function filterAndValidateParameters(
}

if (!paramState) {
skippedParameters.push(param.name || param.id);
const name = param.name || param.id;
skippedParameters.push(name);
invalidParameters.push({
name,
message: `Parameter "${name}" does not exist in the current model session.`,
});
continue;
}

// Validate parameter value
if (!paramState.actions.isValid(param.value)) {
skippedParameters.push(param.name || param.id);
const name = param.name || param.id;
skippedParameters.push(name);
invalidParameters.push({
name,
message: `Value is not valid for parameter "${name}"`,
});
continue;
}

Expand All @@ -78,6 +96,7 @@ export function filterAndValidateParameters(
return {
validParameters,
skippedParameters: skippedParameters,
invalidParameters,
hasValidParameters: Object.keys(validParameters).length > 0,
};
}
Expand Down Expand Up @@ -125,9 +144,14 @@ export function generateParameterFeedback(
}

if (result.skippedParameters.length > 0) {
const message =
result.invalidParameters.length > 0
? result.invalidParameters.map((p) => p.message).join(" ")
: `The following parameters could not be matched or ar invalid: ${result.skippedParameters.join(", ")}`;

return {
type: NotificationAction.WARNING,
message: `The following parameters could not be matched or ar invalid: ${result.skippedParameters.join(", ")}`,
message,
};
}

Expand Down
16 changes: 7 additions & 9 deletions features/appbuilder/config/appbuildertypecheck.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {filterableDatabaseSettingsSchema} from "@AppBuilderLib/entities/parameter/lib/filterableDatabase/filterableDatabaseSettingsSchema";
import {createModelStateCoreSchema} from "@AppBuilderLib/features/model-state/config/createModelState.zod";
import {prettifyError, z} from "@AppBuilderLib/shared/lib/zod";
import {appBuilderThemeOtherPropsSchema} from "@AppBuilderLib/shared/mantine-props/appBuilderThemeOther.zod";
import {mantineThemeOverridePropsSchema} from "@AppBuilderLib/shared/mantine-props/themeOverride.zod";
Expand Down Expand Up @@ -391,15 +392,12 @@ const IAppBuilderParameterValueSourcePropsSdtfSchema = z.strictObject({
});

// Zod type definition for IAppBuilderActionPropsCreateModelState
const IAppBuilderActionPropsCreateModelStateSchema = z.strictObject({
includeImage: z.boolean().optional(),
image: IAppBuilderImageRefSchema.optional(),
includeGltf: z.boolean().optional(),
parameterNamesToInclude: z.array(z.string()).optional(),
parameterNamesToExclude: z.array(z.string()).optional(),
successMessage: z.string().optional(),
errorMessage: z.string().optional(),
});
const IAppBuilderActionPropsCreateModelStateSchema =
createModelStateCoreSchema.extend({
image: IAppBuilderImageRefSchema.optional(),
successMessage: z.string().optional(),
errorMessage: z.string().optional(),
});

// Zod type definition for IAppBuilderParameterValueSourcePropsModelState
const IAppBuilderParameterValueSourcePropsModelStateSchema =
Expand Down
18 changes: 5 additions & 13 deletions features/ecommerce/config/ecommerceapitypecheck.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
import z from "@AppBuilderLib/shared/lib/zod";
import {IAppBuilderImageRefSchema} from "@AppBuilderShared/features/appbuilder/config/appbuildertypecheck";
import {createModelStateDataSchema} from "@AppBuilderLib/features/model-state/config/createModelState.zod";
import {importModelStateDataSchema} from "@AppBuilderLib/features/model-state/config/importModelState.zod";
import z from "zod";

// Zod type definition for ICreateModelStateData
export const ICreateModelStateDataSchema = z.object({
parameterNamesToInclude: z.array(z.string()).optional(),
parameterNamesToExclude: z.array(z.string()).optional(),
includeImage: z.boolean().optional(),
image: IAppBuilderImageRefSchema.optional(),
data: z.record(z.string(), z.any()).optional(),
includeGltf: z.boolean().optional(),
});
export const ICreateModelStateDataSchema = createModelStateDataSchema;

export const validateCreateModelStateData = (value: any) => {
return ICreateModelStateDataSchema.safeParse(value);
};

// Zod type definition for IImportModelStateData
export const IImportModelStateDataSchema = z.object({
modelStateId: z.string(),
});
export const IImportModelStateDataSchema = importModelStateDataSchema;
Comment thread
MajorMeerkatThe3rd marked this conversation as resolved.

export const validateImportModelStateData = (value: any) => {
return IImportModelStateDataSchema.safeParse(value);
Expand Down
45 changes: 3 additions & 42 deletions features/model-state/config/createModelState.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,10 @@
import {ResExportDefinition} from "@shapediver/sdk.geometry-api-sdk-v2";

/**
* Reference to an export (defined by the session)
* (duplicate of IAppBuilderExportRef to avoid importing mantine from here)
*/
interface IExportRef {
/** Id or name or displayname of the referenced export (in that order). */
name: string;
/** Optional id of the session the referenced parameter belongs to. */
sessionId?: string;
/** Properties of the export to be overridden. */
overrides?: Pick<
Partial<ResExportDefinition>,
"displayname" | "group" | "order" | "tooltip" | "hidden"
>;
}

/**
* Reference to an image
* (duplicate of IAppBuilderImageRef to avoid importing mantine from here)
*/
interface IImageRef {
/** Optional reference to export which provides the image. */
export?: Pick<IExportRef, "name" | "sessionId">;
/** URL to image. Can be a data URL including a base 64 encoded image. Takes precedence over export reference. */
href?: string;
}
import type {z} from "zod";
import {createModelStateDataSchema} from "./createModelState.zod";

/**
* Data accepted by the useCreateModelState hook to create a model state.
*/
export interface ICreateModelStateData {
/** Optional list of parameter ids/names to include. */
parameterNamesToInclude?: string[];
/** Optional list of parameter names to exclude. */
parameterNamesToExclude?: string[];
/** Whether to include an image. */
includeImage?: boolean;
/** Optional image definition. If undefined, a screenshot will be used. */
image?: IImageRef | undefined;
/** Optional data to include with the saved model state. */
data?: Record<string, any>;
/** Whether to save a glTF of the scene. */
includeGltf?: boolean;
}
export type ICreateModelStateData = z.infer<typeof createModelStateDataSchema>;

/**
* Data returned from the useCreateModelState hook.
Expand Down
40 changes: 40 additions & 0 deletions features/model-state/config/createModelState.zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {z} from "zod";

export const createModelStateImageRefSchema = z.strictObject({
export: z
.strictObject({
name: z.string(),
sessionId: z.string().optional(),
})
.optional(),
href: z.string().optional(),
});

export const createModelStateCoreSchema = z.strictObject({
parameterNamesToInclude: z
.array(z.string())
.optional()
.describe("Only include these parameters in the saved state."),
parameterNamesToExclude: z
.array(z.string())
.optional()
.describe("Exclude these parameters from the saved state."),
includeImage: z
.boolean()
.optional()
.describe(
"Whether to include a preview image. Use false when not needed.",
),
includeGltf: z
.boolean()
.optional()
.describe("Whether to include an exported GLTF asset."),
});

export const createModelStateDataSchema = createModelStateCoreSchema.extend({
image: createModelStateImageRefSchema.optional(),
data: z
.record(z.string(), z.any())
.optional()
.describe("Optional custom metadata to store with the state."),
});
Loading