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

Refactor state management to useContext and useReducer #138

Merged
merged 18 commits into from
Nov 13, 2023
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
25 changes: 14 additions & 11 deletions src/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { GitNotFound } from "./screens/GitNotFound/GitNotFound";
import { LinkedProject } from "./screens/LinkProject/LinkedProject";
import { LinkingProjectFailed } from "./screens/LinkProject/LinkingProjectFailed";
import { LinkProject } from "./screens/LinkProject/LinkProject";
import { ControlsProvider } from "./screens/VisualTests/ControlsContext";
import { VisualTests } from "./screens/VisualTests/VisualTests";
import { GitInfoPayload, LocalBuildProgress, UpdateStatusFunction } from "./types";
import { client, Provider, useAccessToken } from "./utils/graphQLClient";
Expand Down Expand Up @@ -143,17 +144,19 @@ export const Panel = ({ active, api }: PanelProps) => {
return (
<Provider key={PANEL_ID} value={client}>
<Sections hidden={!active}>
<VisualTests
dismissBuildError={() => setLocalBuildProgress(undefined)}
localBuildProgress={localBuildIsRightBranch ? localBuildProgress : undefined}
startDevBuild={() => emit(START_BUILD)}
setAccessToken={setAccessToken}
setOutdated={setOutdated}
updateBuildStatus={updateBuildStatus}
projectId={projectId}
gitInfo={gitInfo}
storyId={storyId}
/>
<ControlsProvider>
<VisualTests
dismissBuildError={() => setLocalBuildProgress(undefined)}
localBuildProgress={localBuildIsRightBranch ? localBuildProgress : undefined}
startDevBuild={() => emit(START_BUILD)}
setAccessToken={setAccessToken}
setOutdated={setOutdated}
updateBuildStatus={updateBuildStatus}
projectId={projectId}
gitInfo={gitInfo}
storyId={storyId}
/>
</ControlsProvider>
</Sections>
</Provider>
);
Expand Down
5 changes: 3 additions & 2 deletions src/screens/Authentication/Authentication.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { findByRole, userEvent } from "@storybook/testing-library";
import { http, HttpResponse } from "msw";

import { panelModes } from "../../modes";
import { storyWrapper } from "../../utils/graphQLClient";
import { GraphQLClientProvider } from "../../utils/graphQLClient";
import { playAll } from "../../utils/playAll";
import { storyWrapper } from "../../utils/storyWrapper";
import { withFigmaDesign } from "../../utils/withFigmaDesign";
import { Authentication } from "./Authentication";

const meta = {
component: Authentication,
decorators: [storyWrapper],
decorators: [storyWrapper(GraphQLClientProvider)],
args: {
setAccessToken: action("setAccessToken"),
hasProjectId: false,
Expand Down
6 changes: 3 additions & 3 deletions src/screens/GitNotFound/GitNotFound.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";

import { storyWrapper } from "../../utils/graphQLClient";
import { GraphQLClientProvider } from "../../utils/graphQLClient";
import { storyWrapper } from "../../utils/storyWrapper";
import { GitNotFound } from "./GitNotFound";

const meta = {
component: GitNotFound,
decorators: [storyWrapper],
decorators: [storyWrapper(GraphQLClientProvider)],
args: {
gitInfoError: new Error("Git info not found"),
},
Expand Down
5 changes: 3 additions & 2 deletions src/screens/LinkProject/LinkProject.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { delay, graphql, HttpResponse } from "msw";

import { SelectProjectsQueryQuery } from "../../gql/graphql";
import { panelModes } from "../../modes";
import { storyWrapper } from "../../utils/graphQLClient";
import { GraphQLClientProvider } from "../../utils/graphQLClient";
import { playAll } from "../../utils/playAll";
import { storyWrapper } from "../../utils/storyWrapper";
import { withFigmaDesign } from "../../utils/withFigmaDesign";
import { LinkProject } from "./LinkProject";

const meta = {
component: LinkProject,
decorators: [storyWrapper],
decorators: [storyWrapper(GraphQLClientProvider)],
args: {
createdProjectId: undefined,
onUpdateProject: action("updateProject"),
Expand Down
5 changes: 3 additions & 2 deletions src/screens/LinkProject/LinkedProject.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { graphql, HttpResponse } from "msw";

import { ProjectQueryQuery } from "../../gql/graphql";
import { panelModes } from "../../modes";
import { storyWrapper } from "../../utils/graphQLClient";
import { GraphQLClientProvider } from "../../utils/graphQLClient";
import { storyWrapper } from "../../utils/storyWrapper";
import { withFigmaDesign } from "../../utils/withFigmaDesign";
import { LinkedProject } from "./LinkedProject";

Expand All @@ -22,7 +23,7 @@ const meta = {
goToNext: action("goToNext"),
setAccessToken: action("setAccessToken"),
},
decorators: [storyWrapper],
decorators: [storyWrapper(GraphQLClientProvider)],
parameters: {
chromatic: {
modes: panelModes,
Expand Down
5 changes: 3 additions & 2 deletions src/screens/Onboarding/Onboarding.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import type { Meta, StoryObj } from "@storybook/react";
import { graphql, HttpResponse } from "msw";

import { panelModes } from "../../modes";
import { storyWrapper } from "../../utils/graphQLClient";
import { GraphQLClientProvider } from "../../utils/graphQLClient";
import { storyWrapper } from "../../utils/storyWrapper";
import { withFigmaDesign } from "../../utils/withFigmaDesign";
import { Onboarding } from "./Onboarding";

const meta = {
component: Onboarding,
decorators: [storyWrapper],
decorators: [storyWrapper(GraphQLClientProvider)],
parameters: {
chromatic: {
modes: panelModes,
Expand Down
154 changes: 154 additions & 0 deletions src/screens/VisualTests/BuildContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React, { createContext, useEffect, useMemo } from "react";
import { useQuery } from "urql";

import { getFragment } from "../../gql";
import { StoryTestFieldsFragment, TestStatus } from "../../gql/graphql";
import { GitInfoPayload } from "../../types";
import { summarizeTests } from "../../utils/summarizeTests";
import { statusMap } from "../../utils/testsToStatusUpdate";
import { SelectedBuildInfo } from "../../utils/updateSelectedBuildInfo";
import { useRequiredContext } from "../../utils/useRequiredContext";
import { useTests } from "../../utils/useTests";
import { useControlsDispatch } from "./ControlsContext";
import {
FragmentLastBuildOnBranchBuildFields,
FragmentLastBuildOnBranchTestFields,
FragmentSelectedBuildFields,
FragmentStoryTestFields,
QueryBuild,
} from "./graphql";

export const useBuild = ({
projectId,
storyId,
gitInfo,
selectedBuildInfo,
}: {
projectId: string;
storyId: string;
gitInfo: Pick<
GitInfoPayload,
"branch" | "slug" | "userEmailHash" | "commit" | "committedAt" | "uncommittedHash"
>;
selectedBuildInfo?: SelectedBuildInfo;
}) => {
const [{ data, error: queryError, operation }, rerunQuery] = useQuery({
query: QueryBuild,
variables: {
projectId,
storyId,
testStatuses: Object.keys(statusMap) as any as TestStatus[],
branch: gitInfo.branch || "",
...(gitInfo.slug ? { repositoryOwnerName: gitInfo.slug.split("/", 1)[0] } : {}),
gitUserEmailHash: gitInfo.userEmailHash,
selectedBuildId: selectedBuildInfo?.buildId || "",
hasSelectedBuildId: !!selectedBuildInfo,
},
});

// Poll for updates
useEffect(() => {
const interval = setInterval(rerunQuery, 5000);
return () => clearInterval(interval);
}, [rerunQuery]);

// When you change story, for a period the query will return the previous set of data, and indicate
// that with the operation being for the previous query.
const storyDataIsStale = operation && storyId && operation.variables.storyId !== storyId;

const lastBuildOnBranch = getFragment(
FragmentLastBuildOnBranchBuildFields,
data?.project?.lastBuildOnBranch
);

const lastBuildOnBranchStoryTests = [
...getFragment(
FragmentLastBuildOnBranchTestFields,
lastBuildOnBranch && "testsForStory" in lastBuildOnBranch && lastBuildOnBranch.testsForStory
? lastBuildOnBranch.testsForStory.nodes
: []
),
];

// If the last build is *newer* than the current head commit, we don't want to select it
// as our local code wouldn't yet have the changes made in that build.
const lastBuildOnBranchIsNewer = lastBuildOnBranch?.committedAt > gitInfo.committedAt;
const lastBuildOnBranchIsSelectable = !!lastBuildOnBranch && !lastBuildOnBranchIsNewer;

// If any tests for the current story are still in progress, we aren't ready to select the build
const lastBuildOnBranchIsReady =
!!lastBuildOnBranch &&
lastBuildOnBranchStoryTests.every((t) => t.status !== TestStatus.InProgress);

// If we didn't explicitly select a build, select the last build on the branch (if any)
const selectedBuild = getFragment(
FragmentSelectedBuildFields,
data?.selectedBuild ?? (lastBuildOnBranchIsReady ? data?.project?.lastBuildOnBranch : undefined)
);

return {
hasData: !!data && !storyDataIsStale,
hasProject: !!data?.project,
hasSelectedBuild: selectedBuild?.branch === gitInfo.branch,
lastBuildOnBranch,
lastBuildOnBranchIsNewer,
lastBuildOnBranchIsReady,
lastBuildOnBranchIsSelectable,
selectedBuild,
selectedBuildMatchesGit:
selectedBuild?.branch === gitInfo.branch &&
selectedBuild?.commit === gitInfo.commit &&
selectedBuild?.uncommittedHash === gitInfo.uncommittedHash,
rerunQuery,
queryError,
userCanReview: !!data?.viewer?.projectMembership?.userCanReview,
};
};

type BuildInfo = ReturnType<typeof useBuild> | null;
type SelectedStory =
| ({
hasTests: boolean;
tests: StoryTestFieldsFragment[];
summary: ReturnType<typeof summarizeTests>;
} & ReturnType<typeof useTests>)
| null;

export const BuildContext = createContext<BuildInfo>(null);
export const StoryContext = createContext<SelectedStory>(null);

export const useBuildState = () => useRequiredContext(BuildContext, "Build");
export const useSelectedBuildState = () => {
const { selectedBuild } = useRequiredContext(BuildContext, "Build");
if (!selectedBuild) throw new Error("No selectedBuild on Build context");
return selectedBuild;
};
export const useSelectedStoryState = () => useRequiredContext(StoryContext, "Story");

export const BuildProvider = ({
children,
watchState = null,
}: {
children: React.ReactNode;
watchState?: BuildInfo;
}) => {
const hasTests = !!watchState?.selectedBuild && "testsForStory" in watchState.selectedBuild;
const testsForStory =
watchState?.selectedBuild &&
"testsForStory" in watchState.selectedBuild &&
watchState.selectedBuild.testsForStory?.nodes;
const tests = [...getFragment(FragmentStoryTestFields, testsForStory || [])];
const summary = summarizeTests(tests);

const { toggleDiff } = useControlsDispatch();
useEffect(() => toggleDiff(summary.changeCount > 0), [toggleDiff, summary.changeCount]);

return (
// eslint-disable-next-line react-hooks/exhaustive-deps
<BuildContext.Provider value={useMemo(() => watchState, [JSON.stringify(watchState)])}>
<StoryContext.Provider value={{ hasTests, tests, summary, ...useTests(tests) }}>
{children}
</StoryContext.Provider>
</BuildContext.Provider>
);
};
Loading