Skip to content

Commit

Permalink
#8131 Auto-deactivate unassigned mods on Mods Page (#8164)
Browse files Browse the repository at this point in the history
* refactor rename extensions -> activatedModComponents

* refactor rename uninstallAllDeployments -> deactivateAllDeployedMods

* refactor rename vars in deactivateAllDeployedMods

* refactor rename uninstallExtensionsAndSaveState -> deactivateModComponentsAndSaveState

* refactor rename vars in deactivateModComponentsAndSaveState

* reactor rename uninstallExtensinoFromStates -> deactivateModComponentFromStates

* refactor rename setExtensionsState -> saveModComponentStateAndReactivateTabs

* refactor rename uninstallUmnatchedDeployments -> deactivateUnassignedDeployments

* refactor rename uninstallRecipe -> deactivateMod

* refactor rename installDeployment -> activateDeployment

* refactor rename installDeployments -> activateDeployments

* refacto rename canAutomaticallyInstall -> canAutoActivate

* refactor rename vars in activateDeploymentsInBackground

* refactor clarify manual/auto activation logic in activateDeploymentsInBackground

* refactor consolidate resetUpdateTimestamp func

* replace straggling references to 'install' with 'activate'

* add test for changing mod id

* fix test inteference in deploymentUpdater

* add empty test file for activateDeployments

* fix name and docs for updatePromptTimestamp

* refactor integrate graham suggestion

* add empty test to DeploymentsContext

* add basic test body

* update deployment timestamps

* fix type errors

* fix registry id in test

* finish replication test

* refactor rename activateDeployment param installed -> activatedModComponents

* refactor rename activateDeployments param installed -> activatedModComponents

* refactor consolidate redundant prop

* refactor rename extension -> modComponent

* bugfix deactivate mods based on deloyment id instead of recipe id

* Refactor improve deploymentOverride spread

* refactor extract mockDeploymentActivationRequests

* add test for unmanaged activated mod becoming managed

* fix type errors

* fix modcomponentstate tyep errors

* misc refactors

* fix lint errors

* add restricted flags mock

* make fix for deactivate unmananged mod components

* remove deactivate unassigned

* remove todos in tests

* add unassignedModComponents to deploymentUpdateState

* refactor rename activeExtensions -> activeModComponents

* refactor rename installedExtensions -> activatedModComponents

* add unassignedModComponents to useAutoDeploy

* deactivate unassigned mod components in useAutoDeploy effect

* fix autoDeploy tests

* remove unnecessary async

* fix lint errors'

* add try catch to deactivateUnassignedModComponents

* fix lint errors

* create useDeactivateUnassignedDeploymentsEffect

* replace useAutoDeploy deactivateUnassignedModComponents with effect

* replace useAutoDeploy deactivateUnassignedModComponents with effect

* remove from strictnull
  • Loading branch information
mnholtz committed Apr 5, 2024
1 parent ef1dce2 commit b2b87d2
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 33 deletions.
47 changes: 33 additions & 14 deletions src/extensionConsole/pages/deployments/DeploymentsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,14 @@ import useAutoDeploy from "@/extensionConsole/pages/deployments/useAutoDeploy";
import { activateDeployments } from "@/extensionConsole/pages/deployments/activateDeployments";
import { useGetDeploymentsQuery } from "@/data/service/api";
import { fetchDeploymentModDefinitions } from "@/modDefinitions/modDefinitionRawApiCalls";
import { isEqual } from "lodash";
import { isEmpty, isEqual } from "lodash";
import useMemoCompare from "@/hooks/useMemoCompare";
import useDeriveAsyncState from "@/hooks/useDeriveAsyncState";
import type { Deployment } from "@/types/contract";
import useBrowserIdentifier from "@/hooks/useBrowserIdentifier";
import type { ActivatableDeployment } from "@/types/deploymentTypes";
import type { Permissions } from "webextension-polyfill";
import useDeactivateUnassignedDeploymentsEffect from "@/extensionConsole/pages/deployments/useDeactivateUnassignedDeploymentsEffect";

