forked from github/plane
refactor: analytics (#2419)
* refactor: helper functions * chore: updated all the page headers * refactor: custom analytics * refactor: project analytics modal
This commit is contained in:
parent
f2c3ad442d
commit
404e6a0cfc
@ -1,174 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import analyticsService from "services/analytics.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button, Input, TextArea } from "@plane/ui";
|
||||
// types
|
||||
import { IAnalyticsParams, ISaveAnalyticsFormData } from "types";
|
||||
|
||||
// types
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
params?: IAnalyticsParams;
|
||||
};
|
||||
|
||||
type FormValues = {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const defaultValues: FormValues = {
|
||||
name: "",
|
||||
description: "",
|
||||
};
|
||||
|
||||
export const CreateUpdateAnalyticsModal: React.FC<Props> = ({ isOpen, handleClose, params }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
} = useForm<FormValues>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
handleClose();
|
||||
reset(defaultValues);
|
||||
};
|
||||
|
||||
const onSubmit = async (formData: FormValues) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
const payload: ISaveAnalyticsFormData = {
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
query_dict: {
|
||||
x_axis: "priority",
|
||||
y_axis: "issue_count",
|
||||
...params,
|
||||
project: params?.project ?? [],
|
||||
},
|
||||
};
|
||||
|
||||
await analyticsService
|
||||
.saveAnalytics(workspaceSlug.toString(), payload)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Analytics saved successfully.",
|
||||
});
|
||||
onClose();
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Analytics could not be saved. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-30" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Save Analytics
|
||||
</Dialog.Title>
|
||||
<div className="mt-5">
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "Title is required",
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Title"
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TextArea
|
||||
id="description"
|
||||
name="description"
|
||||
placeholder="Description"
|
||||
className="mt-3 h-32 resize-none text-sm"
|
||||
hasError={Boolean(errors?.description)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Save Analytics"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
@ -1,24 +1,20 @@
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { Control, UseFormSetValue, useForm } from "react-hook-form";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
// components
|
||||
import { AnalyticsGraph, AnalyticsSelectBar, AnalyticsSidebar, AnalyticsTable } from "components/analytics";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
// helpers
|
||||
import { convertResponseToBarGraphData } from "helpers/analytics.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse, IUser } from "types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS } from "constants/fetch-keys";
|
||||
import useSWR from "swr";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// services
|
||||
import analyticsService from "services/analytics.service";
|
||||
// components
|
||||
import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "components/analytics";
|
||||
// types
|
||||
import { IAnalyticsParams } from "types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
additionalParams?: Partial<IAnalyticsParams>;
|
||||
fullScreen: boolean;
|
||||
user?: IUser | undefined;
|
||||
};
|
||||
|
||||
const defaultValues: IAnalyticsParams = {
|
||||
@ -28,17 +24,20 @@ const defaultValues: IAnalyticsParams = {
|
||||
project: null,
|
||||
};
|
||||
|
||||
export const CustomAnalytics: React.FC<Props> = ({ fullScreen, user }) => {
|
||||
export const CustomAnalytics: React.FC<Props> = observer((props) => {
|
||||
const { additionalParams, fullScreen } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { control, watch, setValue } = useForm<IAnalyticsParams>({ defaultValues });
|
||||
const { control, watch, setValue } = useForm({ defaultValues });
|
||||
|
||||
const params: IAnalyticsParams = {
|
||||
x_axis: watch("x_axis"),
|
||||
y_axis: watch("y_axis"),
|
||||
segment: watch("segment"),
|
||||
project: watch("project"),
|
||||
project: projectId ? [projectId.toString()] : watch("project"),
|
||||
...additionalParams,
|
||||
};
|
||||
|
||||
const { data: analytics, error: analyticsError } = useSWR(
|
||||
@ -48,80 +47,29 @@ export const CustomAnalytics: React.FC<Props> = ({ fullScreen, user }) => {
|
||||
|
||||
const isProjectLevel = projectId ? true : false;
|
||||
|
||||
const yAxisKey = params.y_axis === "issue_count" ? "count" : "estimate";
|
||||
const barGraphData = convertResponseToBarGraphData(analytics?.distribution, params);
|
||||
|
||||
const { projects } = useProjects();
|
||||
|
||||
return (
|
||||
<div className={`overflow-hidden flex flex-col-reverse ${fullScreen ? "md:grid md:grid-cols-4 md:h-full" : ""}`}>
|
||||
<div className="col-span-3 flex flex-col h-full overflow-hidden">
|
||||
<AnalyticsSelectBar
|
||||
<CustomAnalyticsSelectBar
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
projects={projects ?? []}
|
||||
params={params}
|
||||
fullScreen={fullScreen}
|
||||
isProjectLevel={isProjectLevel}
|
||||
/>
|
||||
{!analyticsError ? (
|
||||
analytics ? (
|
||||
analytics.total > 0 ? (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<AnalyticsGraph
|
||||
analytics={analytics}
|
||||
barGraphData={barGraphData}
|
||||
params={params}
|
||||
yAxisKey={yAxisKey}
|
||||
fullScreen={fullScreen}
|
||||
/>
|
||||
<AnalyticsTable analytics={analytics} barGraphData={barGraphData} params={params} yAxisKey={yAxisKey} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-full place-items-center p-5">
|
||||
<div className="space-y-4 text-custom-text-200">
|
||||
<p className="text-sm">No matching issues found. Try changing the parameters.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-6 p-5">
|
||||
<Loader.Item height="300px" />
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</Loader>
|
||||
</Loader>
|
||||
)
|
||||
) : (
|
||||
<div className="grid h-full place-items-center p-5">
|
||||
<div className="space-y-4 text-custom-text-200">
|
||||
<p className="text-sm">There was some error in fetching the data.</p>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<CustomAnalyticsMainContent
|
||||
analytics={analytics}
|
||||
error={analyticsError}
|
||||
fullScreen={fullScreen}
|
||||
params={params}
|
||||
/>
|
||||
</div>
|
||||
<AnalyticsSidebar
|
||||
<CustomAnalyticsSidebar
|
||||
analytics={analytics}
|
||||
params={params}
|
||||
fullScreen={fullScreen}
|
||||
isProjectLevel={isProjectLevel}
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ import { CustomTooltip } from "./custom-tooltip";
|
||||
import { BarGraph } from "components/ui";
|
||||
// helpers
|
||||
import { findStringWithMostCharacters } from "helpers/array.helper";
|
||||
import { generateBarColor } from "helpers/analytics.helper";
|
||||
import { generateBarColor, generateDisplayName } from "helpers/analytics.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||
|
||||
@ -21,21 +21,7 @@ type Props = {
|
||||
fullScreen: boolean;
|
||||
};
|
||||
|
||||
export const AnalyticsGraph: React.FC<Props> = ({
|
||||
analytics,
|
||||
barGraphData,
|
||||
params,
|
||||
yAxisKey,
|
||||
fullScreen,
|
||||
}) => {
|
||||
const renderAssigneeName = (assigneeId: string): string => {
|
||||
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__id === assigneeId);
|
||||
|
||||
if (!assignee) return "?";
|
||||
|
||||
return assignee.assignees__display_name || "?";
|
||||
};
|
||||
|
||||
export const AnalyticsGraph: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey, fullScreen }) => {
|
||||
const generateYAxisTickValues = () => {
|
||||
if (!analytics) return [];
|
||||
|
||||
@ -110,7 +96,7 @@ export const AnalyticsGraph: React.FC<Props> = ({
|
||||
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
||||
{params.x_axis === "assignees__id"
|
||||
? datum.value && datum.value !== "None"
|
||||
? renderAssigneeName(datum.value)[0].toUpperCase()
|
||||
? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase()
|
||||
: "?"
|
||||
: datum.value && datum.value !== "None"
|
||||
? `${datum.value}`.toUpperCase()[0]
|
||||
@ -119,7 +105,13 @@ export const AnalyticsGraph: React.FC<Props> = ({
|
||||
</g>
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
: (datum) => (
|
||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||
<text x={0} y={21} textAnchor="middle" fontSize={10}>
|
||||
{generateDisplayName(datum.value, analytics, params, "x_axis")}
|
||||
</text>
|
||||
</g>
|
||||
),
|
||||
}}
|
||||
theme={{
|
||||
axis: {},
|
||||
|
@ -1,6 +1,7 @@
|
||||
export * from "./graph";
|
||||
export * from "./create-update-analytics-modal";
|
||||
export * from "./select";
|
||||
export * from "./custom-analytics";
|
||||
export * from "./main-content";
|
||||
export * from "./select-bar";
|
||||
export * from "./sidebar";
|
||||
export * from "./table";
|
||||
|
85
web/components/analytics/custom-analytics/main-content.tsx
Normal file
85
web/components/analytics/custom-analytics/main-content.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { mutate } from "swr";
|
||||
|
||||
// components
|
||||
import { AnalyticsGraph, AnalyticsTable } from "components/analytics";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
// helpers
|
||||
import { convertResponseToBarGraphData } from "helpers/analytics.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
analytics: IAnalyticsResponse | undefined;
|
||||
error: any;
|
||||
fullScreen: boolean;
|
||||
params: IAnalyticsParams;
|
||||
};
|
||||
|
||||
export const CustomAnalyticsMainContent: React.FC<Props> = (props) => {
|
||||
const { analytics, error, fullScreen, params } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const yAxisKey = params.y_axis === "issue_count" ? "count" : "estimate";
|
||||
const barGraphData = convertResponseToBarGraphData(analytics?.distribution, params);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!error ? (
|
||||
analytics ? (
|
||||
analytics.total > 0 ? (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<AnalyticsGraph
|
||||
analytics={analytics}
|
||||
barGraphData={barGraphData}
|
||||
params={params}
|
||||
yAxisKey={yAxisKey}
|
||||
fullScreen={fullScreen}
|
||||
/>
|
||||
<AnalyticsTable analytics={analytics} barGraphData={barGraphData} params={params} yAxisKey={yAxisKey} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-full place-items-center p-5">
|
||||
<div className="space-y-4 text-custom-text-200">
|
||||
<p className="text-sm">No matching issues found. Try changing the parameters.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-6 p-5">
|
||||
<Loader.Item height="300px" />
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</Loader>
|
||||
</Loader>
|
||||
)
|
||||
) : (
|
||||
<div className="grid h-full place-items-center p-5">
|
||||
<div className="space-y-4 text-custom-text-200">
|
||||
<p className="text-sm">There was some error in fetching the data.</p>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,80 +1,85 @@
|
||||
// react-hook-form
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Control, Controller, UseFormSetValue } from "react-hook-form";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics";
|
||||
// types
|
||||
import { IAnalyticsParams, IProject } from "types";
|
||||
import { IAnalyticsParams } from "types";
|
||||
|
||||
type Props = {
|
||||
control: Control<IAnalyticsParams, any>;
|
||||
setValue: UseFormSetValue<IAnalyticsParams>;
|
||||
projects: IProject[];
|
||||
params: IAnalyticsParams;
|
||||
fullScreen: boolean;
|
||||
isProjectLevel: boolean;
|
||||
};
|
||||
|
||||
export const AnalyticsSelectBar: React.FC<Props> = ({
|
||||
control,
|
||||
setValue,
|
||||
projects,
|
||||
params,
|
||||
fullScreen,
|
||||
isProjectLevel,
|
||||
}) => (
|
||||
<div
|
||||
className={`grid items-center gap-4 px-5 py-2.5 ${
|
||||
isProjectLevel ? "grid-cols-3" : "grid-cols-2"
|
||||
} ${fullScreen ? "lg:grid-cols-4 md:py-5" : ""}`}
|
||||
>
|
||||
{!isProjectLevel && (
|
||||
export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
|
||||
const { control, setValue, params, fullScreen, isProjectLevel } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { project: projectStore } = useMobxStore();
|
||||
|
||||
const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`grid items-center gap-4 px-5 py-2.5 ${isProjectLevel ? "grid-cols-3" : "grid-cols-2"} ${
|
||||
fullScreen ? "lg:grid-cols-4 md:py-5" : ""
|
||||
}`}
|
||||
>
|
||||
{!isProjectLevel && (
|
||||
<div>
|
||||
<h6 className="text-xs text-custom-text-200">Project</h6>
|
||||
<Controller
|
||||
name="project"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SelectProject value={value ?? undefined} onChange={onChange} projects={projectsList ?? undefined} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h6 className="text-xs text-custom-text-200">Project</h6>
|
||||
<h6 className="text-xs text-custom-text-200">Measure (y-axis)</h6>
|
||||
<Controller
|
||||
name="project"
|
||||
name="y_axis"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => <SelectYAxis value={value} onChange={onChange} />}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="text-xs text-custom-text-200">Dimension (x-axis)</h6>
|
||||
<Controller
|
||||
name="x_axis"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SelectProject value={value} onChange={onChange} projects={projects} />
|
||||
<SelectXAxis
|
||||
value={value}
|
||||
onChange={(val: string) => {
|
||||
if (params.segment === val) setValue("segment", null);
|
||||
|
||||
onChange(val);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="text-xs text-custom-text-200">Group</h6>
|
||||
<Controller
|
||||
name="segment"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SelectSegment value={value} onChange={onChange} params={params} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h6 className="text-xs text-custom-text-200">Measure (y-axis)</h6>
|
||||
<Controller
|
||||
name="y_axis"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SelectYAxis value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="text-xs text-custom-text-200">Dimension (x-axis)</h6>
|
||||
<Controller
|
||||
name="x_axis"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SelectXAxis
|
||||
value={value}
|
||||
onChange={(val: string) => {
|
||||
if (params.segment === val) setValue("segment", null);
|
||||
|
||||
onChange(val);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="text-xs text-custom-text-200">Group</h6>
|
||||
<Controller
|
||||
name="segment"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SelectSegment value={value} onChange={onChange} params={params} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
});
|
||||
|
@ -4,9 +4,9 @@ import { CustomSearchSelect } from "components/ui";
|
||||
import { IProject } from "types";
|
||||
|
||||
type Props = {
|
||||
value: string[] | null | undefined;
|
||||
value: string[] | undefined;
|
||||
onChange: (val: string[] | null) => void;
|
||||
projects: IProject[];
|
||||
projects: IProject[] | undefined;
|
||||
};
|
||||
|
||||
export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) => {
|
||||
@ -29,7 +29,7 @@ export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) =>
|
||||
label={
|
||||
value && value.length > 0
|
||||
? projects
|
||||
.filter((p) => value.includes(p.id))
|
||||
?.filter((p) => value.includes(p.id))
|
||||
.map((p) => p.identifier)
|
||||
.join(", ")
|
||||
: "All projects"
|
@ -34,8 +34,8 @@ export const SelectSegment: React.FC<Props> = ({ value, onChange, params }) => {
|
||||
<CustomSelect.Option value={null}>No value</CustomSelect.Option>
|
||||
{ANALYTICS_X_AXIS_VALUES.map((item) => {
|
||||
if (params.x_axis === item.value) return null;
|
||||
if (cycleId && item.value === "issue_cycle__cycle__name") return null;
|
||||
if (moduleId && item.value === "issue_module__module__name") return null;
|
||||
if (cycleId && item.value === "issue_cycle__cycle_id") return null;
|
||||
if (moduleId && item.value === "issue_module__module_id") return null;
|
||||
|
||||
return (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
@ -25,8 +25,8 @@ export const SelectXAxis: React.FC<Props> = ({ value, onChange }) => {
|
||||
maxHeight="lg"
|
||||
>
|
||||
{ANALYTICS_X_AXIS_VALUES.map((item) => {
|
||||
if (cycleId && item.value === "issue_cycle__cycle__name") return null;
|
||||
if (moduleId && item.value === "issue_module__module__name") return null;
|
||||
if (cycleId && item.value === "issue_cycle__cycle_id") return null;
|
||||
if (moduleId && item.value === "issue_module__module_id") return null;
|
||||
|
||||
return (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
@ -1,347 +0,0 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import analyticsService from "services/analytics.service";
|
||||
import projectService from "services/project.service";
|
||||
import cyclesService from "services/cycles.service";
|
||||
import modulesService from "services/modules.service";
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { ArrowDownTrayIcon, ArrowPathIcon, CalendarDaysIcon, UserGroupIcon } from "@heroicons/react/24/outline";
|
||||
import { ContrastIcon, LayerDiagonalIcon } from "components/icons";
|
||||
// helpers
|
||||
import { renderShortDate } from "helpers/date-time.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import {
|
||||
IAnalyticsParams,
|
||||
IAnalyticsResponse,
|
||||
ICurrentUserResponse,
|
||||
IExportAnalyticsFormData,
|
||||
IWorkspace,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { NETWORK_CHOICES } from "constants/project";
|
||||
|
||||
type Props = {
|
||||
analytics: IAnalyticsResponse | undefined;
|
||||
params: IAnalyticsParams;
|
||||
fullScreen: boolean;
|
||||
isProjectLevel: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScreen, isProjectLevel = false, user }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const { projects } = useProjects();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: projectDetails } = useSWR(
|
||||
workspaceSlug && projectId && !(cycleId || moduleId) ? PROJECT_DETAILS(projectId.toString()) : null,
|
||||
workspaceSlug && projectId && !(cycleId || moduleId)
|
||||
? () => projectService.getProject(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: cycleDetails } = useSWR(
|
||||
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
|
||||
workspaceSlug && projectId && cycleId
|
||||
? () => cyclesService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: moduleDetails } = useSWR(
|
||||
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
|
||||
workspaceSlug && projectId && moduleId
|
||||
? () => modulesService.getModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const trackExportAnalytics = () => {
|
||||
const eventPayload: any = {
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
params: {
|
||||
x_axis: params.x_axis,
|
||||
y_axis: params.y_axis,
|
||||
group: params.segment,
|
||||
project: params.project,
|
||||
},
|
||||
};
|
||||
|
||||
if (projectDetails) {
|
||||
const workspaceDetails = projectDetails.workspace as IWorkspace;
|
||||
|
||||
eventPayload.workspaceId = workspaceDetails.id;
|
||||
eventPayload.workspaceName = workspaceDetails.name;
|
||||
eventPayload.projectId = projectDetails.id;
|
||||
eventPayload.projectIdentifier = projectDetails.identifier;
|
||||
eventPayload.projectName = projectDetails.name;
|
||||
}
|
||||
|
||||
if (cycleDetails || moduleDetails) {
|
||||
const details = cycleDetails || moduleDetails;
|
||||
|
||||
eventPayload.workspaceId = details?.workspace_detail?.id;
|
||||
eventPayload.workspaceName = details?.workspace_detail?.name;
|
||||
eventPayload.projectId = details?.project_detail.id;
|
||||
eventPayload.projectIdentifier = details?.project_detail.identifier;
|
||||
eventPayload.projectName = details?.project_detail.name;
|
||||
}
|
||||
|
||||
if (cycleDetails) {
|
||||
eventPayload.cycleId = cycleDetails.id;
|
||||
eventPayload.cycleName = cycleDetails.name;
|
||||
}
|
||||
|
||||
if (moduleDetails) {
|
||||
eventPayload.moduleId = moduleDetails.id;
|
||||
eventPayload.moduleName = moduleDetails.name;
|
||||
}
|
||||
|
||||
trackEventServices.trackAnalyticsEvent(
|
||||
eventPayload,
|
||||
cycleId
|
||||
? "CYCLE_ANALYTICS_EXPORT"
|
||||
: moduleId
|
||||
? "MODULE_ANALYTICS_EXPORT"
|
||||
: projectId
|
||||
? "PROJECT_ANALYTICS_EXPORT"
|
||||
: "WORKSPACE_ANALYTICS_EXPORT",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const exportAnalytics = () => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
const data: IExportAnalyticsFormData = {
|
||||
x_axis: params.x_axis,
|
||||
y_axis: params.y_axis,
|
||||
};
|
||||
|
||||
if (params.segment) data.segment = params.segment;
|
||||
if (params.project) data.project = params.project;
|
||||
|
||||
analyticsService
|
||||
.exportAnalytics(workspaceSlug.toString(), data)
|
||||
.then((res) => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: res.message,
|
||||
});
|
||||
|
||||
trackExportAnalytics();
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "There was some error in exporting the analytics. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const selectedProjects = params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`px-5 py-2.5 flex items-center justify-between space-y-2 ${
|
||||
fullScreen
|
||||
? "border-l border-custom-border-200 md:h-full md:border-l md:border-custom-border-200 md:space-y-4 overflow-hidden md:flex-col md:items-start md:py-5"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
|
||||
<LayerDiagonalIcon height={14} width={14} />
|
||||
{analytics ? analytics.total : "..."} Issues
|
||||
</div>
|
||||
{isProjectLevel && (
|
||||
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
|
||||
<CalendarDaysIcon className="h-3.5 w-3.5" />
|
||||
{renderShortDate(
|
||||
(cycleId
|
||||
? cycleDetails?.created_at
|
||||
: moduleId
|
||||
? moduleDetails?.created_at
|
||||
: projectDetails?.created_at) ?? ""
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
{fullScreen ? (
|
||||
<>
|
||||
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
||||
<div className="hidden h-full overflow-hidden md:flex md:flex-col">
|
||||
<h4 className="font-medium">Selected Projects</h4>
|
||||
<div className="space-y-6 mt-4 h-full overflow-y-auto">
|
||||
{selectedProjects.map((projectId) => {
|
||||
const project = projects?.find((p) => p.id === projectId);
|
||||
|
||||
if (project)
|
||||
return (
|
||||
<div key={project.id} className="w-full">
|
||||
<div className="text-sm flex items-center gap-1">
|
||||
{project.emoji ? (
|
||||
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">
|
||||
{renderEmoji(project.emoji)}
|
||||
</span>
|
||||
) : project.icon_prop ? (
|
||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
||||
{renderEmoji(project.icon_prop)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||
{project?.name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<h5 className="flex items-center gap-1">
|
||||
<p className="break-words">{truncateText(project.name, 20)}</p>
|
||||
<span className="text-custom-text-200 text-xs ml-1">({project.identifier})</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="mt-4 space-y-3 pl-2 w-full">
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
|
||||
<h6>Total members</h6>
|
||||
</div>
|
||||
<span className="text-custom-text-200">{project.total_members}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<ContrastIcon height={16} width={16} />
|
||||
<h6>Total cycles</h6>
|
||||
</div>
|
||||
<span className="text-custom-text-200">{project.total_cycles}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
|
||||
<h6>Total modules</h6>
|
||||
</div>
|
||||
<span className="text-custom-text-200">{project.total_modules}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{projectId ? (
|
||||
cycleId && cycleDetails ? (
|
||||
<div className="hidden md:block h-full overflow-y-auto">
|
||||
<h4 className="font-medium break-words">Analytics for {cycleDetails.name}</h4>
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Lead</h6>
|
||||
<span>{cycleDetails.owned_by?.display_name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Start Date</h6>
|
||||
<span>
|
||||
{cycleDetails.start_date && cycleDetails.start_date !== ""
|
||||
? renderShortDate(cycleDetails.start_date)
|
||||
: "No start date"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Target Date</h6>
|
||||
<span>
|
||||
{cycleDetails.end_date && cycleDetails.end_date !== ""
|
||||
? renderShortDate(cycleDetails.end_date)
|
||||
: "No end date"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : moduleId && moduleDetails ? (
|
||||
<div className="hidden md:block h-full overflow-y-auto">
|
||||
<h4 className="font-medium break-words">Analytics for {moduleDetails.name}</h4>
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Lead</h6>
|
||||
<span>{moduleDetails.lead_detail?.display_name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Start Date</h6>
|
||||
<span>
|
||||
{moduleDetails.start_date && moduleDetails.start_date !== ""
|
||||
? renderShortDate(moduleDetails.start_date)
|
||||
: "No start date"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Target Date</h6>
|
||||
<span>
|
||||
{moduleDetails.target_date && moduleDetails.target_date !== ""
|
||||
? renderShortDate(moduleDetails.target_date)
|
||||
: "No end date"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="hidden md:flex md:flex-col h-full overflow-y-auto">
|
||||
<div className="flex items-center gap-1">
|
||||
{projectDetails?.emoji ? (
|
||||
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">
|
||||
{renderEmoji(projectDetails.emoji)}
|
||||
</div>
|
||||
) : projectDetails?.icon_prop ? (
|
||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
||||
{renderEmoji(projectDetails.icon_prop)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||
{projectDetails?.name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<h4 className="font-medium break-words">{projectDetails?.name}</h4>
|
||||
</div>
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Network</h6>
|
||||
<span>{NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.label ?? ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap justify-self-end">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
prependIcon={<ArrowPathIcon className="h-3.5 w-3.5" />}
|
||||
onClick={() => {
|
||||
if (!workspaceSlug) return;
|
||||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button variant="primary" prependIcon={<ArrowDownTrayIcon />} onClick={exportAnalytics}>
|
||||
Export as CSV
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export * from "./projects-list";
|
||||
export * from "./sidebar-header";
|
||||
export * from "./sidebar";
|
@ -0,0 +1,65 @@
|
||||
// icons
|
||||
import { Contrast, LayoutGrid, Users } from "lucide-react";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import { IProject } from "types";
|
||||
|
||||
type Props = {
|
||||
projects: IProject[];
|
||||
};
|
||||
|
||||
export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = (props) => {
|
||||
const { projects } = props;
|
||||
|
||||
return (
|
||||
<div className="hidden h-full overflow-hidden md:flex md:flex-col">
|
||||
<h4 className="font-medium">Selected Projects</h4>
|
||||
<div className="space-y-6 mt-4 h-full overflow-y-auto">
|
||||
{projects.map((project) => (
|
||||
<div key={project.id} className="w-full">
|
||||
<div className="text-sm flex items-center gap-1">
|
||||
{project.emoji ? (
|
||||
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.emoji)}</span>
|
||||
) : project.icon_prop ? (
|
||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">{renderEmoji(project.icon_prop)}</div>
|
||||
) : (
|
||||
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||
{project?.name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<h5 className="flex items-center gap-1">
|
||||
<p className="break-words">{truncateText(project.name, 20)}</p>
|
||||
<span className="text-custom-text-200 text-xs ml-1">({project.identifier})</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="mt-4 space-y-3 pl-2 w-full">
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||
<h6>Total members</h6>
|
||||
</div>
|
||||
<span className="text-custom-text-200">{project.total_members}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<Contrast className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||
<h6>Total cycles</h6>
|
||||
</div>
|
||||
<span className="text-custom-text-200">{project.total_cycles}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutGrid className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||
<h6>Total modules</h6>
|
||||
</div>
|
||||
<span className="text-custom-text-200">{project.total_modules}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,107 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
import { renderShortDate } from "helpers/date-time.helper";
|
||||
// constants
|
||||
import { NETWORK_CHOICES } from "constants/project";
|
||||
|
||||
export const CustomAnalyticsSidebarHeader = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const { cycle: cycleStore, module: moduleStore, project: projectStore } = useMobxStore();
|
||||
|
||||
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
|
||||
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
|
||||
const projectDetails =
|
||||
workspaceSlug && projectId
|
||||
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectId ? (
|
||||
cycleDetails ? (
|
||||
<div className="hidden md:block h-full overflow-y-auto">
|
||||
<h4 className="font-medium break-words">Analytics for {cycleDetails.name}</h4>
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Lead</h6>
|
||||
<span>{cycleDetails.owned_by?.display_name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Start Date</h6>
|
||||
<span>
|
||||
{cycleDetails.start_date && cycleDetails.start_date !== ""
|
||||
? renderShortDate(cycleDetails.start_date)
|
||||
: "No start date"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Target Date</h6>
|
||||
<span>
|
||||
{cycleDetails.end_date && cycleDetails.end_date !== ""
|
||||
? renderShortDate(cycleDetails.end_date)
|
||||
: "No end date"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : moduleDetails ? (
|
||||
<div className="hidden md:block h-full overflow-y-auto">
|
||||
<h4 className="font-medium break-words">Analytics for {moduleDetails.name}</h4>
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Lead</h6>
|
||||
<span>{moduleDetails.lead_detail?.display_name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Start Date</h6>
|
||||
<span>
|
||||
{moduleDetails.start_date && moduleDetails.start_date !== ""
|
||||
? renderShortDate(moduleDetails.start_date)
|
||||
: "No start date"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Target Date</h6>
|
||||
<span>
|
||||
{moduleDetails.target_date && moduleDetails.target_date !== ""
|
||||
? renderShortDate(moduleDetails.target_date)
|
||||
: "No end date"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="hidden md:flex md:flex-col h-full overflow-y-auto">
|
||||
<div className="flex items-center gap-1">
|
||||
{projectDetails?.emoji ? (
|
||||
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(projectDetails.emoji)}</div>
|
||||
) : projectDetails?.icon_prop ? (
|
||||
<div className="h-6 w-6 grid place-items-center flex-shrink-0">
|
||||
{renderEmoji(projectDetails.icon_prop)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="grid h-6 w-6 mr-1 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||
{projectDetails?.name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<h4 className="font-medium break-words">{projectDetails?.name}</h4>
|
||||
</div>
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Network</h6>
|
||||
<span>{NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.label ?? ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
});
|
214
web/components/analytics/custom-analytics/sidebar/sidebar.tsx
Normal file
214
web/components/analytics/custom-analytics/sidebar/sidebar.tsx
Normal file
@ -0,0 +1,214 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { mutate } from "swr";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import analyticsService from "services/analytics.service";
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "components/analytics";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { ArrowDownTrayIcon, ArrowPathIcon, CalendarDaysIcon } from "@heroicons/react/24/outline";
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
// helpers
|
||||
import { renderShortDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
analytics: IAnalyticsResponse | undefined;
|
||||
params: IAnalyticsParams;
|
||||
fullScreen: boolean;
|
||||
isProjectLevel: boolean;
|
||||
};
|
||||
|
||||
export const CustomAnalyticsSidebar: React.FC<Props> = observer(
|
||||
({ analytics, params, fullScreen, isProjectLevel = false }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { user: userStore, project: projectStore, cycle: cycleStore, module: moduleStore } = useMobxStore();
|
||||
|
||||
const user = userStore.currentUser;
|
||||
|
||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
||||
const projectDetails =
|
||||
workspaceSlug && projectId
|
||||
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString()) ?? undefined
|
||||
: undefined;
|
||||
|
||||
const trackExportAnalytics = () => {
|
||||
if (!user) return;
|
||||
|
||||
const eventPayload: any = {
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
params: {
|
||||
x_axis: params.x_axis,
|
||||
y_axis: params.y_axis,
|
||||
group: params.segment,
|
||||
project: params.project,
|
||||
},
|
||||
};
|
||||
|
||||
if (projectDetails) {
|
||||
const workspaceDetails = projectDetails.workspace as IWorkspace;
|
||||
|
||||
eventPayload.workspaceId = workspaceDetails.id;
|
||||
eventPayload.workspaceName = workspaceDetails.name;
|
||||
eventPayload.projectId = projectDetails.id;
|
||||
eventPayload.projectIdentifier = projectDetails.identifier;
|
||||
eventPayload.projectName = projectDetails.name;
|
||||
}
|
||||
|
||||
if (cycleDetails || moduleDetails) {
|
||||
const details = cycleDetails || moduleDetails;
|
||||
|
||||
eventPayload.workspaceId = details?.workspace_detail?.id;
|
||||
eventPayload.workspaceName = details?.workspace_detail?.name;
|
||||
eventPayload.projectId = details?.project_detail.id;
|
||||
eventPayload.projectIdentifier = details?.project_detail.identifier;
|
||||
eventPayload.projectName = details?.project_detail.name;
|
||||
}
|
||||
|
||||
if (cycleDetails) {
|
||||
eventPayload.cycleId = cycleDetails.id;
|
||||
eventPayload.cycleName = cycleDetails.name;
|
||||
}
|
||||
|
||||
if (moduleDetails) {
|
||||
eventPayload.moduleId = moduleDetails.id;
|
||||
eventPayload.moduleName = moduleDetails.name;
|
||||
}
|
||||
|
||||
trackEventServices.trackAnalyticsEvent(
|
||||
eventPayload,
|
||||
cycleId
|
||||
? "CYCLE_ANALYTICS_EXPORT"
|
||||
: moduleId
|
||||
? "MODULE_ANALYTICS_EXPORT"
|
||||
: projectId
|
||||
? "PROJECT_ANALYTICS_EXPORT"
|
||||
: "WORKSPACE_ANALYTICS_EXPORT",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const exportAnalytics = () => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
const data: IExportAnalyticsFormData = {
|
||||
x_axis: params.x_axis,
|
||||
y_axis: params.y_axis,
|
||||
};
|
||||
|
||||
if (params.segment) data.segment = params.segment;
|
||||
if (params.project) data.project = params.project;
|
||||
|
||||
analyticsService
|
||||
.exportAnalytics(workspaceSlug.toString(), data)
|
||||
.then((res) => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: res.message,
|
||||
});
|
||||
|
||||
trackExportAnalytics();
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "There was some error in exporting the analytics. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
|
||||
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
|
||||
|
||||
// fetch cycle details
|
||||
useEffect(() => {
|
||||
if (!workspaceSlug || !projectId || !cycleId || cycleDetails) return;
|
||||
|
||||
cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
|
||||
}, [cycleId, cycleDetails, cycleStore, projectId, workspaceSlug]);
|
||||
|
||||
// fetch module details
|
||||
useEffect(() => {
|
||||
if (!workspaceSlug || !projectId || !moduleId || moduleDetails) return;
|
||||
|
||||
moduleStore.fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
|
||||
}, [moduleId, moduleDetails, moduleStore, projectId, workspaceSlug]);
|
||||
|
||||
const selectedProjects = params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`px-5 py-2.5 flex items-center justify-between space-y-2 ${
|
||||
fullScreen
|
||||
? "border-l border-custom-border-200 md:h-full md:border-l md:border-custom-border-200 md:space-y-4 overflow-hidden md:flex-col md:items-start md:py-5"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
|
||||
<LayerDiagonalIcon height={14} width={14} />
|
||||
{analytics ? analytics.total : "..."} Issues
|
||||
</div>
|
||||
{isProjectLevel && (
|
||||
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
|
||||
<CalendarDaysIcon className="h-3.5 w-3.5" />
|
||||
{renderShortDate(
|
||||
(cycleId
|
||||
? cycleDetails?.created_at
|
||||
: moduleId
|
||||
? moduleDetails?.created_at
|
||||
: projectDetails?.created_at) ?? ""
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
{fullScreen ? (
|
||||
<>
|
||||
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
||||
<CustomAnalyticsSidebarProjectsList
|
||||
projects={projects?.filter((p) => selectedProjects.includes(p.id)) ?? []}
|
||||
/>
|
||||
)}
|
||||
<CustomAnalyticsSidebarHeader />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap justify-self-end">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
prependIcon={<ArrowPathIcon className="h-3.5 w-3.5" />}
|
||||
onClick={() => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button variant="primary" prependIcon={<ArrowDownTrayIcon />} onClick={exportAnalytics}>
|
||||
Export as CSV
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
@ -1,15 +1,13 @@
|
||||
// nivo
|
||||
import { BarDatum } from "@nivo/bar";
|
||||
|
||||
// icons
|
||||
import { PriorityIcon } from "components/icons";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// helpers
|
||||
import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper";
|
||||
import { generateBarColor, generateDisplayName } from "helpers/analytics.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "types";
|
||||
// constants
|
||||
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, DATE_KEYS } from "constants/analytics";
|
||||
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "constants/analytics";
|
||||
|
||||
type Props = {
|
||||
analytics: IAnalyticsResponse;
|
||||
@ -21,112 +19,81 @@ type Props = {
|
||||
yAxisKey: "count" | "estimate";
|
||||
};
|
||||
|
||||
export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey }) => {
|
||||
const renderAssigneeName = (assigneeId: string): string => {
|
||||
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__id === assigneeId);
|
||||
|
||||
if (!assignee) return "No assignee";
|
||||
|
||||
return assignee.assignees__display_name || "No assignee";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flow-root">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full align-middle">
|
||||
<table className="min-w-full divide-y divide-custom-border-200 whitespace-nowrap border-y border-custom-border-200">
|
||||
<thead className="bg-custom-background-80">
|
||||
<tr className="divide-x divide-custom-border-200 text-sm text-custom-text-100">
|
||||
<th scope="col" className="py-3 px-2.5 text-left font-medium">
|
||||
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label}
|
||||
</th>
|
||||
{params.segment ? (
|
||||
barGraphData.xAxisKeys.map((key) => (
|
||||
<th
|
||||
key={`segment-${key}`}
|
||||
scope="col"
|
||||
className={`px-2.5 py-3 text-left font-medium ${
|
||||
params.segment === "priority" || params.segment === "state__group"
|
||||
? "capitalize"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{params.segment === "priority" ? (
|
||||
<PriorityIcon priority={key as TIssuePriorities} />
|
||||
) : (
|
||||
<span
|
||||
className="h-3 w-3 flex-shrink-0 rounded"
|
||||
style={{
|
||||
backgroundColor: generateBarColor(key, analytics, params, "segment"),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{params.segment === "assignees__id"
|
||||
? renderAssigneeName(key)
|
||||
: DATE_KEYS.includes(params.segment ?? "")
|
||||
? renderMonthAndYear(key)
|
||||
: key}
|
||||
</div>
|
||||
</th>
|
||||
))
|
||||
) : (
|
||||
<th scope="col" className="py-3 px-2.5 text-left font-medium sm:pr-0">
|
||||
{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === params.y_axis)?.label}
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-custom-border-200">
|
||||
{barGraphData.data.map((item, index) => (
|
||||
<tr
|
||||
key={`table-row-${index}`}
|
||||
className="divide-x divide-custom-border-200 text-xs text-custom-text-200"
|
||||
>
|
||||
<td
|
||||
className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${
|
||||
params.x_axis === "priority" || params.x_axis === "state__group"
|
||||
? "capitalize"
|
||||
: ""
|
||||
export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey }) => (
|
||||
<div className="flow-root">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full align-middle">
|
||||
<table className="min-w-full divide-y divide-custom-border-200 whitespace-nowrap border-y border-custom-border-200">
|
||||
<thead className="bg-custom-background-80">
|
||||
<tr className="divide-x divide-custom-border-200 text-sm text-custom-text-100">
|
||||
<th scope="col" className="py-3 px-2.5 text-left font-medium">
|
||||
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label}
|
||||
</th>
|
||||
{params.segment ? (
|
||||
barGraphData.xAxisKeys.map((key) => (
|
||||
<th
|
||||
key={`segment-${key}`}
|
||||
scope="col"
|
||||
className={`px-2.5 py-3 text-left font-medium ${
|
||||
params.segment === "priority" || params.segment === "state__group" ? "capitalize" : ""
|
||||
}`}
|
||||
>
|
||||
{params.x_axis === "priority" ? (
|
||||
<PriorityIcon priority={item.name as TIssuePriorities} />
|
||||
) : (
|
||||
<span
|
||||
className="h-3 w-3 rounded"
|
||||
style={{
|
||||
backgroundColor: generateBarColor(
|
||||
`${item.name}`,
|
||||
analytics,
|
||||
params,
|
||||
"x_axis"
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{params.x_axis === "assignees__id"
|
||||
? renderAssigneeName(`${item.name}`)
|
||||
: addSpaceIfCamelCase(`${item.name}`)}
|
||||
</td>
|
||||
{params.segment ? (
|
||||
barGraphData.xAxisKeys.map((key, index) => (
|
||||
<td
|
||||
key={`segment-value-${index}`}
|
||||
className="whitespace-nowrap py-2 px-2.5 sm:pr-0"
|
||||
>
|
||||
{item[key] ?? 0}
|
||||
</td>
|
||||
))
|
||||
<div className="flex items-center gap-2">
|
||||
{params.segment === "priority" ? (
|
||||
<PriorityIcon priority={key as TIssuePriorities} />
|
||||
) : (
|
||||
<span
|
||||
className="h-3 w-3 flex-shrink-0 rounded"
|
||||
style={{
|
||||
backgroundColor: generateBarColor(key, analytics, params, "segment"),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{generateDisplayName(key, analytics, params, "segment")}
|
||||
</div>
|
||||
</th>
|
||||
))
|
||||
) : (
|
||||
<th scope="col" className="py-3 px-2.5 text-left font-medium sm:pr-0">
|
||||
{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === params.y_axis)?.label}
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-custom-border-200">
|
||||
{barGraphData.data.map((item, index) => (
|
||||
<tr key={`table-row-${index}`} className="divide-x divide-custom-border-200 text-xs text-custom-text-200">
|
||||
<td
|
||||
className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${
|
||||
params.x_axis === "priority" || params.x_axis === "state__group" ? "capitalize" : ""
|
||||
}`}
|
||||
>
|
||||
{params.x_axis === "priority" ? (
|
||||
<PriorityIcon priority={item.name as TIssuePriorities} />
|
||||
) : (
|
||||
<td className="whitespace-nowrap py-2 px-2.5 sm:pr-0">{item[yAxisKey]}</td>
|
||||
<span
|
||||
className="h-3 w-3 rounded"
|
||||
style={{
|
||||
backgroundColor: generateBarColor(`${item.name}`, analytics, params, "x_axis"),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{generateDisplayName(`${item.name}`, analytics, params, "x_axis")}
|
||||
</td>
|
||||
{params.segment ? (
|
||||
barGraphData.xAxisKeys.map((key, index) => (
|
||||
<td key={`segment-value-${index}`} className="whitespace-nowrap py-2 px-2.5 sm:pr-0">
|
||||
{item[key] ?? 0}
|
||||
</td>
|
||||
))
|
||||
) : (
|
||||
<td className="whitespace-nowrap py-2 px-2.5 sm:pr-0">{item[yAxisKey]}</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
@ -1,4 +1,3 @@
|
||||
export * from "./custom-analytics";
|
||||
export * from "./scope-and-demand";
|
||||
export * from "./select";
|
||||
export * from "./project-modal";
|
||||
|
@ -1,207 +0,0 @@
|
||||
import React, { Fragment, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
// services
|
||||
import analyticsService from "services/analytics.service";
|
||||
import projectService from "services/project.service";
|
||||
import cyclesService from "services/cycles.service";
|
||||
import modulesService from "services/modules.service";
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// components
|
||||
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
||||
// icons
|
||||
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IAnalyticsParams, IWorkspace } from "types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const defaultValues: IAnalyticsParams = {
|
||||
x_axis: "priority",
|
||||
y_axis: "issue_count",
|
||||
segment: null,
|
||||
project: null,
|
||||
};
|
||||
|
||||
const tabsList = ["Scope and Demand", "Custom Analytics"];
|
||||
|
||||
export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const { control, watch, setValue } = useForm<IAnalyticsParams>({ defaultValues });
|
||||
|
||||
const params: IAnalyticsParams = {
|
||||
x_axis: watch("x_axis"),
|
||||
y_axis: watch("y_axis"),
|
||||
segment: watch("segment"),
|
||||
project: projectId ? [projectId.toString()] : watch("project"),
|
||||
cycle: cycleId ? cycleId.toString() : null,
|
||||
module: moduleId ? moduleId.toString() : null,
|
||||
};
|
||||
|
||||
const { data: analytics, error: analyticsError } = useSWR(
|
||||
workspaceSlug ? ANALYTICS(workspaceSlug.toString(), params) : null,
|
||||
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
|
||||
);
|
||||
|
||||
const { data: projectDetails } = useSWR(
|
||||
workspaceSlug && projectId && !(cycleId || moduleId) ? PROJECT_DETAILS(projectId.toString()) : null,
|
||||
workspaceSlug && projectId && !(cycleId || moduleId)
|
||||
? () => projectService.getProject(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: cycleDetails } = useSWR(
|
||||
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
|
||||
workspaceSlug && projectId && cycleId
|
||||
? () => cyclesService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: moduleDetails } = useSWR(
|
||||
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
|
||||
workspaceSlug && projectId && moduleId
|
||||
? () => modulesService.getModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const trackAnalyticsEvent = (tab: string) => {
|
||||
const eventPayload: any = {
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
};
|
||||
|
||||
if (projectDetails) {
|
||||
const workspaceDetails = projectDetails.workspace as IWorkspace;
|
||||
|
||||
eventPayload.workspaceId = workspaceDetails.id;
|
||||
eventPayload.workspaceName = workspaceDetails.name;
|
||||
eventPayload.projectId = projectDetails.id;
|
||||
eventPayload.projectIdentifier = projectDetails.identifier;
|
||||
eventPayload.projectName = projectDetails.name;
|
||||
}
|
||||
|
||||
if (cycleDetails || moduleDetails) {
|
||||
const details = cycleDetails || moduleDetails;
|
||||
|
||||
eventPayload.workspaceId = details?.workspace_detail?.id;
|
||||
eventPayload.workspaceName = details?.workspace_detail?.name;
|
||||
eventPayload.projectId = details?.project_detail.id;
|
||||
eventPayload.projectIdentifier = details?.project_detail.identifier;
|
||||
eventPayload.projectName = details?.project_detail.name;
|
||||
}
|
||||
|
||||
if (cycleDetails) {
|
||||
eventPayload.cycleId = cycleDetails.id;
|
||||
eventPayload.cycleName = cycleDetails.name;
|
||||
}
|
||||
|
||||
if (moduleDetails) {
|
||||
eventPayload.moduleId = moduleDetails.id;
|
||||
eventPayload.moduleName = moduleDetails.name;
|
||||
}
|
||||
|
||||
const eventType = tab === "Scope and Demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS";
|
||||
|
||||
trackEventServices.trackAnalyticsEvent(
|
||||
eventPayload,
|
||||
cycleId ? `CYCLE_${eventType}` : moduleId ? `MODULE_${eventType}` : `PROJECT_${eventType}`,
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute top-0 z-30 h-full bg-custom-background-90 ${fullScreen ? "p-2 w-full" : "w-1/2"} ${
|
||||
isOpen ? "right-0" : "-right-full"
|
||||
} duration-300 transition-all`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-full flex-col overflow-hidden border-custom-border-200 bg-custom-background-100 text-left ${
|
||||
fullScreen ? "rounded-lg border" : "border-l"
|
||||
}`}
|
||||
>
|
||||
<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 {cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={() => setFullScreen((prevData) => !prevData)}
|
||||
>
|
||||
{fullScreen ? (
|
||||
<ArrowsPointingInIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<ArrowsPointingOutIcon className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Tab.Group as={Fragment}>
|
||||
<Tab.List as="div" className="space-x-2 border-b border-custom-border-200 p-5 pt-0">
|
||||
{tabsList.map((tab) => (
|
||||
<Tab
|
||||
key={tab}
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
||||
selected ? "bg-custom-background-80" : ""
|
||||
}`
|
||||
}
|
||||
onClick={() => trackAnalyticsEvent(tab)}
|
||||
>
|
||||
{tab}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
{/* <h4 className="p-5 pb-0">Analytics for</h4> */}
|
||||
<Tab.Panels as={Fragment}>
|
||||
<Tab.Panel as={Fragment}>
|
||||
<ScopeAndDemand fullScreen={fullScreen} />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as={Fragment}>
|
||||
<CustomAnalytics
|
||||
analytics={analytics}
|
||||
analyticsError={analyticsError}
|
||||
params={params}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
fullScreen={fullScreen}
|
||||
user={user}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
37
web/components/analytics/project-modal/header.tsx
Normal file
37
web/components/analytics/project-modal/header.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// icons
|
||||
import { Expand, Shrink, X } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
fullScreen: boolean;
|
||||
handleClose: () => void;
|
||||
setFullScreen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const ProjectAnalyticsModalHeader: React.FC<Props> = observer((props) => {
|
||||
const { fullScreen, handleClose, setFullScreen, title } = 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>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={() => setFullScreen((prevData) => !prevData)}
|
||||
>
|
||||
{fullScreen ? <Shrink size={14} strokeWidth={2} /> : <Expand size={14} strokeWidth={2} />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<X size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
3
web/components/analytics/project-modal/index.ts
Normal file
3
web/components/analytics/project-modal/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./header";
|
||||
export * from "./main-content";
|
||||
export * from "./modal";
|
113
web/components/analytics/project-modal/main-content.tsx
Normal file
113
web/components/analytics/project-modal/main-content.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// components
|
||||
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
||||
// types
|
||||
import { ICycle, IModule, IProject, IWorkspace } from "types";
|
||||
// constants
|
||||
import { ANALYTICS_TABS } from "constants/analytics";
|
||||
|
||||
type Props = {
|
||||
fullScreen: boolean;
|
||||
cycleDetails: ICycle | undefined;
|
||||
moduleDetails: IModule | undefined;
|
||||
projectDetails: IProject | undefined;
|
||||
};
|
||||
|
||||
export const ProjectAnalyticsModalMainContent: React.FC<Props> = observer((props) => {
|
||||
const { fullScreen, cycleDetails, moduleDetails, projectDetails } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { user: userStore } = useMobxStore();
|
||||
|
||||
const user = userStore.currentUser;
|
||||
|
||||
const trackAnalyticsEvent = (tab: string) => {
|
||||
if (!workspaceSlug || !user) return;
|
||||
|
||||
const eventPayload: any = {
|
||||
workspaceSlug: workspaceSlug.toString(),
|
||||
};
|
||||
|
||||
if (projectDetails) {
|
||||
const workspaceDetails = projectDetails.workspace as IWorkspace;
|
||||
|
||||
eventPayload.workspaceId = workspaceDetails.id;
|
||||
eventPayload.workspaceName = workspaceDetails.name;
|
||||
eventPayload.projectId = projectDetails.id;
|
||||
eventPayload.projectIdentifier = projectDetails.identifier;
|
||||
eventPayload.projectName = projectDetails.name;
|
||||
}
|
||||
|
||||
if (cycleDetails || moduleDetails) {
|
||||
const details = cycleDetails || moduleDetails;
|
||||
|
||||
eventPayload.workspaceId = details?.workspace_detail?.id;
|
||||
eventPayload.workspaceName = details?.workspace_detail?.name;
|
||||
eventPayload.projectId = details?.project_detail.id;
|
||||
eventPayload.projectIdentifier = details?.project_detail.identifier;
|
||||
eventPayload.projectName = details?.project_detail.name;
|
||||
}
|
||||
|
||||
if (cycleDetails) {
|
||||
eventPayload.cycleId = cycleDetails.id;
|
||||
eventPayload.cycleName = cycleDetails.name;
|
||||
}
|
||||
|
||||
if (moduleDetails) {
|
||||
eventPayload.moduleId = moduleDetails.id;
|
||||
eventPayload.moduleName = moduleDetails.name;
|
||||
}
|
||||
|
||||
const eventType = tab === "scope_and_demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS";
|
||||
|
||||
trackEventServices.trackAnalyticsEvent(
|
||||
eventPayload,
|
||||
cycleDetails ? `CYCLE_${eventType}` : moduleDetails ? `MODULE_${eventType}` : `PROJECT_${eventType}`,
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tab.Group as={React.Fragment}>
|
||||
<Tab.List as="div" className="space-x-2 border-b border-custom-border-200 p-5 pt-0">
|
||||
{ANALYTICS_TABS.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
||||
selected ? "bg-custom-background-80" : ""
|
||||
}`
|
||||
}
|
||||
onClick={() => trackAnalyticsEvent(tab.key)}
|
||||
>
|
||||
{tab.title}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels as={React.Fragment}>
|
||||
<Tab.Panel as={React.Fragment}>
|
||||
<ScopeAndDemand fullScreen={fullScreen} />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as={React.Fragment}>
|
||||
<CustomAnalytics
|
||||
additionalParams={{
|
||||
cycle: cycleDetails?.id,
|
||||
module: moduleDetails?.id,
|
||||
}}
|
||||
fullScreen={fullScreen}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
});
|
70
web/components/analytics/project-modal/modal.tsx
Normal file
70
web/components/analytics/project-modal/modal.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
// components
|
||||
import { ProjectAnalyticsModalHeader, ProjectAnalyticsModalMainContent } from "components/analytics";
|
||||
// types
|
||||
import { ICycle, IModule, IProject } from "types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
cycleDetails?: ICycle | undefined;
|
||||
moduleDetails?: IModule | undefined;
|
||||
projectDetails?: IProject | undefined;
|
||||
};
|
||||
|
||||
export const ProjectAnalyticsModal: React.FC<Props> = observer((props) => {
|
||||
const { isOpen, onClose, cycleDetails, moduleDetails, projectDetails } = props;
|
||||
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root appear show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="transition-transform duration-300"
|
||||
enterFrom="translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-transform duration-200"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
{/* TODO: fix full screen mode */}
|
||||
<Dialog.Panel
|
||||
className={`fixed z-20 bg-custom-background-100 top-0 right-0 h-full shadow-custom-shadow-md ${
|
||||
fullScreen ? "w-full p-2" : "w-1/2"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-full flex-col overflow-hidden border-custom-border-200 bg-custom-background-100 text-left ${
|
||||
fullScreen ? "rounded-lg border" : "border-l"
|
||||
}`}
|
||||
>
|
||||
<ProjectAnalyticsModalHeader
|
||||
fullScreen={fullScreen}
|
||||
handleClose={handleClose}
|
||||
setFullScreen={setFullScreen}
|
||||
title={cycleDetails?.name ?? moduleDetails?.name ?? projectDetails?.name ?? ""}
|
||||
/>
|
||||
<ProjectAnalyticsModalMainContent
|
||||
fullScreen={fullScreen}
|
||||
cycleDetails={cycleDetails}
|
||||
moduleDetails={moduleDetails}
|
||||
projectDetails={projectDetails}
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
});
|
@ -17,12 +17,7 @@ type Props = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const AnalyticsLeaderboard: React.FC<Props> = ({
|
||||
users,
|
||||
title,
|
||||
emptyStateMessage,
|
||||
workspaceSlug,
|
||||
}) => (
|
||||
export const AnalyticsLeaderBoard: React.FC<Props> = ({ users, title, emptyStateMessage, workspaceSlug }) => (
|
||||
<div className="p-3 border border-custom-border-200 rounded-[10px]">
|
||||
<h6 className="text-base font-medium">{title}</h6>
|
||||
{users.length > 0 ? (
|
||||
|
@ -5,7 +5,7 @@ import useSWR from "swr";
|
||||
// services
|
||||
import analyticsService from "services/analytics.service";
|
||||
// components
|
||||
import { AnalyticsDemand, AnalyticsLeaderboard, AnalyticsScope, AnalyticsYearWiseIssues } from "components/analytics";
|
||||
import { AnalyticsDemand, AnalyticsLeaderBoard, AnalyticsScope, AnalyticsYearWiseIssues } from "components/analytics";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
// fetch-keys
|
||||
@ -15,7 +15,9 @@ type Props = {
|
||||
fullScreen?: boolean;
|
||||
};
|
||||
|
||||
export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
||||
export const ScopeAndDemand: React.FC<Props> = (props) => {
|
||||
const { fullScreen = true } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
@ -46,7 +48,7 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
||||
<div className={`grid grid-cols-1 gap-5 ${fullScreen ? "md:grid-cols-2" : ""}`}>
|
||||
<AnalyticsDemand defaultAnalytics={defaultAnalytics} />
|
||||
<AnalyticsScope defaultAnalytics={defaultAnalytics} />
|
||||
<AnalyticsLeaderboard
|
||||
<AnalyticsLeaderBoard
|
||||
users={defaultAnalytics.most_issue_created_user?.map((user) => ({
|
||||
avatar: user?.created_by__avatar,
|
||||
firstName: user?.created_by__first_name,
|
||||
@ -56,10 +58,10 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
||||
id: user?.created_by__id,
|
||||
}))}
|
||||
title="Most issues created"
|
||||
emptyStateMessage="Co-workers and the number issues created by them appears here."
|
||||
emptyStateMessage="Co-workers and the number of issues created by them appears here."
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
<AnalyticsLeaderboard
|
||||
<AnalyticsLeaderBoard
|
||||
users={defaultAnalytics.most_issue_closed_user?.map((user) => ({
|
||||
avatar: user?.assignees__avatar,
|
||||
firstName: user?.assignees__first_name,
|
||||
@ -69,7 +71,7 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
||||
id: user?.assignees__id,
|
||||
}))}
|
||||
title="Most issues closed"
|
||||
emptyStateMessage="Co-workers and the number issues closed by them appears here."
|
||||
emptyStateMessage="Co-workers and the number of issues closed by them appears here."
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
<div className={fullScreen ? "md:col-span-2" : ""}>
|
||||
|
@ -47,9 +47,7 @@ export const AllViews: React.FC = observer(() => {
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full flex flex-col overflow-auto">
|
||||
<div className="p-4">
|
||||
<AppliedFiltersRoot />
|
||||
</div>
|
||||
<AppliedFiltersRoot />
|
||||
<div className="w-full h-full">
|
||||
{activeLayout === "list" ? (
|
||||
<ListLayout />
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
@ -6,17 +6,25 @@ import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||
import { ProjectAnalyticsModal } from "components/analytics";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { Plus } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
|
||||
export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
cycle: cycleStore,
|
||||
cycleIssueFilter: cycleIssueFilterStore,
|
||||
project: projectStore,
|
||||
} = useMobxStore();
|
||||
@ -80,32 +88,60 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
cycleDetails={cycleDetails ?? undefined}
|
||||
/>
|
||||
<FiltersDropdown title="Filters">
|
||||
<FilterSelection
|
||||
filters={cycleIssueFilterStore.cycleFilters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="View">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
<FiltersDropdown title="Filters">
|
||||
<FilterSelection
|
||||
filters={cycleIssueFilterStore.cycleFilters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="View">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "c",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
@ -6,16 +6,28 @@ import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||
import { ProjectAnalyticsModal } from "components/analytics";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { Plus } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
|
||||
export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
|
||||
const { issueFilter: issueFilterStore, moduleFilter: moduleFilterStore, project: projectStore } = useMobxStore();
|
||||
const {
|
||||
issueFilter: issueFilterStore,
|
||||
module: moduleStore,
|
||||
moduleFilter: moduleFilterStore,
|
||||
project: projectStore,
|
||||
} = useMobxStore();
|
||||
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
|
||||
@ -76,32 +88,60 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
moduleDetails={moduleDetails ?? undefined}
|
||||
/>
|
||||
<FiltersDropdown title="Filters">
|
||||
<FilterSelection
|
||||
filters={moduleFilterStore.moduleFilters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="View">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
<FiltersDropdown title="Filters">
|
||||
<FilterSelection
|
||||
filters={moduleFilterStore.moduleFilters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="View">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "c",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
@ -6,12 +7,19 @@ import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||
import { ProjectAnalyticsModal } from "components/analytics";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { Plus } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
|
||||
export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
@ -78,32 +86,79 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const projectDetails =
|
||||
workspaceSlug && projectId
|
||||
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
<>
|
||||
<ProjectAnalyticsModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
projectDetails={projectDetails ?? undefined}
|
||||
/>
|
||||
<FiltersDropdown title="Filters">
|
||||
<FilterSelection
|
||||
filters={issueFilterStore.userFilters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="View">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
<FiltersDropdown title="Filters">
|
||||
<FilterSelection
|
||||
filters={issueFilterStore.userFilters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="View">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
displayProperties={issueFilterStore.userDisplayProperties}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
{/* TODO: add inbox redirection here */}
|
||||
{projectDetails?.inbox_view && (
|
||||
// <Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`}>
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/inboxId`}>
|
||||
<a>
|
||||
<Button variant="neutral-primary" size="sm">
|
||||
<span>Inbox</span>
|
||||
{/* {inboxList && inboxList?.[0]?.pending_issue_count !== 0 && (
|
||||
<span className="absolute -top-1 -right-1 h-4 w-4 rounded-full text-custom-text-100 bg-custom-sidebar-background-80 border border-custom-sidebar-border-200">
|
||||
{inboxList?.[0]?.pending_issue_count}
|
||||
</span>
|
||||
)} */}
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<Button onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "c",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
size="sm"
|
||||
prependIcon={<Plus />}
|
||||
>
|
||||
Add Issue
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import {
|
||||
CycleAppliedFiltersRoot,
|
||||
CycleCalendarLayout,
|
||||
CycleGanttLayout,
|
||||
CycleKanBanLayout,
|
||||
@ -51,18 +52,21 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
{activeLayout === "list" ? (
|
||||
<CycleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<CycleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CycleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<CycleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<CycleSpreadsheetLayout />
|
||||
) : null}
|
||||
<div className="relative w-full h-full flex flex-col overflow-auto">
|
||||
<CycleAppliedFiltersRoot />
|
||||
<div className="w-full h-full">
|
||||
{activeLayout === "list" ? (
|
||||
<CycleListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<CycleKanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CycleCalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<CycleGanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<CycleSpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -0,0 +1,77 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { AppliedFiltersList } from "components/issues";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "types";
|
||||
|
||||
export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
|
||||
const { project: projectStore, cycleIssueFilter: cycleIssueFilterStore } = useMobxStore();
|
||||
|
||||
const userFilters = cycleIssueFilterStore.cycleFilters;
|
||||
|
||||
// filters whose value not null or empty array
|
||||
const appliedFilters: IIssueFilterOptions = {};
|
||||
Object.entries(userFilters).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
|
||||
// remove all values of the key if value is null
|
||||
if (!value) {
|
||||
cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), {
|
||||
[key]: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the passed value from the key
|
||||
let newValues = cycleIssueFilterStore.cycleFilters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), {
|
||||
[key]: newValues,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
|
||||
cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId?.toString(), {
|
||||
...newFilters,
|
||||
});
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,3 +1,4 @@
|
||||
export * from "./cycle-root";
|
||||
export * from "./date";
|
||||
export * from "./filters-list";
|
||||
export * from "./global-views-root";
|
||||
|
@ -63,13 +63,15 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""]}
|
||||
/>
|
||||
<div className="p-4">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -67,13 +67,15 @@ export const AppliedFiltersRoot: React.FC = observer(() => {
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""]}
|
||||
/>
|
||||
<div className="p-4">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -2,6 +2,8 @@ import React, { Fragment, useState } from "react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { ChevronUp } from "lucide-react";
|
||||
|
||||
@ -28,20 +30,18 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Popover.Button as={React.Fragment}>
|
||||
<button
|
||||
type="button"
|
||||
className={`outline-none border border-custom-border-200 text-xs rounded flex items-center gap-2 px-2 py-1.5 hover:bg-custom-background-80 ${
|
||||
open ? "text-custom-text-100" : "text-custom-text-200"
|
||||
}`}
|
||||
<Button
|
||||
ref={setReferenceElement}
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
appendIcon={
|
||||
<ChevronUp className={`transition-all ${open ? "" : "rotate-180"}`} size={14} strokeWidth={2} />
|
||||
}
|
||||
>
|
||||
<div className="font-medium">{title}</div>
|
||||
<div
|
||||
className={`w-3.5 h-3.5 flex items-center justify-center transition-all ${open ? "" : "rotate-180"}`}
|
||||
>
|
||||
<ChevronUp width={14} strokeWidth={2} />
|
||||
<div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
</button>
|
||||
</Button>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
|
@ -51,9 +51,7 @@ export const ModuleAllLayouts: React.FC = observer(() => {
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full flex flex-col overflow-auto">
|
||||
<div className="p-4">
|
||||
<ModuleAppliedFiltersRoot />
|
||||
</div>
|
||||
<ModuleAppliedFiltersRoot />
|
||||
<div className="h-full w-full">
|
||||
{activeLayout === "list" ? (
|
||||
<ModuleListLayout />
|
||||
|
@ -1,9 +1,14 @@
|
||||
// types
|
||||
import { TXAxisValues, TYAxisValues } from "types";
|
||||
|
||||
export const ANALYTICS_TABS = [
|
||||
{ key: "scope_and_demand", title: "Scope and Demand" },
|
||||
{ key: "custom", title: "Custom Analytics" },
|
||||
];
|
||||
|
||||
export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] = [
|
||||
{
|
||||
value: "state__name",
|
||||
value: "state_id",
|
||||
label: "State name",
|
||||
},
|
||||
{
|
||||
@ -15,7 +20,7 @@ export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
|
||||
label: "Priority",
|
||||
},
|
||||
{
|
||||
value: "labels__name",
|
||||
value: "labels__id",
|
||||
label: "Label",
|
||||
},
|
||||
{
|
||||
@ -27,11 +32,11 @@ export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
|
||||
label: "Estimate point",
|
||||
},
|
||||
{
|
||||
value: "issue_cycle__cycle__name",
|
||||
value: "issue_cycle__cycle_id",
|
||||
label: "Cycle",
|
||||
},
|
||||
{
|
||||
value: "issue_module__module__name",
|
||||
value: "issue_module__module_id",
|
||||
label: "Module",
|
||||
},
|
||||
{
|
||||
|
@ -1,7 +1,7 @@
|
||||
// nivo
|
||||
import { BarDatum } from "@nivo/bar";
|
||||
// helpers
|
||||
import { capitalizeFirstLetter, generateRandomColor } from "helpers/string.helper";
|
||||
import { addSpaceIfCamelCase, capitalizeFirstLetter, generateRandomColor } from "helpers/string.helper";
|
||||
// types
|
||||
import { IAnalyticsData, IAnalyticsParams, IAnalyticsResponse, TStateGroups } from "types";
|
||||
// constants
|
||||
@ -69,8 +69,11 @@ export const generateBarColor = (
|
||||
|
||||
if (!analytics) return color;
|
||||
|
||||
if (params[type] === "state__name" || params[type] === "labels__name")
|
||||
color = analytics?.extras?.colors.find((c) => c.name === value)?.color;
|
||||
if (params[type] === "state_id")
|
||||
color = analytics?.extras.state_details.find((s) => s.state_id === value)?.state__color;
|
||||
|
||||
if (params[type] === "labels__id")
|
||||
color = analytics?.extras.label_details.find((l) => l.labels__id === value)?.labels__color ?? undefined;
|
||||
|
||||
if (params[type] === "state__group") color = STATE_GROUP_COLORS[value.toLowerCase() as TStateGroups];
|
||||
|
||||
@ -92,6 +95,42 @@ export const generateBarColor = (
|
||||
return color ?? generateRandomColor(value);
|
||||
};
|
||||
|
||||
export const generateDisplayName = (
|
||||
value: string,
|
||||
analytics: IAnalyticsResponse,
|
||||
params: IAnalyticsParams,
|
||||
type: "x_axis" | "segment"
|
||||
): string => {
|
||||
let displayName = addSpaceIfCamelCase(value);
|
||||
|
||||
if (!analytics) return displayName;
|
||||
|
||||
if (params[type] === "assignees__id")
|
||||
displayName =
|
||||
analytics?.extras.assignee_details.find((a) => a.assignees__id === value)?.assignees__display_name ??
|
||||
"No assignee";
|
||||
|
||||
if (params[type] === "issue_cycle__cycle_id")
|
||||
displayName =
|
||||
analytics?.extras.cycle_details.find((c) => c.issue_cycle__cycle_id === value)?.issue_cycle__cycle__name ??
|
||||
"None";
|
||||
|
||||
if (params[type] === "issue_module__module_id")
|
||||
displayName =
|
||||
analytics?.extras.module_details.find((m) => m.issue_module__module_id === value)?.issue_module__module__name ??
|
||||
"None";
|
||||
|
||||
if (params[type] === "labels__id")
|
||||
displayName = analytics?.extras.label_details.find((l) => l.labels__id === value)?.labels__name ?? "None";
|
||||
|
||||
if (params[type] === "state_id")
|
||||
displayName = analytics?.extras.state_details.find((s) => s.state_id === value)?.state__name ?? "None";
|
||||
|
||||
if (DATE_KEYS.includes(params.segment ?? "")) displayName = renderMonthAndYear(value);
|
||||
|
||||
return displayName;
|
||||
};
|
||||
|
||||
export const renderMonthAndYear = (date: string | number | null): string => {
|
||||
if (!date || date === "") return "";
|
||||
|
||||
|
@ -1,58 +1,45 @@
|
||||
import React, { Fragment, useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// react-hook-form
|
||||
|
||||
// headless ui
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import analyticsService from "services/analytics.service";
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout-legacy";
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
||||
import { WorkspaceAnalyticsHeader } from "components/headers";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
import { EmptyState } from "components/ui";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
// images
|
||||
// assets
|
||||
import emptyAnalytics from "public/empty-state/analytics.svg";
|
||||
// types
|
||||
import { IAnalyticsParams } from "types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS } from "constants/fetch-keys";
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { WorkspaceAnalyticsHeader } from "components/headers/workspace-analytics";
|
||||
|
||||
const tabsList = ["Scope and Demand", "Custom Analytics"];
|
||||
// constants
|
||||
import { ANALYTICS_TABS } from "constants/analytics";
|
||||
|
||||
const AnalyticsPage = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// store
|
||||
const { workspace: workspaceStore, project: projectStore, user: userStore } = useMobxStore();
|
||||
const { project: projectStore, user: userStore } = useMobxStore();
|
||||
|
||||
const user = userStore.currentUser;
|
||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug?.toString()] : null;
|
||||
|
||||
// const { user } = useUserAuth();
|
||||
// const { projects } = useProjects();
|
||||
|
||||
const trackAnalyticsEvent = (tab: string) => {
|
||||
if (!user) return;
|
||||
|
||||
const eventPayload = {
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
};
|
||||
|
||||
const eventType =
|
||||
tab === "Scope and Demand" ? "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" : "WORKSPACE_CUSTOM_ANALYTICS";
|
||||
tab === "scope_and_demand" ? "WORKSPACE_SCOPE_AND_DEMAND_ANALYTICS" : "WORKSPACE_CUSTOM_ANALYTICS";
|
||||
|
||||
trackEventServices.trackAnalyticsEvent(eventPayload, eventType, user);
|
||||
};
|
||||
@ -69,101 +56,36 @@ const AnalyticsPage = observer(() => {
|
||||
}, [user, workspaceSlug]);
|
||||
|
||||
return (
|
||||
// <WorkspaceAuthorizationLayout
|
||||
// breadcrumbs={
|
||||
// <Breadcrumbs>
|
||||
// <BreadcrumbItem title="Workspace Analytics" />
|
||||
// </Breadcrumbs>
|
||||
// }
|
||||
// >
|
||||
// {projects ? (
|
||||
// projects.length > 0 ? (
|
||||
// <div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
|
||||
// <Tab.Group as={Fragment}>
|
||||
// <Tab.List as="div" className="space-x-2 border-b border-custom-border-200 px-5 py-3">
|
||||
// {tabsList.map((tab) => (
|
||||
// <Tab
|
||||
// key={tab}
|
||||
// className={({ selected }) =>
|
||||
// `rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
||||
// selected ? "bg-custom-background-80" : ""
|
||||
// }`
|
||||
// }
|
||||
// onClick={() => trackAnalyticsEvent(tab)}
|
||||
// >
|
||||
// {tab}
|
||||
// </Tab>
|
||||
// ))}
|
||||
// </Tab.List>
|
||||
// <Tab.Panels as={Fragment}>
|
||||
// <Tab.Panel as={Fragment}>
|
||||
// <ScopeAndDemand fullScreen />
|
||||
// </Tab.Panel>
|
||||
// <Tab.Panel as={Fragment}>
|
||||
// <CustomAnalytics
|
||||
// analytics={analytics}
|
||||
// analyticsError={analyticsError}
|
||||
// params={params}
|
||||
// control={control}
|
||||
// setValue={setValue}
|
||||
// user={user}
|
||||
// fullScreen
|
||||
// />
|
||||
// </Tab.Panel>
|
||||
// </Tab.Panels>
|
||||
// </Tab.Group>
|
||||
// </div>
|
||||
// ) : (
|
||||
// <EmptyState
|
||||
// title="You can see your all projects' analytics here"
|
||||
// description="Let's create your first project and analyze the stats with various graphs."
|
||||
// image={emptyAnalytics}
|
||||
// primaryButton={{
|
||||
// icon: <PlusIcon className="h-4 w-4" />,
|
||||
// text: "New Project",
|
||||
// onClick: () => {
|
||||
// const e = new KeyboardEvent("keydown", {
|
||||
// key: "p",
|
||||
// });
|
||||
// document.dispatchEvent(e);
|
||||
// },
|
||||
// }}
|
||||
// />
|
||||
// )
|
||||
// ) : null}
|
||||
// </WorkspaceAuthorizationLayout>
|
||||
<AppLayout header={<WorkspaceAnalyticsHeader />}>
|
||||
<>
|
||||
{projects && projects.length > 0 ? (
|
||||
<>
|
||||
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
|
||||
<Tab.Group as={Fragment}>
|
||||
<Tab.List as="div" className="space-x-2 border-b border-custom-border-200 px-5 py-3">
|
||||
{tabsList.map((tab) => (
|
||||
<Tab
|
||||
key={tab}
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
||||
selected ? "bg-custom-background-80" : ""
|
||||
}`
|
||||
}
|
||||
onClick={() => trackAnalyticsEvent(tab)}
|
||||
>
|
||||
{tab}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<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>
|
||||
</>
|
||||
<div className="h-full flex flex-col overflow-hidden bg-custom-background-100">
|
||||
<Tab.Group as={Fragment}>
|
||||
<Tab.List as="div" className="space-x-2 border-b border-custom-border-200 px-5 py-3">
|
||||
{ANALYTICS_TABS.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
||||
selected ? "bg-custom-background-80" : ""
|
||||
}`
|
||||
}
|
||||
onClick={() => trackAnalyticsEvent(tab.key)}
|
||||
>
|
||||
{tab.title}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<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>
|
||||
) : (
|
||||
<>
|
||||
<EmptyState
|
||||
|
@ -130,9 +130,6 @@ const SingleCycle: React.FC = () => {
|
||||
right={
|
||||
<div className={`flex flex-shrink-0 items-center gap-2 duration-300`}>
|
||||
<CycleIssuesHeader />
|
||||
<Button variant="neutral-primary" onClick={() => setAnalyticsModal(true)}>
|
||||
Analytics
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
|
||||
@ -159,8 +156,6 @@ const SingleCycle: React.FC = () => {
|
||||
<>
|
||||
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
|
||||
|
||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||
|
||||
<div
|
||||
className={`relative w-full h-full flex flex-col overflow-auto ${cycleSidebar ? "mr-[24rem]" : ""} ${
|
||||
analyticsModal ? "mr-[50%]" : ""
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
@ -8,27 +6,21 @@ import useSWR from "swr";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
import inboxService from "services/inbox.service";
|
||||
// layouts
|
||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// components
|
||||
import { AllViews } from "components/core";
|
||||
import { AnalyticsProjectModal } from "components/analytics";
|
||||
import { ProjectIssuesHeader } from "components/headers";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { PROJECT_DETAILS, INBOX_LIST } from "constants/fetch-keys";
|
||||
import { PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
const ProjectIssues: NextPage = () => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
@ -39,11 +31,6 @@ const ProjectIssues: NextPage = () => {
|
||||
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
|
||||
);
|
||||
|
||||
const { data: inboxList } = useSWR(
|
||||
workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId ? () => inboxService.getInboxes(workspaceSlug as string, projectId as string) : null
|
||||
);
|
||||
|
||||
// TODO: update the fetch keys
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? "REVALIDATE_USER_PROJECT_FILTERS" : null,
|
||||
@ -81,48 +68,9 @@ const ProjectIssues: NextPage = () => {
|
||||
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`} />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
right={
|
||||
<ProjectIssuesHeader />
|
||||
// <div className="flex items-center gap-2">
|
||||
// <SecondaryButton
|
||||
// onClick={() => setAnalyticsModal(true)}
|
||||
// className="!py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
|
||||
// outline
|
||||
// >
|
||||
// Analytics
|
||||
// </SecondaryButton>
|
||||
// {projectDetails && projectDetails.inbox_view && (
|
||||
// <Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`}>
|
||||
// <a>
|
||||
// <SecondaryButton
|
||||
// className="relative !py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
|
||||
// outline
|
||||
// >
|
||||
// <span>Inbox</span>
|
||||
// {inboxList && inboxList?.[0]?.pending_issue_count !== 0 && (
|
||||
// <span className="absolute -top-1 -right-1 h-4 w-4 rounded-full text-custom-text-100 bg-custom-sidebar-background-80 border border-custom-sidebar-border-200">
|
||||
// {inboxList?.[0]?.pending_issue_count}
|
||||
// </span>
|
||||
// )}
|
||||
// </SecondaryButton>
|
||||
// </a>
|
||||
// </Link>
|
||||
// )}
|
||||
// <PrimaryButton
|
||||
// className="flex items-center gap-2"
|
||||
// onClick={() => {
|
||||
// const e = new KeyboardEvent("keydown", { key: "c" });
|
||||
// document.dispatchEvent(e);
|
||||
// }}
|
||||
// >
|
||||
// <PlusIcon className="h-4 w-4" />
|
||||
// Add Issue
|
||||
// </PrimaryButton>
|
||||
// </div>
|
||||
}
|
||||
right={<ProjectIssuesHeader />}
|
||||
bg="secondary"
|
||||
>
|
||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<AllViews />
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@ import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
|
||||
// icons
|
||||
import { ArrowLeftIcon, RectangleGroupIcon } from "@heroicons/react/24/outline";
|
||||
import { RectangleGroupIcon } from "@heroicons/react/24/outline";
|
||||
// services
|
||||
import modulesService from "services/modules.service";
|
||||
// hooks
|
||||
@ -14,9 +14,8 @@ import useUserAuth from "hooks/use-user-auth";
|
||||
// layouts
|
||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
|
||||
// components
|
||||
import { ExistingIssuesListModal, IssuesFilterView } from "components/core";
|
||||
import { ExistingIssuesListModal } from "components/core";
|
||||
import { ModuleDetailsSidebar } from "components/modules";
|
||||
import { AnalyticsProjectModal } from "components/analytics";
|
||||
// ui
|
||||
import { CustomMenu, EmptyState } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
@ -34,7 +33,6 @@ import { ModuleIssuesHeader } from "components/headers";
|
||||
const SingleModule: React.FC = () => {
|
||||
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
||||
const [moduleSidebar, setModuleSidebar] = useState(false);
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
@ -138,10 +136,9 @@ const SingleModule: React.FC = () => {
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||
<div
|
||||
className={`relative overflow-y-auto h-full flex flex-col ${moduleSidebar ? "mr-[24rem]" : ""} ${
|
||||
analyticsModal ? "mr-[50%]" : ""
|
||||
className={`relative overflow-y-auto h-full flex flex-col ${
|
||||
moduleSidebar ? "mr-[24rem]" : ""
|
||||
} duration-300`}
|
||||
>
|
||||
<ModuleAllLayouts />
|
||||
|
@ -1,12 +1,11 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
const trackEvent = process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
// types
|
||||
import type {
|
||||
ICurrentUserResponse,
|
||||
IUser,
|
||||
ICycle,
|
||||
IEstimate,
|
||||
IGptResponse,
|
||||
@ -17,7 +16,7 @@ import type {
|
||||
IPageBlock,
|
||||
IProject,
|
||||
IState,
|
||||
IView,
|
||||
IProjectView,
|
||||
IWorkspace,
|
||||
IssueCommentReaction,
|
||||
IssueReaction,
|
||||
@ -50,18 +49,9 @@ type PagesEventType = "PAGE_CREATE" | "PAGE_UPDATE" | "PAGE_DELETE";
|
||||
|
||||
type ViewEventType = "VIEW_CREATE" | "VIEW_UPDATE" | "VIEW_DELETE";
|
||||
|
||||
type IssueCommentEventType =
|
||||
| "ISSUE_COMMENT_CREATE"
|
||||
| "ISSUE_COMMENT_UPDATE"
|
||||
| "ISSUE_COMMENT_DELETE";
|
||||
type IssueCommentEventType = "ISSUE_COMMENT_CREATE" | "ISSUE_COMMENT_UPDATE" | "ISSUE_COMMENT_DELETE";
|
||||
|
||||
type Toggle =
|
||||
| "TOGGLE_CYCLE"
|
||||
| "TOGGLE_MODULE"
|
||||
| "TOGGLE_VIEW"
|
||||
| "TOGGLE_PAGES"
|
||||
| "TOGGLE_STATE"
|
||||
| "TOGGLE_INBOX";
|
||||
type Toggle = "TOGGLE_CYCLE" | "TOGGLE_MODULE" | "TOGGLE_VIEW" | "TOGGLE_PAGES" | "TOGGLE_STATE" | "TOGGLE_INBOX";
|
||||
|
||||
export type MiscellaneousEventType = `${Toggle}_ON` | `${Toggle}_OFF`;
|
||||
|
||||
@ -126,11 +116,7 @@ class TrackEventServices extends APIService {
|
||||
super("/");
|
||||
}
|
||||
|
||||
async trackWorkspaceEvent(
|
||||
data: IWorkspace | any,
|
||||
eventName: WorkspaceEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackWorkspaceEvent(data: IWorkspace | any, eventName: WorkspaceEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
let payload: any;
|
||||
@ -160,19 +146,11 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackProjectEvent(
|
||||
data: Partial<IProject> | any,
|
||||
eventName: ProjectEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackProjectEvent(data: Partial<IProject> | any, eventName: ProjectEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
let payload: any;
|
||||
if (
|
||||
eventName !== "DELETE_PROJECT" &&
|
||||
eventName !== "PROJECT_MEMBER_INVITE" &&
|
||||
eventName !== "PROJECT_MEMBER_LEAVE"
|
||||
)
|
||||
if (eventName !== "DELETE_PROJECT" && eventName !== "PROJECT_MEMBER_INVITE" && eventName !== "PROJECT_MEMBER_LEAVE")
|
||||
payload = {
|
||||
workspaceId: data?.workspace_detail?.id,
|
||||
workspaceName: data?.workspace_detail?.name,
|
||||
@ -195,10 +173,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackUserOnboardingCompleteEvent(
|
||||
data: any,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackUserOnboardingCompleteEvent(data: any, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
return this.request({
|
||||
@ -214,10 +189,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackUserTourCompleteEvent(
|
||||
data: any,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackUserTourCompleteEvent(data: any, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
return this.request({
|
||||
@ -233,11 +205,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackIssueEvent(
|
||||
data: IIssue | any,
|
||||
eventName: IssueEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackIssueEvent(data: IIssue | any, eventName: IssueEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
let payload: any;
|
||||
@ -266,10 +234,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackIssueMarkedAsDoneEvent(
|
||||
data: any,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackIssueMarkedAsDoneEvent(data: any, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
@ -292,7 +257,7 @@ class TrackEventServices extends APIService {
|
||||
| "ISSUE_PROPERTY_UPDATE_ASSIGNEE"
|
||||
| "ISSUE_PROPERTY_UPDATE_DUE_DATE"
|
||||
| "ISSUE_PROPERTY_UPDATE_ESTIMATE",
|
||||
user: ICurrentUserResponse | undefined
|
||||
user: IUser
|
||||
): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
return this.request({
|
||||
@ -311,7 +276,7 @@ class TrackEventServices extends APIService {
|
||||
async trackIssueCommentEvent(
|
||||
data: Partial<IIssueComment> | any,
|
||||
eventName: IssueCommentEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
user: IUser
|
||||
): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
@ -343,7 +308,7 @@ class TrackEventServices extends APIService {
|
||||
async trackIssueRelationEvent(
|
||||
data: any,
|
||||
eventName: "ISSUE_RELATION_CREATE" | "ISSUE_RELATION_DELETE",
|
||||
user: ICurrentUserResponse
|
||||
user: IUser
|
||||
): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
@ -365,7 +330,7 @@ class TrackEventServices extends APIService {
|
||||
| "ISSUE_MOVED_TO_MODULE"
|
||||
| "ISSUE_MOVED_TO_CYCLE_IN_BULK"
|
||||
| "ISSUE_MOVED_TO_MODULE_IN_BULK",
|
||||
user: ICurrentUserResponse | undefined
|
||||
user: IUser
|
||||
): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
@ -382,7 +347,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackIssueBulkDeleteEvent(data: any, user: ICurrentUserResponse | undefined): Promise<any> {
|
||||
async trackIssueBulkDeleteEvent(data: any, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
return this.request({
|
||||
@ -398,11 +363,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackIssueLabelEvent(
|
||||
data: any,
|
||||
eventName: IssueLabelEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackIssueLabelEvent(data: any, eventName: IssueLabelEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
return this.request({
|
||||
@ -418,11 +379,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackStateEvent(
|
||||
data: IState | any,
|
||||
eventName: StateEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackStateEvent(data: IState | any, eventName: StateEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
let payload: any;
|
||||
@ -451,11 +408,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackCycleEvent(
|
||||
data: ICycle | any,
|
||||
eventName: CycleEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackCycleEvent(data: ICycle | any, eventName: CycleEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
let payload: any;
|
||||
@ -484,11 +437,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackModuleEvent(
|
||||
data: IModule | any,
|
||||
eventName: ModuleEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackModuleEvent(data: IModule | any, eventName: ModuleEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
let payload: any;
|
||||
@ -517,11 +466,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackPageEvent(
|
||||
data: Partial<IPage> | any,
|
||||
eventName: PagesEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackPageEvent(data: Partial<IPage> | any, eventName: PagesEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
let payload: any;
|
||||
@ -553,7 +498,7 @@ class TrackEventServices extends APIService {
|
||||
async trackPageBlockEvent(
|
||||
data: Partial<IPageBlock> | IIssue,
|
||||
eventName: PageBlocksEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
user: IUser
|
||||
): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
@ -594,11 +539,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackAskGptEvent(
|
||||
data: IGptResponse,
|
||||
eventName: GptEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackAskGptEvent(data: IGptResponse, eventName: GptEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
const payload = {
|
||||
@ -623,11 +564,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackUseGPTResponseEvent(
|
||||
data: IIssue | IPageBlock,
|
||||
eventName: GptEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackUseGPTResponseEvent(data: IIssue | IPageBlock, eventName: GptEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
let payload: any;
|
||||
@ -668,11 +605,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackViewEvent(
|
||||
data: IView,
|
||||
eventName: ViewEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackViewEvent(data: IProjectView, eventName: ViewEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
let payload: any;
|
||||
@ -699,11 +632,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackMiscellaneousEvent(
|
||||
data: any,
|
||||
eventName: MiscellaneousEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackMiscellaneousEvent(data: any, eventName: MiscellaneousEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
return this.request({
|
||||
@ -719,11 +648,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackAppIntegrationEvent(
|
||||
data: any,
|
||||
eventName: IntegrationEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackAppIntegrationEvent(data: any, eventName: IntegrationEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
return this.request({
|
||||
@ -739,11 +664,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackGitHubSyncEvent(
|
||||
data: any,
|
||||
eventName: GitHubSyncEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackGitHubSyncEvent(data: any, eventName: GitHubSyncEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
return this.request({
|
||||
@ -762,7 +683,7 @@ class TrackEventServices extends APIService {
|
||||
async trackIssueEstimateEvent(
|
||||
data: { estimate: IEstimate },
|
||||
eventName: IssueEstimateEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
user: IUser
|
||||
): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
@ -792,16 +713,11 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackImporterEvent(
|
||||
data: any,
|
||||
eventName: ImporterEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackImporterEvent(data: any, eventName: ImporterEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
let payload: any;
|
||||
if (eventName === "GITHUB_IMPORTER_DELETE" || eventName === "JIRA_IMPORTER_DELETE")
|
||||
payload = data;
|
||||
if (eventName === "GITHUB_IMPORTER_DELETE" || eventName === "JIRA_IMPORTER_DELETE") payload = data;
|
||||
else
|
||||
payload = {
|
||||
workspaceId: data?.workspace_detail?.id,
|
||||
@ -825,11 +741,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackAnalyticsEvent(
|
||||
data: any,
|
||||
eventName: AnalyticsEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackAnalyticsEvent(data: any, eventName: AnalyticsEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
const payload = { ...data };
|
||||
@ -845,11 +757,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackExporterEvent(
|
||||
data: any,
|
||||
eventName: ExporterEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackExporterEvent(data: any, eventName: ExporterEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
const payload = { ...data };
|
||||
@ -868,11 +776,7 @@ class TrackEventServices extends APIService {
|
||||
}
|
||||
|
||||
// TODO: add types to the data
|
||||
async trackInboxEvent(
|
||||
data: any,
|
||||
eventName: InboxEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackInboxEvent(data: any, eventName: InboxEventType, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
let payload: any;
|
||||
@ -904,13 +808,12 @@ class TrackEventServices extends APIService {
|
||||
async trackReactionEvent(
|
||||
data: IssueReaction | IssueCommentReaction,
|
||||
eventName: ReactionEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
user: IUser
|
||||
): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
let payload: any;
|
||||
if (eventName === "ISSUE_REACTION_DELETE" || eventName === "ISSUE_COMMENT_REACTION_DELETE")
|
||||
payload = data;
|
||||
if (eventName === "ISSUE_REACTION_DELETE" || eventName === "ISSUE_COMMENT_REACTION_DELETE") payload = data;
|
||||
else
|
||||
payload = {
|
||||
workspaceId: data?.workspace,
|
||||
@ -929,11 +832,7 @@ class TrackEventServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async trackProjectPublishSettingsEvent(
|
||||
data: any,
|
||||
eventName: string,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
async trackProjectPublishSettingsEvent(data: any, eventName: string, user: IUser): Promise<any> {
|
||||
if (!trackEvent) return;
|
||||
|
||||
const payload: any = data;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { action, observable, makeObservable, runInAction } from "mobx";
|
||||
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
||||
// services
|
||||
import { ProjectService } from "services/project.service";
|
||||
import { ModuleService } from "services/modules.service";
|
||||
@ -8,9 +8,11 @@ import { IIssue, IModule } from "types";
|
||||
import { IIssueGroupWithSubGroupsStructure, IIssueGroupedStructure, IIssueUnGroupedStructure } from "./issue";
|
||||
|
||||
export interface IModuleStore {
|
||||
// states
|
||||
loader: boolean;
|
||||
error: any | null;
|
||||
|
||||
// observables
|
||||
moduleId: string | null;
|
||||
modules: {
|
||||
[project_id: string]: IModule[];
|
||||
@ -26,33 +28,37 @@ export interface IModuleStore {
|
||||
};
|
||||
};
|
||||
|
||||
// actions
|
||||
setModuleId: (moduleSlug: string) => void;
|
||||
|
||||
getModuleById: (moduleId: string) => IModule | null;
|
||||
|
||||
fetchModules: (workspaceSlug: string, projectId: string) => void;
|
||||
fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => void;
|
||||
|
||||
// crud operations
|
||||
createModule: (workspaceSlug: string, projectId: string, data: Partial<IModule>) => Promise<IModule>;
|
||||
updateModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string, data: Partial<IModule>) => void;
|
||||
deleteModule: (workspaceSlug: string, projectId: string, moduleId: string) => void;
|
||||
addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => void;
|
||||
removeModuleFromFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => void;
|
||||
|
||||
// computed
|
||||
projectModules: IModule[] | null;
|
||||
}
|
||||
|
||||
class ModuleStore implements IModuleStore {
|
||||
// states
|
||||
loader: boolean = false;
|
||||
error: any | null = null;
|
||||
|
||||
// observables
|
||||
moduleId: string | null = null;
|
||||
|
||||
modules: {
|
||||
[project_id: string]: IModule[];
|
||||
} = {};
|
||||
|
||||
moduleDetails: {
|
||||
[module_id: string]: IModule;
|
||||
} = {};
|
||||
|
||||
issues: {
|
||||
[module_id: string]: {
|
||||
grouped: {
|
||||
@ -69,15 +75,18 @@ class ModuleStore implements IModuleStore {
|
||||
|
||||
// root store
|
||||
rootStore;
|
||||
|
||||
// services
|
||||
projectService;
|
||||
moduleService;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// states
|
||||
loader: observable,
|
||||
error: observable.ref,
|
||||
|
||||
// observables
|
||||
moduleId: observable.ref,
|
||||
modules: observable.ref,
|
||||
moduleDetails: observable.ref,
|
||||
@ -86,6 +95,8 @@ class ModuleStore implements IModuleStore {
|
||||
// actions
|
||||
setModuleId: action,
|
||||
|
||||
getModuleById: action,
|
||||
|
||||
fetchModules: action,
|
||||
fetchModuleDetails: action,
|
||||
|
||||
@ -94,9 +105,14 @@ class ModuleStore implements IModuleStore {
|
||||
deleteModule: action,
|
||||
addModuleToFavorites: action,
|
||||
removeModuleFromFavorites: action,
|
||||
|
||||
// computed
|
||||
projectModules: computed,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
|
||||
// services
|
||||
this.projectService = new ProjectService();
|
||||
this.moduleService = new ModuleService();
|
||||
}
|
||||
@ -104,9 +120,12 @@ class ModuleStore implements IModuleStore {
|
||||
// computed
|
||||
get projectModules() {
|
||||
if (!this.rootStore.project.projectId) return null;
|
||||
|
||||
return this.modules[this.rootStore.project.projectId] || null;
|
||||
}
|
||||
|
||||
getModuleById = (moduleId: string) => this.moduleDetails[moduleId] || null;
|
||||
|
||||
// actions
|
||||
setModuleId = (moduleSlug: string) => {
|
||||
this.moduleId = moduleSlug ?? null;
|
||||
|
52
web/types/analytics.d.ts
vendored
52
web/types/analytics.d.ts
vendored
@ -2,14 +2,11 @@ export interface IAnalyticsResponse {
|
||||
total: number;
|
||||
distribution: IAnalyticsData;
|
||||
extras: {
|
||||
colors: IAnalyticsExtra[];
|
||||
assignee_details: {
|
||||
assignees__id: string | null;
|
||||
assignees__display_name: string | null;
|
||||
assignees__avatar: string | null;
|
||||
assignees__first_name: string;
|
||||
assignees__last_name: string;
|
||||
}[];
|
||||
assignee_details: IAnalyticsAssigneeDetails[];
|
||||
cycle_details: IAnalyticsCycleDetails[];
|
||||
label_details: IAnalyticsLabelDetails[];
|
||||
module_details: IAnalyticsModuleDetails[];
|
||||
state_details: IAnalyticsStateDetails[];
|
||||
};
|
||||
}
|
||||
|
||||
@ -22,19 +19,44 @@ export interface IAnalyticsData {
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface IAnalyticsExtra {
|
||||
name: string;
|
||||
color: string;
|
||||
export interface IAnalyticsAssigneeDetails {
|
||||
assignees__avatar: string | null;
|
||||
assignees__display_name: string | null;
|
||||
assignees__first_name: string;
|
||||
assignees__id: string | null;
|
||||
assignees__last_name: string;
|
||||
}
|
||||
|
||||
export interface IAnalyticsCycleDetails {
|
||||
issue_cycle__cycle__name: string | null;
|
||||
issue_cycle__cycle_id: string | null;
|
||||
}
|
||||
|
||||
export interface IAnalyticsLabelDetails {
|
||||
labels__color: string | null;
|
||||
labels__id: string | null;
|
||||
labels__name: string | null;
|
||||
}
|
||||
|
||||
export interface IAnalyticsModuleDetails {
|
||||
issue_module__module__name: string | null;
|
||||
issue_module__module_id: string | null;
|
||||
}
|
||||
|
||||
export interface IAnalyticsStateDetails {
|
||||
state__color: string;
|
||||
state__name: string;
|
||||
state_id: string;
|
||||
}
|
||||
|
||||
export type TXAxisValues =
|
||||
| "state__name"
|
||||
| "state_id"
|
||||
| "state__group"
|
||||
| "labels__name"
|
||||
| "labels__id"
|
||||
| "assignees__id"
|
||||
| "estimate_point"
|
||||
| "issue_cycle__cycle__name"
|
||||
| "issue_module__module__name"
|
||||
| "issue_cycle__cycle_id"
|
||||
| "issue_module__module_id"
|
||||
| "priority"
|
||||
| "start_date"
|
||||
| "target_date"
|
||||
|
Loading…
Reference in New Issue
Block a user