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

#9137: types and migrations for mod variable declaration section (1/3) #9138

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import type { Deployment } from "@/types/contract";
import type { OptionsArgs } from "@/types/runtimeTypes";
import type { IntegrationDependency } from "@/integrations/integrationTypes";
import { nowTimestamp } from "@/utils/timeUtils";
import { emptyModVariablesDefinitionFactory } from "@/utils/modUtils";
import type { SetRequired } from "type-fest";

export type ActivateModComponentParam = {
/**
Expand Down Expand Up @@ -64,7 +66,10 @@ export function mapModComponentDefinitionToActivatedModComponent<
deployment,
optionsArgs,
integrationDependencies,
}: ActivateModComponentParam): ActivatedModComponent<Config> {
}: ActivateModComponentParam): SetRequired<
ActivatedModComponent<Config>,
"variables"
> {
const timestamp = nowTimestamp();

const activatedModComponent = {
Expand All @@ -75,14 +80,16 @@ export function mapModComponentDefinitionToActivatedModComponent<
// Definitions are pushed down into the mod components. That's OK because `resolveDefinitions` determines
// uniqueness based on the content of the definition. Therefore, bricks will be re-used as necessary
definitions: modDefinition.definitions ?? {},
// Mod variable definitions/declarations are pushed down into the mod components.
variables: modDefinition.variables ?? emptyModVariablesDefinitionFactory(),
optionsArgs,
label: modComponentDefinition.label,
extensionPointId: modComponentDefinition.id,
config: modComponentDefinition.config as Config,
active: true,
createTimestamp: timestamp,
updateTimestamp: timestamp,
} as ActivatedModComponent<Config>;
} as SetRequired<ActivatedModComponent<Config>, "variables">;

// Set optional fields only if the source mod component has a value. Normalizing the values
// here makes testing harder because we then have to account for the normalized value in assertions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { validateOutputKey } from "@/runtime/runtimeTypes";
import { toExpression } from "@/utils/expressionUtils";
import { normalizeAvailability } from "@/bricks/available";
import { StarterBrickTypes } from "@/types/starterBrickTypes";
import { emptyModVariablesDefinitionFactory } from "@/utils/modUtils";

describe("selectVariables", () => {
test("selects nothing when no services used", () => {
Expand Down Expand Up @@ -222,6 +223,7 @@ describe("selectVariables", () => {
],
permissions: emptyPermissionsFactory(),
optionsArgs: {},
variablesDefinition: emptyModVariablesDefinitionFactory(),
modMetadata: undefined,
modComponent: {
brickPipeline: [
Expand Down
140 changes: 140 additions & 0 deletions src/pageEditor/panes/save/saveHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
} from "@/types/registryTypes";
import {
type ModOptionsDefinition,
type ModVariablesDefinition,
type UnsavedModDefinition,
} from "@/types/modDefinitionTypes";
import {
Expand All @@ -67,6 +68,7 @@ import { integrationDependencyFactory } from "@/testUtils/factories/integrationF
import { minimalUiSchemaFactory } from "@/utils/schemaUtils";
import {
emptyModOptionsDefinitionFactory,
emptyModVariablesDefinitionFactory,
normalizeModDefinition,
} from "@/utils/modUtils";
import { INTEGRATIONS_BASE_SCHEMA_URL } from "@/integrations/constants";
Expand Down Expand Up @@ -131,6 +133,9 @@ describe("replaceModComponent round trip", () => {
produce(modDefinition, (draft) => {
castDraft(draft.metadata).id = newId;
draft.extensionPoints[0]!.label = "New Label";

// `variableDefinition` is required on mod component form state, so it gets populated on the mod definition
draft.variables = emptyModVariablesDefinitionFactory();
}),
);
});
Expand Down Expand Up @@ -176,6 +181,9 @@ describe("replaceModComponent round trip", () => {
produce(modDefinition, (draft) => {
castDraft(draft.metadata).id = newId;
draft.extensionPoints[0]!.label = "New Label";

// `variableDefinition` is required on mod component form state, so it gets populated on the mod definition
draft.variables = emptyModVariablesDefinitionFactory();
}),
);
});
Expand Down Expand Up @@ -384,6 +392,9 @@ describe("replaceModComponent round trip", () => {
produce(modDefinition, (draft) => {
castDraft(draft.metadata).id = newId;
draft.extensionPoints[0]!.label = "New Label";

// `variableDefinition` is required on mod component form state, so it gets populated on the mod definition
draft.variables = emptyModVariablesDefinitionFactory();
}),
);
});
Expand Down Expand Up @@ -435,6 +446,9 @@ describe("replaceModComponent round trip", () => {
castDraft(draft.metadata).id = newId;
draft.definitions![starterBrick.metadata!.id!]!.apiVersion = "v3";
draft.extensionPoints[0]!.label = "New Label";

// `variableDefinition` is required on mod component form state, so it gets populated on the mod definition
draft.variables = emptyModVariablesDefinitionFactory();
}),
);
});
Expand Down Expand Up @@ -483,6 +497,132 @@ describe("replaceModComponent round trip", () => {
});
});

describe("mod variables", () => {
async function runReplaceModComponent(
modVariablesDefinition: ModVariablesDefinition | undefined,
modComponentModVariables: ModVariablesDefinition,
) {
const modDefinition = defaultModDefinitionFactory({
variables: modVariablesDefinition,
});

const modComponentState = modComponentSlice.reducer(
{ activatedModComponents: [] },
modComponentSlice.actions.activateMod({
modDefinition,
screen: "pageEditor",
isReactivate: false,
}),
);

const modComponentFormState =
await brickModComponentAdapter.fromModComponent(
modComponentState.activatedModComponents[0]!,
);

modComponentFormState.variablesDefinition = modComponentModVariables;

return replaceModComponent(
modDefinition,
modDefinition.metadata,
modComponentState.activatedModComponents,
modComponentFormState,
);
}

test("adds empty schema when mod options is empty", async () => {
const emptyVariables = emptyModVariablesDefinitionFactory();

const updatedModDefinition = await runReplaceModComponent(
undefined,
emptyVariables,
);

expect(updatedModDefinition.variables).toStrictEqual(
emptyModVariablesDefinitionFactory(),
);
});

test("creates mod variables", async () => {
const modVariablesDefinition: ModVariablesDefinition = {
schema: {
type: "object",
properties: {
channels: {
type: "string",
title: "Channels",
},
},
},
};

const updatedModDefinition = await runReplaceModComponent(
undefined,
modVariablesDefinition,
);

expect(updatedModDefinition.variables).toBe(modVariablesDefinition);
});

test("updates mod variables", async () => {
const modVariablesDefinition: ModVariablesDefinition = {
schema: {
type: "object",
properties: {
channels: {
type: "string",
title: "Channels",
},
},
},
};

const modComponentModVariables: ModVariablesDefinition = {
schema: {
type: "object",
properties: {
credentials: {
type: "string",
},
},
},
};

const updatedModDefinition = await runReplaceModComponent(
modVariablesDefinition,
modComponentModVariables,
);

expect(updatedModDefinition.variables).toBe(modComponentModVariables);
});

test("preserves mod variables", async () => {
const modVariablesDefinition: ModVariablesDefinition = {
schema: {
type: "object",
properties: {
channels: {
type: "string",
title: "Channels",
},
},
},
};

const modComponentModVariables: ModVariablesDefinition =
emptyModVariablesDefinitionFactory();

const updatedMod = await runReplaceModComponent(
modVariablesDefinition,
modComponentModVariables,
);

expect(updatedMod.variables).toStrictEqual(
emptyModVariablesDefinitionFactory(),
);
});
});

describe("mod options", () => {
async function runReplaceModComponent(
modOptionsDefinition: ModOptionsDefinition,
Expand Down
17 changes: 11 additions & 6 deletions src/pageEditor/panes/save/saveHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ import {
type ModDependencyAPIVersion,
} from "@/integrations/integrationTypes";
import { type Schema } from "@/types/schemaTypes";
import { normalizeModOptionsDefinition } from "@/utils/modUtils";
import {
emptyModVariablesDefinitionFactory,
normalizeModOptionsDefinition,
} from "@/utils/modUtils";
import { INTEGRATIONS_BASE_SCHEMA_URL } from "@/integrations/constants";
import {
isStarterBrickDefinitionLike,
Expand Down Expand Up @@ -212,13 +215,13 @@ function deleteUnusedStarterBrickDefinitions(
/**
* Create a copy of `sourceMod` with `modMetadata` and `modComponent`.
*
* NOTE: the caller is responsible for updating an starter brick package (i.e., that has its own version). This method
* NOTE: the caller is responsible for updating a starter brick package (i.e., that has its own version). This method
* only handles the starter brick if it's an inner definition
*
* @param sourceMod the original mod
* @param sourceMod the original mod definition
* @param modMetadata the metadata for the new mod
* @param activatedModComponents the user's locally activated mod components (i.e., from optionsSlice). Used to locate the
* mod component's position in sourceMod
* @param activatedModComponents the user's locally activated mod components (i.e., from modComponentsSlice). Used to
* locate the mod component's position in sourceMod
* @param newModComponent the new mod component state (i.e., submitted via Formik)
*/
export function replaceModComponent(
Expand All @@ -240,6 +243,7 @@ export function replaceModComponent(
return produce(sourceMod, (draft: ModDefinition) => {
draft.metadata = modMetadata;
draft.options = newModComponent.optionsDefinition;
draft.variables = newModComponent.variablesDefinition;

if (sourceMod.apiVersion !== newModComponent.apiVersion) {
const canUpdateModApiVersion = sourceMod.extensionPoints.length <= 1;
Expand All @@ -250,7 +254,7 @@ export function replaceModComponent(

assertNotNullish(
starterBrickId,
"First mod compnent in mod definition has no starter brick",
"First mod component in mod definition has no starter brick",
);

// eslint-disable-next-line security/detect-object-injection -- getting a property by mod component id
Expand Down Expand Up @@ -389,6 +393,7 @@ const emptyModDefinition: UnsavedModDefinition = {
extensionPoints: [],
definitions: {},
options: normalizeModOptionsDefinition(null),
variables: emptyModVariablesDefinitionFactory(),
};

/**
Expand Down
12 changes: 11 additions & 1 deletion src/pageEditor/starterBricks/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ import {
type BaseFormState,
type SingleLayerReaderConfig,
} from "@/pageEditor/store/editor/baseFormStateTypes";
import { emptyModOptionsDefinitionFactory } from "@/utils/modUtils";
import {
emptyModOptionsDefinitionFactory,
emptyModVariablesDefinitionFactory,
} from "@/utils/modUtils";
import {
type Availability,
type NormalizedAvailability,
Expand Down Expand Up @@ -123,6 +126,7 @@ export function baseFromModComponent<T extends StarterBrickType>(
| "integrationDependencies"
| "permissions"
| "optionsArgs"
| "variablesDefinition"
| "modMetadata"
> & { type: T } {
return {
Expand All @@ -134,6 +138,8 @@ export function baseFromModComponent<T extends StarterBrickType>(
integrationDependencies: config.integrationDependencies ?? [],
permissions: config.permissions ?? {},
optionsArgs: config.optionsArgs ?? {},
variablesDefinition:
config.variables ?? emptyModVariablesDefinitionFactory(),
type,
modMetadata: config._recipe,
};
Expand Down Expand Up @@ -173,6 +179,7 @@ export function baseSelectModComponent({
uuid,
label,
optionsArgs,
variablesDefinition,
integrationDependencies,
permissions,
starterBrick,
Expand All @@ -187,6 +194,7 @@ export function baseSelectModComponent({
| "integrationDependencies"
| "permissions"
| "optionsArgs"
| "variables"
> {
return {
id: uuid,
Expand All @@ -197,6 +205,7 @@ export function baseSelectModComponent({
integrationDependencies,
permissions,
optionsArgs,
variables: variablesDefinition,
};
}

Expand All @@ -209,6 +218,7 @@ export function makeInitialBaseState(
integrationDependencies: [],
permissions: emptyPermissionsFactory(),
optionsArgs: {},
variablesDefinition: emptyModVariablesDefinitionFactory(),
modComponent: {
brickPipeline: [],
},
Expand Down
Loading
Loading