chore: revamped the analytics for cycle and module in peek view. (#7075)

* chore: added cycles and modules in analytics peek view

* chore: added cycles and modules analytics

* chore: added project filter for work items

* chore: added a peekview flag and based on that table columns

* chore: added peek view

* chore: added check for display name

* chore: cleaned up some code

* chore: fixed export csv data

* chore: added distinct work items

* chore: assignee in peek view

* updated csv fields

* chore: updated workitems peek with assignee

* fix: removed type assersions for workspaceslug

* chore: added day wise filter in cycles and modules

* chore: added extra validations

---------

Co-authored-by: JayashTripathy <jayashtripathy371@gmail.com>
This commit is contained in:
Bavisetti Narayan 2025-05-17 17:11:26 +05:30 committed by GitHub
parent ba158d5d6e
commit 5b776392bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 642 additions and 236 deletions

View file

@ -21,7 +21,7 @@ export const InsightTable = <T extends Exclude<TAnalyticsTabsV2Base, "overview">
const { data, isLoading, columns, columnsLabels } = props;
const params = useParams();
const { t } = useTranslation();
const workspaceSlug = params.workspaceSlug as string;
const workspaceSlug = params.workspaceSlug.toString();
if (isLoading) {
return <TableLoader columns={columns} rows={5} />;
}
@ -35,7 +35,7 @@ export const InsightTable = <T extends Exclude<TAnalyticsTabsV2Base, "overview">
const exportCSV = (rows: Row<AnalyticsTableDataMap[T]>[]) => {
const rowData: any = rows.map((row) => {
const { project_id, ...exportableData } = row.original;
const { project_id, avatar_url, assignee_id, ...exportableData } = row.original;
return Object.fromEntries(
Object.entries(exportableData).map(([key, value]) => {
if (columnsLabels?.[key]) {

View file

@ -26,16 +26,20 @@ const analyticsV2Service = new AnalyticsV2Service();
const ProjectInsights = observer(() => {
const params = useParams();
const { t } = useTranslation();
const workspaceSlug = params.workspaceSlug as string;
const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2();
const workspaceSlug = params.workspaceSlug.toString();
const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } =
useAnalyticsV2();
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-radar" });
const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR(
`radar-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}`,
`radar-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
() =>
analyticsV2Service.getAdvanceAnalyticsCharts<TChartData<string, string>[]>(workspaceSlug, "projects", {
// date_filter: selectedDuration,
...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }),
...(selectedCycle ? { cycle_id: selectedCycle } : {}),
...(selectedModule ? { module_id: selectedModule } : {}),
...(isPeekView ? { peek_view: true } : {}),
})
);

View file

@ -18,16 +18,20 @@ const analyticsV2Service = new AnalyticsV2Service();
const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base; peekView?: boolean }> = observer(
({ analyticsType, peekView }) => {
const params = useParams();
const workspaceSlug = params.workspaceSlug as string;
const workspaceSlug = params.workspaceSlug.toString();
const { t } = useTranslation();
const { selectedDuration, selectedProjects, selectedDurationLabel } = useAnalyticsV2();
const { selectedDuration, selectedProjects, selectedDurationLabel, selectedCycle, selectedModule, isPeekView } =
useAnalyticsV2();
const { data: totalInsightsData, isLoading } = useSWR(
`total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}`,
`total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
() =>
analyticsV2Service.getAdvanceAnalytics<IAnalyticsResponseV2>(workspaceSlug, analyticsType, {
// date_filter: selectedDuration,
...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}),
...(selectedCycle ? { cycle_id: selectedCycle } : {}),
...(selectedModule ? { module_id: selectedModule } : {}),
...(isPeekView ? { peek_view: true } : {}),
})
);
return (

View file

@ -19,17 +19,21 @@ import { ChartLoader } from "../loaders";
const analyticsV2Service = new AnalyticsV2Service();
const CreatedVsResolved = observer(() => {
const { selectedDuration, selectedDurationLabel, selectedProjects } = useAnalyticsV2();
const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } =
useAnalyticsV2();
const params = useParams();
const { t } = useTranslation();
const workspaceSlug = params.workspaceSlug as string;
const workspaceSlug = params.workspaceSlug.toString();
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-area" });
const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR(
`created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}`,
`created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
() =>
analyticsV2Service.getAdvanceAnalyticsCharts<IChartResponseV2>(workspaceSlug, "work-items", {
// date_filter: selectedDuration,
...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }),
...(selectedCycle ? { cycle_id: selectedCycle } : {}),
...(selectedModule ? { module_id: selectedModule } : {}),
...(isPeekView ? { peek_view: true } : {}),
})
);
const parsedData: TChartData<string, string>[] = useMemo(() => {

View file

@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { Tab } from "@headlessui/react";
// plane package imports
import { IProject } from "@plane/types";
import { ICycle, IModule, IProject } from "@plane/types";
import { Spinner } from "@plane/ui";
// hooks
import { useAnalyticsV2 } from "@/hooks/store";
@ -15,20 +15,52 @@ import WorkItemsInsightTable from "../workitems-insight-table";
type Props = {
fullScreen: boolean;
projectDetails: IProject | undefined;
cycleDetails: ICycle | undefined;
moduleDetails: IModule | undefined;
};
export const WorkItemsModalMainContent: React.FC<Props> = observer((props) => {
const { projectDetails, fullScreen } = props;
const { updateSelectedProjects } = useAnalyticsV2();
const [isProjectConfigured, setIsProjectConfigured] = useState(false);
const { projectDetails, cycleDetails, moduleDetails, fullScreen } = props;
const { updateSelectedProjects, updateSelectedCycle, updateSelectedModule, updateIsPeekView } = useAnalyticsV2();
const [isModalConfigured, setIsModalConfigured] = useState(false);
useEffect(() => {
if (!projectDetails?.id) return;
updateSelectedProjects([projectDetails?.id ?? ""]);
setIsProjectConfigured(true);
}, [projectDetails?.id, updateSelectedProjects]);
updateIsPeekView(true);
if (!isProjectConfigured)
// Handle project selection
if (projectDetails?.id) {
updateSelectedProjects([projectDetails.id]);
}
// Handle cycle selection
if (cycleDetails?.id) {
updateSelectedCycle(cycleDetails.id);
}
// Handle module selection
if (moduleDetails?.id) {
updateSelectedModule(moduleDetails.id);
}
setIsModalConfigured(true);
// Cleanup fields
return () => {
updateSelectedProjects([]);
updateSelectedCycle("");
updateSelectedModule("");
updateIsPeekView(false);
};
}, [
projectDetails?.id,
cycleDetails?.id,
moduleDetails?.id,
updateSelectedProjects,
updateSelectedCycle,
updateSelectedModule,
updateIsPeekView,
]);
if (!isModalConfigured)
return (
<div className="flex h-full items-center justify-center">
<Spinner />

View file

@ -1,21 +1,26 @@
import { observer } from "mobx-react";
// icons
// plane package imports
import { Expand, Shrink, X } from "lucide-react";
import { ICycle, IModule } from "@plane/types";
// icons
type Props = {
fullScreen: boolean;
handleClose: () => void;
setFullScreen: React.Dispatch<React.SetStateAction<boolean>>;
title: string;
cycle?: ICycle;
module?: IModule;
};
export const WorkItemsModalHeader: React.FC<Props> = observer((props) => {
const { fullScreen, handleClose, setFullScreen, title } = props;
const { fullScreen, handleClose, setFullScreen, title, cycle, module } = props;
return (
<div className="flex items-center justify-between gap-4 bg-custom-background-100 px-5 py-4 text-sm">
<h3 className="break-words">Analytics for {title}</h3>
<h3 className="break-words">
Analytics for {title} {cycle && `in ${cycle.name}`} {module && `in ${module.name}`}
</h3>
<div className="flex items-center gap-2">
<button
type="button"

View file

@ -2,7 +2,7 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
import { Dialog, Transition } from "@headlessui/react";
// plane package imports
import { IProject } from "@plane/types";
import { ICycle, IModule, IProject } from "@plane/types";
// plane web components
import { WorkItemsModalMainContent } from "./content";
import { WorkItemsModalHeader } from "./header";
@ -11,10 +11,12 @@ type Props = {
isOpen: boolean;
onClose: () => void;
projectDetails?: IProject | undefined;
cycleDetails?: ICycle | undefined;
moduleDetails?: IModule | undefined;
};
export const WorkItemsModal: React.FC<Props> = observer((props) => {
const { isOpen, onClose, projectDetails } = props;
const { isOpen, onClose, projectDetails, moduleDetails, cycleDetails } = props;
const [fullScreen, setFullScreen] = useState(false);
@ -51,8 +53,15 @@ export const WorkItemsModal: React.FC<Props> = observer((props) => {
handleClose={handleClose}
setFullScreen={setFullScreen}
title={projectDetails?.name ?? ""}
cycle={cycleDetails}
module={moduleDetails}
/>
<WorkItemsModalMainContent
fullScreen={fullScreen}
projectDetails={projectDetails}
cycleDetails={cycleDetails}
moduleDetails={moduleDetails}
/>
<WorkItemsModalMainContent fullScreen={fullScreen} projectDetails={projectDetails} />
</div>
</div>
</Dialog.Panel>

View file

@ -46,19 +46,23 @@ const PriorityChart = observer((props: Props) => {
const { t } = useTranslation();
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-bar" });
// store hooks
const { selectedDuration, selectedProjects } = useAnalyticsV2();
const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView } = useAnalyticsV2();
const { workspaceStates } = useProjectState();
const { resolvedTheme } = useTheme();
// router
const params = useParams();
const workspaceSlug = params.workspaceSlug as string;
const workspaceSlug = params.workspaceSlug.toString();
const { data: priorityChartData, isLoading: priorityChartLoading } = useSWR(
`customized-insights-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${props.x_axis}-${props.y_axis}-${props.group_by}`,
`customized-insights-chart-${workspaceSlug}-${selectedDuration}-
${selectedProjects}-${selectedCycle}-${selectedModule}-${props.x_axis}-${props.y_axis}-${props.group_by}-${isPeekView}`,
() =>
analyticsV2Service.getAdvanceAnalyticsCharts<TChart>(workspaceSlug, "custom-work-items", {
// date_filter: selectedDuration,
...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }),
...(selectedCycle ? { cycle_id: selectedCycle } : {}),
...(selectedModule ? { module_id: selectedModule } : {}),
...(isPeekView ? { peek_view: true } : {}),
...props,
})
);
@ -158,10 +162,23 @@ const PriorityChart = observer((props: Props) => {
});
const exportCSV = (rows: Row<TChartDatum>[]) => {
const rowData = rows.map((row) => ({
name: row.original.name,
count: row.original.count,
}));
const rowData = rows.map((row) => {
const hiddenFields = ["key", "avatar_url", "assignee_id", "project_id"];
const otherFields = Object.keys(row.original).filter(
(key) => key !== "name" && key !== "count" && !hiddenFields.includes(key) && !key.includes("id")
);
return {
name: row.original.name,
count: row.original.count,
...otherFields.reduce(
(acc, key) => {
acc[parsedData?.schema[key] ?? key] = row.original[key];
return acc;
},
{} as Record<string, string | number>
),
};
});
const csv = generateCsv(csvConfig)(rowData);
download(csvConfig)(csv);
};

View file

@ -1,13 +1,15 @@
import { useMemo } from "react";
import { ColumnDef } from "@tanstack/react-table";
import { ColumnDef, Row } from "@tanstack/react-table";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
import { Briefcase } from "lucide-react";
import { Briefcase, UserRound } from "lucide-react";
// plane package imports
import { useTranslation } from "@plane/i18n";
import { WorkItemInsightColumns, AnalyticsTableDataMap } from "@plane/types";
// plane web components
import { Avatar } from "@plane/ui";
import { getFileURL } from "@plane/utils";
import { Logo } from "@/components/common/logo";
// hooks
import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2";
@ -21,44 +23,85 @@ const analyticsV2Service = new AnalyticsV2Service();
const WorkItemsInsightTable = observer(() => {
// router
const params = useParams();
const workspaceSlug = params.workspaceSlug as string;
const workspaceSlug = params.workspaceSlug.toString();
const { t } = useTranslation();
// store hooks
const { getProjectById } = useProject();
const { selectedDuration, selectedProjects } = useAnalyticsV2();
const { selectedDuration, selectedProjects, selectedCycle, selectedModule, isPeekView } = useAnalyticsV2();
const { data: workItemsData, isLoading } = useSWR(
`insights-table-work-items-${workspaceSlug}-${selectedDuration}-${selectedProjects}`,
`insights-table-work-items-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
() =>
analyticsV2Service.getAdvanceAnalyticsStats<WorkItemInsightColumns[]>(workspaceSlug, "work-items", {
// date_filter: selectedDuration,
...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}),
...(selectedCycle ? { cycle_id: selectedCycle } : {}),
...(selectedModule ? { module_id: selectedModule } : {}),
...(isPeekView ? { peek_view: true } : {}),
})
);
// derived values
const columnsLabels: Record<string, string> = {
backlog_work_items: t("workspace_projects.state.backlog"),
started_work_items: t("workspace_projects.state.started"),
un_started_work_items: t("workspace_projects.state.unstarted"),
completed_work_items: t("workspace_projects.state.completed"),
cancelled_work_items: t("workspace_projects.state.cancelled"),
project__name: t("common.project"),
};
const columnsLabels = useMemo(
() => ({
backlog_work_items: t("workspace_projects.state.backlog"),
started_work_items: t("workspace_projects.state.started"),
un_started_work_items: t("workspace_projects.state.unstarted"),
completed_work_items: t("workspace_projects.state.completed"),
cancelled_work_items: t("workspace_projects.state.cancelled"),
project__name: t("common.project"),
display_name: t("common.assignee"),
}),
[t]
);
const columns = useMemo(
() =>
[
{
accessorKey: "project__name",
header: () => <div className="text-left">{columnsLabels["project__name"]}</div>,
cell: ({ row }) => {
const project = getProjectById(row.original.project_id);
return (
<div className="flex items-center gap-2">
{project?.logo_props ? <Logo logo={project.logo_props} size={18} /> : <Briefcase className="h-4 w-4" />}
{project?.name}
</div>
);
},
},
!isPeekView
? {
accessorKey: "project__name",
header: () => <div className="text-left">{columnsLabels["project__name"]}</div>,
cell: ({ row }) => {
const project = getProjectById(row.original.project_id);
return (
<div className="flex items-center gap-2">
{project?.logo_props ? (
<Logo logo={project.logo_props} size={18} />
) : (
<Briefcase className="h-4 w-4" />
)}
{project?.name}
</div>
);
},
}
: {
accessorKey: "display_name",
header: () => <div className="text-left">{columnsLabels["display_name"]}</div>,
cell: ({ row }: { row: Row<WorkItemInsightColumns> }) => (
<div className="text-left">
<div className="flex items-center gap-2">
{row.original.avatar_url && row.original.avatar_url !== "" ? (
<Avatar
name={row.original.display_name}
src={getFileURL(row.original.avatar_url)}
size={24}
shape="circle"
/>
) : (
<div className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-custom-background-80 capitalize overflow-hidden">
{row.original.display_name ? (
row.original.display_name?.[0]
) : (
<UserRound className="text-custom-text-200 " size={12} />
)}
</div>
)}
<span className="break-words text-custom-text-200">
{row.original.display_name ?? t(`Unassigned`)}
</span>
</div>
</div>
),
},
{
accessorKey: "backlog_work_items",
header: () => <div className="text-right">{columnsLabels["backlog_work_items"]}</div>,
@ -85,7 +128,7 @@ const WorkItemsInsightTable = observer(() => {
cell: ({ row }) => <div className="text-right">{row.original.cancelled_work_items}</div>,
},
] as ColumnDef<AnalyticsTableDataMap["work-items"]>[],
[getProjectById]
[columnsLabels, getProjectById, isPeekView, t]
);
return (

View file

@ -0,0 +1,107 @@
"use client";
import React, { Fragment } from "react";
import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
import { Tab } from "@headlessui/react";
// plane package imports
import { ANALYTICS_TABS, EUserPermissionsLevel, EUserPermissions } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Header, EHeaderVariant } from "@plane/ui";
// components
import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics";
import { PageHead } from "@/components/core";
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
// hooks
import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
const OldAnalyticsPage = observer(() => {
const searchParams = useSearchParams();
const analytics_tab = searchParams.get("analytics_tab");
// plane imports
const { t } = useTranslation();
// store hooks
const { toggleCreateProjectModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { workspaceProjectIds, loader } = useProject();
const { currentWorkspace } = useWorkspace();
const { allowPermissions } = useUserPermissions();
// helper hooks
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" });
// derived values
const pageTitle = currentWorkspace?.name
? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name })
: undefined;
// permissions
const canPerformEmptyStateActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
// TODO: refactor loader implementation
return (
<>
<PageHead title={pageTitle} />
{workspaceProjectIds && (
<>
{workspaceProjectIds.length > 0 || loader === "init-loader" ? (
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
<Header variant={EHeaderVariant.SECONDARY}>
<Tab.List as="div" className="flex space-x-2 h-full">
{ANALYTICS_TABS.map((tab) => (
<Tab key={tab.key} as={Fragment}>
{({ selected }) => (
<button
className={`text-sm group relative flex items-center gap-1 h-full px-3 cursor-pointer transition-all font-medium outline-none ${
selected ? "text-custom-primary-100 " : "hover:text-custom-text-200"
}`}
>
{t(tab.i18n_title)}
<div
className={`border absolute bottom-0 right-0 left-0 rounded-t-md ${selected ? "border-custom-primary-100" : "border-transparent group-hover:border-custom-border-200"}`}
/>
</button>
)}
</Tab>
))}
</Tab.List>
</Header>
<Tab.Panels as={Fragment}>
<Tab.Panel as={Fragment}>
<ScopeAndDemand fullScreen />
</Tab.Panel>
<Tab.Panel as={Fragment}>
<CustomAnalytics fullScreen />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
) : (
<DetailedEmptyState
title={t("workspace_analytics.empty_state.general.title")}
description={t("workspace_analytics.empty_state.general.description")}
assetPath={resolvedPath}
customPrimaryButton={
<ComicBoxButton
label={t("workspace_analytics.empty_state.general.primary_button.text")}
title={t("workspace_analytics.empty_state.general.primary_button.comic.title")}
description={t("workspace_analytics.empty_state.general.primary_button.comic.description")}
onClick={() => {
setTrackElement("Analytics empty state");
toggleCreateProjectModal(true);
}}
disabled={!canPerformEmptyStateActions}
/>
}
/>
)}
</>
)}
</>
);
});
export default OldAnalyticsPage;

View file

@ -10,6 +10,9 @@ export interface IAnalyticsStoreV2 {
currentTab: TAnalyticsTabsV2Base;
selectedProjects: string[];
selectedDuration: DurationType;
selectedCycle: string;
selectedModule: string;
isPeekView?: boolean;
//computed
selectedDurationLabel: DurationType | null;
@ -17,25 +20,36 @@ export interface IAnalyticsStoreV2 {
//actions
updateSelectedProjects: (projects: string[]) => void;
updateSelectedDuration: (duration: DurationType) => void;
updateSelectedCycle: (cycle: string) => void;
updateSelectedModule: (module: string) => void;
updateIsPeekView: (isPeekView: boolean) => void;
}
export class AnalyticsStoreV2 implements IAnalyticsStoreV2 {
//observables
currentTab: TAnalyticsTabsV2Base = "overview";
selectedProjects: DurationType[] = [];
selectedProjects: string[] = [];
selectedDuration: DurationType = "last_30_days";
constructor(_rootStore: CoreRootStore) {
selectedCycle: string = "";
selectedModule: string = "";
isPeekView: boolean = false;
constructor() {
makeObservable(this, {
// observables
currentTab: observable.ref,
selectedDuration: observable.ref,
selectedProjects: observable.ref,
selectedCycle: observable.ref,
selectedModule: observable.ref,
isPeekView: observable.ref,
// computed
selectedDurationLabel: computed,
// actions
updateSelectedProjects: action,
updateSelectedDuration: action,
updateSelectedCycle: action,
updateSelectedModule: action,
updateIsPeekView: action,
});
}
@ -44,7 +58,6 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 {
}
updateSelectedProjects = (projects: string[]) => {
const initialState = this.selectedProjects;
try {
runInAction(() => {
this.selectedProjects = projects;
@ -65,4 +78,22 @@ export class AnalyticsStoreV2 implements IAnalyticsStoreV2 {
throw error;
}
};
updateSelectedCycle = (cycle: string) => {
runInAction(() => {
this.selectedCycle = cycle;
});
};
updateSelectedModule = (module: string) => {
runInAction(() => {
this.selectedModule = module;
});
};
updateIsPeekView = (isPeekView: boolean) => {
runInAction(() => {
this.isPeekView = isPeekView;
});
};
}

View file

@ -96,7 +96,7 @@ export class CoreRootStore {
this.transient = new TransientStore();
this.stickyStore = new StickyStore();
this.editorAssetStore = new EditorAssetStore();
this.analyticsV2 = new AnalyticsStoreV2(this);
this.analyticsV2 = new AnalyticsStoreV2();
}
resetOnSignOut() {