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

Use API to track when onboarding is completed or dismissed #192

Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 7 additions & 2 deletions src/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ const documents = {
"\n query VisualTestsProjectCountQuery {\n viewer {\n projectCount\n accounts {\n newProjectUrl\n }\n }\n }\n": types.VisualTestsProjectCountQueryDocument,
"\n query ProjectQuery($projectId: ID!) {\n project(id: $projectId) {\n id\n name\n webUrl\n lastBuild {\n branch\n number\n }\n }\n }\n": types.ProjectQueryDocument,
"\n query SelectProjectsQuery {\n viewer {\n accounts {\n id\n name\n avatarUrl\n newProjectUrl\n projects {\n id\n name\n webUrl\n lastBuild {\n branch\n number\n }\n }\n }\n }\n }\n": types.SelectProjectsQueryDocument,
"\n query AddonVisualTestsBuild(\n $projectId: ID!\n $branch: String!\n $gitUserEmailHash: String!\n $repositoryOwnerName: String\n $storyId: String!\n $testStatuses: [TestStatus!]!\n $selectedBuildId: ID!\n $hasSelectedBuildId: Boolean!\n ) {\n project(id: $projectId) {\n name\n lastBuildOnBranch: lastBuild(\n branches: [$branch]\n repositoryOwnerName: $repositoryOwnerName\n localBuilds: { localBuildEmailHash: $gitUserEmailHash }\n ) {\n ...LastBuildOnBranchBuildFields\n ...SelectedBuildFields @skip(if: $hasSelectedBuildId)\n }\n lastBuild {\n id\n slug\n branch\n }\n }\n selectedBuild: build(id: $selectedBuildId) @include(if: $hasSelectedBuildId) {\n ...SelectedBuildFields\n }\n viewer {\n projectMembership(projectId: $projectId) {\n userCanReview: meetsAccessLevel(minimumAccessLevel: REVIEWER)\n }\n }\n }\n": types.AddonVisualTestsBuildDocument,
"\n mutation UpdateUserPreferences($input: UserPreferencesInput!) {\n updateUserPreferences(input: $input) {\n updatedPreferences {\n vtaOnboarding\n }\n }\n }\n": types.UpdateUserPreferencesDocument,
"\n query AddonVisualTestsBuild(\n $projectId: ID!\n $branch: String!\n $gitUserEmailHash: String!\n $repositoryOwnerName: String\n $storyId: String!\n $testStatuses: [TestStatus!]!\n $selectedBuildId: ID!\n $hasSelectedBuildId: Boolean!\n ) {\n project(id: $projectId) {\n name\n lastBuildOnBranch: lastBuild(\n branches: [$branch]\n repositoryOwnerName: $repositoryOwnerName\n localBuilds: { localBuildEmailHash: $gitUserEmailHash }\n ) {\n ...LastBuildOnBranchBuildFields\n ...SelectedBuildFields @skip(if: $hasSelectedBuildId)\n }\n lastBuild {\n id\n slug\n branch\n }\n }\n selectedBuild: build(id: $selectedBuildId) @include(if: $hasSelectedBuildId) {\n ...SelectedBuildFields\n }\n viewer {\n preferences {\n vtaOnboarding\n }\n projectMembership(projectId: $projectId) {\n userCanReview: meetsAccessLevel(minimumAccessLevel: REVIEWER)\n }\n }\n }\n": types.AddonVisualTestsBuildDocument,
"\n fragment LastBuildOnBranchBuildFields on Build {\n __typename\n id\n status\n committedAt\n ... on StartedBuild {\n testsForStatus: tests(first: 1000, statuses: $testStatuses) {\n nodes {\n ...StatusTestFields\n }\n }\n testsForStory: tests(storyId: $storyId) {\n nodes {\n ...LastBuildOnBranchTestFields\n }\n }\n }\n ... on CompletedBuild {\n result\n testsForStatus: tests(first: 1000, statuses: $testStatuses) {\n nodes {\n ...StatusTestFields\n }\n }\n testsForStory: tests(storyId: $storyId) {\n nodes {\n ...LastBuildOnBranchTestFields\n }\n }\n }\n }\n": types.LastBuildOnBranchBuildFieldsFragmentDoc,
"\n fragment SelectedBuildFields on Build {\n __typename\n id\n number\n branch\n commit\n committedAt\n uncommittedHash\n status\n ... on StartedBuild {\n startedAt\n testsForStory: tests(storyId: $storyId) {\n nodes {\n ...StoryTestFields\n }\n }\n }\n ... on CompletedBuild {\n startedAt\n testsForStory: tests(storyId: $storyId) {\n nodes {\n ...StoryTestFields\n }\n }\n }\n }\n": types.SelectedBuildFieldsFragmentDoc,
"\n fragment StatusTestFields on Test {\n id\n status\n result\n story {\n storyId\n }\n }\n": types.StatusTestFieldsFragmentDoc,
Expand Down Expand Up @@ -54,7 +55,11 @@ export function graphql(source: "\n query SelectProjectsQuery {\n viewer {\n
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query AddonVisualTestsBuild(\n $projectId: ID!\n $branch: String!\n $gitUserEmailHash: String!\n $repositoryOwnerName: String\n $storyId: String!\n $testStatuses: [TestStatus!]!\n $selectedBuildId: ID!\n $hasSelectedBuildId: Boolean!\n ) {\n project(id: $projectId) {\n name\n lastBuildOnBranch: lastBuild(\n branches: [$branch]\n repositoryOwnerName: $repositoryOwnerName\n localBuilds: { localBuildEmailHash: $gitUserEmailHash }\n ) {\n ...LastBuildOnBranchBuildFields\n ...SelectedBuildFields @skip(if: $hasSelectedBuildId)\n }\n lastBuild {\n id\n slug\n branch\n }\n }\n selectedBuild: build(id: $selectedBuildId) @include(if: $hasSelectedBuildId) {\n ...SelectedBuildFields\n }\n viewer {\n projectMembership(projectId: $projectId) {\n userCanReview: meetsAccessLevel(minimumAccessLevel: REVIEWER)\n }\n }\n }\n"): (typeof documents)["\n query AddonVisualTestsBuild(\n $projectId: ID!\n $branch: String!\n $gitUserEmailHash: String!\n $repositoryOwnerName: String\n $storyId: String!\n $testStatuses: [TestStatus!]!\n $selectedBuildId: ID!\n $hasSelectedBuildId: Boolean!\n ) {\n project(id: $projectId) {\n name\n lastBuildOnBranch: lastBuild(\n branches: [$branch]\n repositoryOwnerName: $repositoryOwnerName\n localBuilds: { localBuildEmailHash: $gitUserEmailHash }\n ) {\n ...LastBuildOnBranchBuildFields\n ...SelectedBuildFields @skip(if: $hasSelectedBuildId)\n }\n lastBuild {\n id\n slug\n branch\n }\n }\n selectedBuild: build(id: $selectedBuildId) @include(if: $hasSelectedBuildId) {\n ...SelectedBuildFields\n }\n viewer {\n projectMembership(projectId: $projectId) {\n userCanReview: meetsAccessLevel(minimumAccessLevel: REVIEWER)\n }\n }\n }\n"];
export function graphql(source: "\n mutation UpdateUserPreferences($input: UserPreferencesInput!) {\n updateUserPreferences(input: $input) {\n updatedPreferences {\n vtaOnboarding\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateUserPreferences($input: UserPreferencesInput!) {\n updateUserPreferences(input: $input) {\n updatedPreferences {\n vtaOnboarding\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query AddonVisualTestsBuild(\n $projectId: ID!\n $branch: String!\n $gitUserEmailHash: String!\n $repositoryOwnerName: String\n $storyId: String!\n $testStatuses: [TestStatus!]!\n $selectedBuildId: ID!\n $hasSelectedBuildId: Boolean!\n ) {\n project(id: $projectId) {\n name\n lastBuildOnBranch: lastBuild(\n branches: [$branch]\n repositoryOwnerName: $repositoryOwnerName\n localBuilds: { localBuildEmailHash: $gitUserEmailHash }\n ) {\n ...LastBuildOnBranchBuildFields\n ...SelectedBuildFields @skip(if: $hasSelectedBuildId)\n }\n lastBuild {\n id\n slug\n branch\n }\n }\n selectedBuild: build(id: $selectedBuildId) @include(if: $hasSelectedBuildId) {\n ...SelectedBuildFields\n }\n viewer {\n preferences {\n vtaOnboarding\n }\n projectMembership(projectId: $projectId) {\n userCanReview: meetsAccessLevel(minimumAccessLevel: REVIEWER)\n }\n }\n }\n"): (typeof documents)["\n query AddonVisualTestsBuild(\n $projectId: ID!\n $branch: String!\n $gitUserEmailHash: String!\n $repositoryOwnerName: String\n $storyId: String!\n $testStatuses: [TestStatus!]!\n $selectedBuildId: ID!\n $hasSelectedBuildId: Boolean!\n ) {\n project(id: $projectId) {\n name\n lastBuildOnBranch: lastBuild(\n branches: [$branch]\n repositoryOwnerName: $repositoryOwnerName\n localBuilds: { localBuildEmailHash: $gitUserEmailHash }\n ) {\n ...LastBuildOnBranchBuildFields\n ...SelectedBuildFields @skip(if: $hasSelectedBuildId)\n }\n lastBuild {\n id\n slug\n branch\n }\n }\n selectedBuild: build(id: $selectedBuildId) @include(if: $hasSelectedBuildId) {\n ...SelectedBuildFields\n }\n viewer {\n preferences {\n vtaOnboarding\n }\n projectMembership(projectId: $projectId) {\n userCanReview: meetsAccessLevel(minimumAccessLevel: REVIEWER)\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
48 changes: 45 additions & 3 deletions src/gql/graphql.ts

Large diffs are not rendered by default.

38 changes: 30 additions & 8 deletions src/gql/public-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1556,6 +1556,7 @@ type User implements Node {
username: String!
avatarUrl: URL
accounts: [Account!]!
preferences: UserPreferences!

"""The number of projects this user is a member of."""
projectCount: Int!
Expand All @@ -1568,6 +1569,16 @@ type User implements Node {
updatedAt: DateTime!
}

type UserPreferences {
vtaOnboarding: VTAOnboardingPreference!
}

enum VTAOnboardingPreference {
COMPLETED
DISMISSED
UNSET
}

type ProjectMembership {
"""GraphQL node identifier"""
id: ID!
Expand Down Expand Up @@ -1653,21 +1664,17 @@ enum ComponentsOrderField {
}

type Mutation {
createCLIToken(projectId: String!): String!
reviewTest(input: ReviewTestInput!): ReviewTestPayload
createFigmaMetadata(key: String!, url: String!, metadata: JSONObject!): FigmaMetadata
bulkCreateFigmaMetadata(input: [CreateFigmaMetadataInput!]!): [FigmaMetadata!]!
removeFigmaMetadata(id: ID!): FigmaMetadata
bulkRemoveFigmaMetadata(ids: [ID!]!): [FigmaMetadata!]!
reviewTest(input: ReviewTestInput!): ReviewTestPayload
}

input CreateFigmaMetadataInput {
key: String!
url: String!
metadata: JSONObject!
updateUserPreferences(input: UserPreferencesInput!): UpdateUserPreferencesPayload
}

type ReviewTestPayload {
"""The test(s) that were updated, if succesful."""
"""The test(s) that were updated, if successful."""
updatedTests: [Test!]

"""
Expand Down Expand Up @@ -1726,3 +1733,18 @@ enum ReviewTestBatch {
COMPONENT
SPEC
}

input CreateFigmaMetadataInput {
key: String!
url: String!
metadata: JSONObject!
}

type UpdateUserPreferencesPayload {
"""The updated preferences, if successful."""
updatedPreferences: UserPreferences
}

input UserPreferencesInput {
vtaOnboarding: VTAOnboardingPreference
}
4 changes: 1 addition & 3 deletions src/screens/GuidedTour/GuidedTour.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { type API } from "@storybook/manager-api";
import { useTheme } from "@storybook/theming";
import React, { useEffect } from "react";
import Joyride, { CallBackProps } from "react-joyride";
import Joyride from "react-joyride";
import { gql } from "urql";

import { PANEL_ID } from "../../constants";
import { SelectedBuildFieldsFragment } from "../../gql/graphql";
import { ENABLE_FILTER } from "../../SidebarBottom";
import { useSelectedStoryState } from "../VisualTests/BuildContext";
import { Confetti } from "./Confetti";
import { PulsatingEffect } from "./PulsatingEffect";
import { Tooltip, TooltipProps } from "./Tooltip";

const ProjectQuery = gql`
Expand Down
1 change: 1 addition & 0 deletions src/screens/VisualTests/BuildContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const useBuild = ({
rerunQuery,
queryError,
userCanReview: !!data?.viewer?.projectMembership?.userCanReview,
vtaOnboarding: data?.viewer?.preferences?.vtaOnboarding,
};
};

Expand Down
87 changes: 55 additions & 32 deletions src/screens/VisualTests/VisualTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import { useMutation } from "urql";

// TODO: Remove this after completing AP-3586
// import { WALKTHROUGH_COMPLETED_KEY } from "../../constants";
import { getFragment } from "../../gql";
import { getFragment, graphql } from "../../gql";
import {
BuildStatus,
ReviewTestBatch,
ReviewTestInputStatus,
Test,
TestResult,
TestStatus,
VtaOnboardingPreference,
} from "../../gql/graphql";
import { GitInfoPayload, LocalBuildProgress, UpdateStatusFunction } from "../../types";
import { testsToStatusUpdate } from "../../utils/testsToStatusUpdate";
Expand Down Expand Up @@ -95,37 +94,56 @@ const useReview = ({
return { isReviewing, acceptTest, unacceptTest, buildIsReviewable, userCanReview };
};

const MutationUpdateUserPreferences = graphql(/* GraphQL */ `
mutation UpdateUserPreferences($input: UserPreferencesInput!) {
updateUserPreferences(input: $input) {
updatedPreferences {
vtaOnboarding
}
}
}
`);

const useOnboarding = (
{ lastBuildOnBranch }: ReturnType<typeof useBuild>,
{ lastBuildOnBranch, vtaOnboarding }: ReturnType<typeof useBuild>,
managerApi?: Pick<API, "getUrlState">
) => {
const [hasCompletedOnboarding, setHasCompletedOnboarding] = React.useState(false);
const completeOnboarding = React.useCallback(() => {
setHasCompletedOnboarding(true);
}, []);

const [hasCompletedWalkthrough, setHasCompletedWalkthrough] = React.useState(() => {
// Force the onboarding to show by adding ?vtaOnboarding=true to the URL
const force = managerApi?.getUrlState?.().queryParams.vtaOnboarding === "true";
// Only using force instead of localStorage. WALKTHROUGH_COMPLETED flag will be moved to user model in AP-3586
return !force; // && localStorage.getItem(WALKTHROUGH_COMPLETED_KEY) === "true";
});
const completeOnboarding = React.useCallback(() => setHasCompletedOnboarding(true), []);

const [walkthroughInProgress, setWalkthroughInProgress] = React.useState(false);
const startWalkthrough = React.useCallback(() => {
setWalkthroughInProgress(true);
}, []);

const completeWalkthrough = React.useCallback(() => {
setHasCompletedWalkthrough(true);
// TODO: Replace with user model mutation in AP-3586
// localStorage.setItem(WALKTHROUGH_COMPLETED_KEY, "true");
setWalkthroughInProgress(false);
// remove onboarding query parameter from current url
const url = new URL(window.location.href);
url.searchParams.delete("vtaOnboarding");
window.history.replaceState({}, "", url.href);
}, []);
const startWalkthrough = React.useCallback(() => setWalkthroughInProgress(true), []);

const [hasCompletedWalkthrough, setHasCompletedWalkthrough] = React.useState(true);
React.useEffect(() => {
setHasCompletedWalkthrough(
// Force the onboarding to show by adding ?vtaOnboarding=true to the URL
managerApi?.getUrlState?.().queryParams.vtaOnboarding === "true" ||
vtaOnboarding === VtaOnboardingPreference.Completed ||
vtaOnboarding === VtaOnboardingPreference.Dismissed
);
}, [managerApi, vtaOnboarding]);

const [{ fetching: isUpdating }, runMutation] = useMutation(MutationUpdateUserPreferences);

const exitWalkthrough = React.useCallback(
async (completed: boolean) => {
const preference = completed
? VtaOnboardingPreference.Completed
: VtaOnboardingPreference.Dismissed;
await runMutation({ input: { vtaOnboarding: preference } });

setHasCompletedWalkthrough(true);
setWalkthroughInProgress(false);

const url = new URL(window.location.href);
if (url.searchParams.has("vtaOnboarding")) {
url.searchParams.delete("vtaOnboarding");
window.history.replaceState({}, "", url.href);
}
},
[runMutation]
);

const lastBuildHasChanges = React.useMemo(() => {
// select only testsForStatus (or empty array) and return true if any of them are pending and changed
Expand All @@ -148,9 +166,12 @@ const useOnboarding = (
showOnboarding,
showGuidedTour: !showOnboarding && !hasCompletedWalkthrough,
completeOnboarding,
completeWalkthrough,
skipOnboarding: React.useCallback(() => exitWalkthrough(false), [exitWalkthrough]),
completeWalkthrough: React.useCallback(() => exitWalkthrough(true), [exitWalkthrough]),
skipWalkthrough: React.useCallback(() => exitWalkthrough(false), [exitWalkthrough]),
startWalkthrough,
lastBuildHasChanges,
isUpdating,
};
};

Expand Down Expand Up @@ -259,11 +280,13 @@ export const VisualTestsWithoutSelectedBuildId = ({
showGuidedTour,
completeOnboarding,
completeWalkthrough,
skipOnboarding,
skipWalkthrough,
startWalkthrough,
lastBuildHasChanges,
} = useOnboarding(buildInfo, managerApi);

if (showOnboarding) {
if (showOnboarding && hasProject) {
return (
<>
{/* Don't render onboarding until data has loaded to allow initial build logic ot work. */}
Expand All @@ -280,7 +303,7 @@ export const VisualTestsWithoutSelectedBuildId = ({
localBuildProgress,
showInitialBuildScreen: !selectedBuild,
onComplete: completeOnboarding,
onSkip: completeWalkthrough,
onSkip: skipOnboarding,
lastBuildHasChanges,
}}
/>
Expand Down Expand Up @@ -330,7 +353,7 @@ export const VisualTestsWithoutSelectedBuildId = ({
<BuildProvider watchState={{ selectedBuild }}>
<GuidedTour
managerApi={managerApi}
skipWalkthrough={completeWalkthrough}
skipWalkthrough={skipWalkthrough}
startWalkthrough={startWalkthrough}
completeWalkthrough={completeWalkthrough}
/>
Expand Down
3 changes: 3 additions & 0 deletions src/screens/VisualTests/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const QueryBuild = graphql(/* GraphQL */ `
...SelectedBuildFields
}
viewer {
preferences {
vtaOnboarding
}
projectMembership(projectId: $projectId) {
userCanReview: meetsAccessLevel(minimumAccessLevel: REVIEWER)
}
Expand Down
Loading