export type DeploymentsState = {
/**
Expand Down Expand Up @@ -94,10 +95,10 @@ export type DeploymentsState = {
function useDeployments(): DeploymentsState {
const dispatch = useDispatch<Dispatch>();
const { data: browserIdentifier } = useBrowserIdentifier();
const activeExtensions = useSelector(selectActivatedModComponents);
const activatedModComponents = useSelector(selectActivatedModComponents);
const { state: flagsState } = useFlags();
const activeDeployments = useMemoCompare<InstalledDeployment[]>(
selectInstalledDeployments(activeExtensions),
selectInstalledDeployments(activatedModComponents),
isEqual,
);

Expand All @@ -117,10 +118,20 @@ function useDeployments(): DeploymentsState {
deploymentsState,
flagsState,
async (deployments: Deployment[], { restrict }: Restrict) => {
const isUpdated = makeUpdatedFilter(activeExtensions, {
const isUpdated = makeUpdatedFilter(activatedModComponents, {
restricted: restrict("uninstall"),
});

const deployedModIds = new Set(
deployments.map((deployment) => deployment.package.package_id),
);

const unassignedModComponents = activatedModComponents.filter(
(activeModComponent) =>
!isEmpty(activeModComponent._deployment) &&
!deployedModIds.has(activeModComponent._recipe?.id),
);

const updatedDeployments = deployments.filter((x) => isUpdated(x));

const [activatableDeployments] = await Promise.all([
Expand Down Expand Up @@ -150,6 +161,7 @@ function useDeployments(): DeploymentsState {

return {
activatableDeployments,
unassignedModComponents,
extensionUpdateRequired: checkExtensionUpdateRequired(
activatableDeployments,
),
Expand All @@ -159,20 +171,27 @@ function useDeployments(): DeploymentsState {
);

// Fallback values for loading/error states
const { activatableDeployments, extensionUpdateRequired, permissions } =
deploymentUpdateState.data ?? {
// `useAutoDeploy` expects `null` to represent deployment loading state. It tries to activate once available
activatableDeployments: null as ActivatableDeployment[] | null,
extensionUpdateRequired: false as boolean,
permissions: [] as Permissions.Permissions,
};
const {
activatableDeployments,
unassignedModComponents,
extensionUpdateRequired,
permissions,
} = deploymentUpdateState.data ?? {
// `useAutoDeploy` expects `null` to represent deployment loading state. It tries to activate once available
activatableDeployments: null as ActivatableDeployment[] | null,
extensionUpdateRequired: false as boolean,
unassignedModComponents: [],
permissions: [] as Permissions.Permissions,
};

const { isAutoDeploying } = useAutoDeploy({
activatableDeployments,
installedExtensions: activeExtensions,
activatedModComponents,
extensionUpdateRequired,
});

useDeactivateUnassignedDeploymentsEffect(unassignedModComponents);

const handleUpdateFromUserGesture = useCallback(async () => {
// IMPORTANT: can't do a fetch or any potentially stalling operation (including IDB calls) because the call to
// request permissions must occur within 5 seconds of the user gesture. ensurePermissionsFromUserGesture check
Expand Down Expand Up @@ -224,13 +243,13 @@ function useDeployments(): DeploymentsState {
await activateDeployments({
dispatch,
activatableDeployments,
activatedModComponents: activeExtensions,
activatedModComponents,
});
notify.success("Updated team deployments");
} catch (error) {
notify.error({ message: "Error updating team deployments", error });
}
}, [dispatch, activatableDeployments, permissions, activeExtensions]);
}, [dispatch, activatableDeployments, permissions, activatedModComponents]);

const updateExtension = useCallback(async () => {
await reloadIfNewVersionIsReady();
Expand Down
34 changes: 34 additions & 0 deletions src/extensionConsole/pages/deployments/activateDeployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,37 @@ export async function activateDeployments({
throw errors[0];
}
}

export function deactivateUnassignedModComponents({
dispatch,
unassignedModComponents,
}: {
dispatch: Dispatch;
unassignedModComponents: ModComponentBase[];
}) {
const deactivatedModComponents = [];

for (const modComponent of unassignedModComponents) {
try {
dispatch(
actions.removeExtension({
extensionId: modComponent.id,
}),
);
deactivatedModComponents.push(modComponent);
} catch (error) {
reportError(
new Error("Error deactivating unassigned mod component", {
cause: error,
}),
);
}
}

reportEvent(Events.DEPLOYMENT_DEACTIVATE_UNASSIGNED, {
auto: true,
deployments: deactivatedModComponents.map(
(modComponent) => modComponent._deployment.id,
),
});
}
30 changes: 15 additions & 15 deletions src/extensionConsole/pages/deployments/useAutoDeploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe("useAutoDeploy", () => {

const activatableDeployments: ActivatableDeployment[] | undefined =
undefined;
const installedExtensions = [
const activatedModComponents = [
modComponentFactory(),
modComponentFactory(),
];
Expand All @@ -66,7 +66,7 @@ describe("useAutoDeploy", () => {
const { result } = renderHook(() =>
useAutoDeploy({
activatableDeployments,
installedExtensions,
activatedModComponents,
extensionUpdateRequired,
}),
);
Expand All @@ -78,7 +78,7 @@ describe("useAutoDeploy", () => {
mockHooks();

const activatableDeployments: ActivatableDeployment[] = [];
const installedExtensions = [
const activatedModComponents = [
modComponentFactory(),
modComponentFactory(),
];
Expand All @@ -87,7 +87,7 @@ describe("useAutoDeploy", () => {
const { result } = renderHook(() =>
useAutoDeploy({
activatableDeployments,
installedExtensions,
activatedModComponents,
extensionUpdateRequired,
}),
);
Expand All @@ -107,7 +107,7 @@ describe("useAutoDeploy", () => {
const activatableDeployments: ActivatableDeployment[] = [
activatableDeploymentFactory(),
];
const installedExtensions = [
const activatedModComponents = [
modComponentFactory(),
modComponentFactory(),
];
Expand All @@ -116,7 +116,7 @@ describe("useAutoDeploy", () => {
const { result, waitForValueToChange } = renderHook(() =>
useAutoDeploy({
activatableDeployments,
installedExtensions,
activatedModComponents,
extensionUpdateRequired,
}),
);
Expand All @@ -125,7 +125,7 @@ describe("useAutoDeploy", () => {
expect(activateDeployments).toHaveBeenCalledWith({
dispatch: expect.any(Function),
activatableDeployments,
activatedModComponents: installedExtensions,
activatedModComponents,
});

await waitForValueToChange(() => result.current.isAutoDeploying);
Expand All @@ -138,7 +138,7 @@ describe("useAutoDeploy", () => {
const activatableDeployments: ActivatableDeployment[] = [
activatableDeploymentFactory(),
];
const installedExtensions = [
const activatedModComponents = [
modComponentFactory(),
modComponentFactory(),
];
Expand All @@ -147,7 +147,7 @@ describe("useAutoDeploy", () => {
const { result, waitForValueToChange } = renderHook(() =>
useAutoDeploy({
activatableDeployments,
installedExtensions,
activatedModComponents,
extensionUpdateRequired,
}),
);
Expand All @@ -171,7 +171,7 @@ describe("useAutoDeploy", () => {
const activatableDeployment: ActivatableDeployment =
activatableDeploymentFactory();

const installedExtensions = [
const activatedModComponents = [
modComponentFactory(),
modComponentFactory(),
];
Expand All @@ -181,7 +181,7 @@ describe("useAutoDeploy", () => {
({ activatableDeployments }) =>
useAutoDeploy({
activatableDeployments,
installedExtensions,
activatedModComponents,
extensionUpdateRequired,
}),
{
Expand All @@ -208,7 +208,7 @@ describe("useAutoDeploy", () => {
const activatableDeployments: ActivatableDeployment[] = [
activatableDeploymentFactory(),
];
const installedExtensions = [
const activatedModComponents = [
modComponentFactory(),
modComponentFactory(),
];
Expand All @@ -217,7 +217,7 @@ describe("useAutoDeploy", () => {
const { result } = renderHook(() =>
useAutoDeploy({
activatableDeployments,
installedExtensions,
activatedModComponents,
extensionUpdateRequired,
}),
);
Expand All @@ -231,7 +231,7 @@ describe("useAutoDeploy", () => {
const activatableDeployments: ActivatableDeployment[] = [
activatableDeploymentFactory(),
];
const installedExtensions = [
const activatedModComponents = [
modComponentFactory(),
modComponentFactory(),
];
Expand All @@ -240,7 +240,7 @@ describe("useAutoDeploy", () => {
const { result } = renderHook(() =>
useAutoDeploy({
activatableDeployments,
installedExtensions,
activatedModComponents,
extensionUpdateRequired,
}),
);
Expand Down
8 changes: 4 additions & 4 deletions src/extensionConsole/pages/deployments/useAutoDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ type UseAutoDeployReturn = {

function useAutoDeploy({
activatableDeployments,
installedExtensions,
activatedModComponents,
extensionUpdateRequired,
}: {
// Expects nullish value if activatableDeployments are uninitialized/not loaded yet
activatableDeployments: Nullishable<ActivatableDeployment[]>;
installedExtensions: ModComponentBase[];
activatedModComponents: ModComponentBase[];
extensionUpdateRequired: boolean;
}): UseAutoDeployReturn {
const dispatch = useDispatch<Dispatch>();
Expand All @@ -52,7 +52,7 @@ function useAutoDeploy({
] = useState(true);
// Only `true` while deployments are being activated. Prevents multiple activations from happening at once.
const [isActivationInProgress, setIsActivationInProgress] = useState(false);
const { hasPermissions } = useModPermissions(installedExtensions);
const { hasPermissions } = useModPermissions(activatedModComponents);
const { restrict } = useFlags();

/**
Expand Down Expand Up @@ -90,7 +90,7 @@ function useAutoDeploy({
await activateDeployments({
dispatch,
activatableDeployments,
activatedModComponents: installedExtensions,
activatedModComponents,
});
notify.success("Updated team deployments");
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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 { deactivateUnassignedModComponents } from "@/extensionConsole/pages/deployments/activateDeployments";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import type { Dispatch } from "@reduxjs/toolkit";
import { type ModComponentBase } from "@/types/modComponentTypes";

const useDeactivateUnassignedDeploymentsEffect = (
unassignedModComponents: ModComponentBase[],
) => {
const dispatch = useDispatch<Dispatch>();
useEffect(() => {
if (unassignedModComponents.length === 0) return;

deactivateUnassignedModComponents({
dispatch,
unassignedModComponents,
});
}, [unassignedModComponents]);
};

export default useDeactivateUnassignedDeploymentsEffect;

0 comments on commit b2b87d2

Please sign in to comment.