Skip to content

Commit

Permalink
Introduce milestones constant (#9032)
Browse files Browse the repository at this point in the history
  • Loading branch information
twschiller authored Aug 20, 2024
1 parent 2ef708d commit 29a8415
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 34 deletions.
5 changes: 3 additions & 2 deletions src/auth/useRequiredPartnerAuth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { renderHook } from "@/pageEditor/testHelpers";
import { integrationConfigFactory } from "@/testUtils/factories/integrationFactories";
import { valueToAsyncState } from "@/utils/asyncStateUtils";
import usePartnerAuthData from "@/auth/usePartnerAuthData";
import { Milestones } from "@/data/model/UserMilestone";

jest.mock("@/store/enterprise/useManagedStorageState");
jest.mock("@/auth/usePartnerAuthData");
Expand Down Expand Up @@ -148,7 +149,7 @@ describe("useRequiredPartnerAuth", () => {
test("requires integration for CE user", async () => {
await mockAuthenticatedMeApiResponse(
meWithPartnerApiResponseFactory({
milestones: [{ key: "aa_community_edition_register" }],
milestones: [{ key: Milestones.AA_COMMUNITY_EDITION_REGISTER }],
}),
);

Expand All @@ -169,7 +170,7 @@ describe("useRequiredPartnerAuth", () => {
test("does not require integration for CE user once partner is removed", async () => {
await mockAuthenticatedMeApiResponse(
meApiResponseFactory({
milestones: [{ key: "aa_community_edition_register" }],
milestones: [{ key: Milestones.AA_COMMUNITY_EDITION_REGISTER }],
}),
);

Expand Down
5 changes: 3 additions & 2 deletions src/auth/useRequiredPartnerAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import usePartnerAuthData from "@/auth/usePartnerAuthData";
import { type Nullishable } from "@/utils/nullishUtils";
import { type UserPartner } from "@/data/model/UserPartner";
import { type ControlRoom } from "@/data/model/ControlRoom";
import { type UserMilestone } from "@/data/model/UserMilestone";
import { Milestones, type UserMilestone } from "@/data/model/UserMilestone";

/**
* Map from partner keys to partner service IDs
Expand Down Expand Up @@ -196,7 +196,8 @@ function useRequiredPartnerAuth(): RequiredPartnerState {
// `organization?.control_room?.id` can only be set when authenticated or the auth is cached
const hasControlRoom = controlRoom != null || Boolean(managedControlRoomUrl);
const isCommunityEditionUser = userMilestones.some(
({ milestoneName }) => milestoneName === "aa_community_edition_register",
({ milestoneName }) =>
milestoneName === Milestones.AA_COMMUNITY_EDITION_REGISTER,
);

const hasPartner =
Expand Down
19 changes: 16 additions & 3 deletions src/data/model/UserMilestone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,24 @@

import { type RequiredMeMilestoneResponse } from "@/data/service/responseTypeHelpers";

import { type ValueOf } from "type-fest";

export const Milestones = {
FIRST_TIME_PUBLIC_MOD_ACTIVATION: "first_time_public_blueprint_install",
AA_COMMUNITY_EDITION_REGISTER: "aa_community_edition_register",
} as const;

/**
* @see Milestones
*/
export type Milestone = ValueOf<typeof Milestones>;

export type UserMilestone = {
/**
* A lower-snake-case, human-readable identifier for the Milestone, e.g. "first_time_extension_install"
* A lower-snake-case, human-readable identifier for the Milestone.
* @see Milestones
*/
milestoneName: string;
milestoneName: Milestone;
/**
* Optional additional information to provide context about the Milestone
*/
Expand All @@ -32,7 +45,7 @@ export function transformUserMilestoneResponse(
response: RequiredMeMilestoneResponse,
): UserMilestone {
return {
milestoneName: response.key,
milestoneName: response.key as Milestone,
metadata: response.metadata ?? {},
};
}
5 changes: 3 additions & 2 deletions src/extensionConsole/pages/activateMod/ActivateModCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { getModActivationInstructions } from "@/utils/modUtils";
import { type ModDefinition } from "@/types/modDefinitionTypes";
import { isInternalRegistryId } from "@/utils/registryUtils";
import { assertNotNullish } from "@/utils/nullishUtils";
import { Milestones } from "@/data/model/UserMilestone";

const WizardHeader: React.VoidFunctionComponent<{
mod: ModDefinition;
Expand Down Expand Up @@ -144,9 +145,9 @@ const ActivateModCard: React.FC<{
if (success) {
notify.success(`Activated ${modDefinition.metadata.name}`);

if (!hasMilestone("first_time_public_blueprint_install")) {
if (!hasMilestone(Milestones.FIRST_TIME_PUBLIC_MOD_ACTIVATION)) {
await createMilestone({
milestoneName: "first_time_public_blueprint_install",
milestoneName: Milestones.FIRST_TIME_PUBLIC_MOD_ACTIVATION,
metadata: {
blueprintId: modDefinition.metadata.id,
},
Expand Down
39 changes: 21 additions & 18 deletions src/hooks/useMilestones.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { meApiResponseFactory } from "@/testUtils/factories/authFactories";
import { transformUserMilestoneResponse } from "@/data/model/UserMilestone";
import { type components } from "@/types/swagger";
import { transformMeResponse } from "@/data/model/Me";
import { milestoneFactory } from "@/testUtils/factories/milestoneFactories";

const renderUseMilestones = (
milestonesApiResponses: components["schemas"]["Me"]["milestones"],
Expand Down Expand Up @@ -51,53 +52,55 @@ describe("useMilestones", () => {
});

test("has milestone", () => {
const userMilestone = milestoneFactory();

const {
result: {
current: { hasMilestone },
},
} = renderUseMilestones([
{
key: "test_milestone",
key: userMilestone,
},
]);

expect(hasMilestone("test_milestone")).toBe(true);
expect(hasMilestone("does_not_exist")).toBe(false);
expect(hasMilestone(userMilestone)).toBe(true);
expect(hasMilestone(milestoneFactory())).toBe(false);
});

test("has every milestone", () => {
const milestone1 = milestoneFactory();
const milestone2 = milestoneFactory();

const {
result: {
current: { hasEveryMilestone },
},
} = renderUseMilestones([
{
key: "test_milestone_1",
key: milestone1,
},
{
key: "test_milestone_2",
key: milestone2,
},
]);

expect(hasEveryMilestone(["test_milestone_1"])).toBe(true);
expect(hasEveryMilestone(["test_milestone_1", "test_milestone_2"])).toBe(
true,
);
expect(hasEveryMilestone([milestone1])).toBe(true);
expect(hasEveryMilestone([milestone1, milestone2])).toBe(true);
expect(
hasEveryMilestone([
"test_milestone_1",
"test_milestone_2",
"does_not_exist",
]),
hasEveryMilestone([milestone1, milestone2, milestoneFactory()]),
).toBe(false);
expect(hasEveryMilestone([])).toBe(true);
});

test("get milestone", () => {
const milestone1 = milestoneFactory();
const milestone2 = milestoneFactory();

const test_milestone_response: NonNullable<
components["schemas"]["Me"]["milestones"]
>[number] = {
key: "test_milestone_1",
key: milestone1,
metadata: {
value: "foo",
},
Expand All @@ -109,16 +112,16 @@ describe("useMilestones", () => {
} = renderUseMilestones([
test_milestone_response,
{
key: "test_milestone_2",
key: milestone2,
metadata: {
value: "bar",
},
},
]);

expect(getMilestone("test_milestone_1")).toStrictEqual(
expect(getMilestone(milestone1)).toStrictEqual(
transformUserMilestoneResponse(test_milestone_response),
);
expect(getMilestone("does_not_exist")).toBeUndefined();
expect(getMilestone(milestoneFactory())).toBeUndefined();
});
});
14 changes: 7 additions & 7 deletions src/hooks/useMilestones.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ import { useSelector } from "react-redux";
import { selectMilestones } from "@/auth/authSelectors";
import { useMemo } from "react";
import { appApi } from "@/data/service/api";
import { type UserMilestone } from "@/data/model/UserMilestone";
import { type Milestone, type UserMilestone } from "@/data/model/UserMilestone";
import { type Nullishable } from "@/utils/nullishUtils";

type MilestoneHelpers = {
getMilestone: (milestoneKey: string) => Nullishable<UserMilestone>;
hasMilestone: (milestoneKey: string) => boolean;
hasEveryMilestone: (milestoneNames: string[]) => boolean;
getMilestone: (milestoneKey: Milestone) => Nullishable<UserMilestone>;
hasMilestone: (milestoneKey: Milestone) => boolean;
hasEveryMilestone: (milestoneNames: Milestone[]) => boolean;
isFetching: boolean;
isLoading: boolean;
refetch: () => void;
Expand All @@ -45,13 +45,13 @@ function useMilestones(): MilestoneHelpers {
]),
);

const getMilestone = (milestoneKey: string) =>
const getMilestone = (milestoneKey: Milestone) =>
milestonesByName.get(milestoneKey);

const hasMilestone = (milestoneKey: string) =>
const hasMilestone = (milestoneKey: Milestone) =>
milestonesByName.has(milestoneKey);

const hasEveryMilestone = (milestoneKeys: string[]) =>
const hasEveryMilestone = (milestoneKeys: Milestone[]) =>
milestoneKeys.every((milestoneKey) => hasMilestone(milestoneKey));

return {
Expand Down
36 changes: 36 additions & 0 deletions src/testUtils/factories/milestoneFactories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2024 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { type Milestone } from "@/data/model/UserMilestone";

let milestoneId = 0;

/**
* Assume a string is a milestone for testing.
*/
function UNSAFE_assumeMilestone(milestone: string): Milestone {
return milestone as Milestone;
}

/**
* Test factory for creating feature flags.
* @param baseName optional base name for the milestone to improve test output readability.
*/
export function milestoneFactory(baseName = "milestone"): Milestone {
milestoneId++;
return UNSAFE_assumeMilestone([baseName, milestoneId].join("_"));
}
1 change: 1 addition & 0 deletions src/tsconfig.strictNullChecks.json
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,7 @@
"./testUtils/factories/marketplaceFactories.ts",
"./testUtils/factories/messengerFactories.ts",
"./testUtils/factories/metadataFactory.ts",
"./testUtils/factories/milestoneFactories.ts",
"./testUtils/factories/modComponentFactories.ts",
"./testUtils/factories/modDefinitionFactories.ts",
"./testUtils/factories/pageEditorFactories.ts",
Expand Down

0 comments on commit 29a8415

Please sign in to comment.