chore: inbox events

This commit is contained in:
LAKHAN BAHETI 2024-04-30 18:23:16 +05:30
parent a28cca29d5
commit aab34ff36d
16 changed files with 296 additions and 101 deletions

View File

@ -8,8 +8,10 @@ import { Breadcrumbs, Button, LayersIcon } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
import { InboxIssueCreateEditModalRoot } from "@/components/inbox"; import { InboxIssueCreateEditModalRoot } from "@/components/inbox";
import { ProjectLogo } from "@/components/project"; import { ProjectLogo } from "@/components/project";
// constants
import { E_INBOX } from "@/constants/event-tracker";
// hooks // hooks
import { useProject, useProjectInbox } from "@/hooks/store"; import { useEventTracker, useProject, useProjectInbox } from "@/hooks/store";
export const ProjectInboxHeader: FC = observer(() => { export const ProjectInboxHeader: FC = observer(() => {
// states // states
@ -20,6 +22,7 @@ export const ProjectInboxHeader: FC = observer(() => {
// store hooks // store hooks
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { loader } = useProjectInbox(); const { loader } = useProjectInbox();
const { setTrackElement } = useEventTracker();
return ( return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4"> <div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
@ -70,7 +73,15 @@ export const ProjectInboxHeader: FC = observer(() => {
issue={undefined} issue={undefined}
/> />
<Button variant="primary" prependIcon={<Plus />} size="sm" onClick={() => setCreateIssueModal(true)}> <Button
variant="primary"
prependIcon={<Plus />}
size="sm"
onClick={() => {
setTrackElement(E_INBOX);
setCreateIssueModal(true);
}}
>
Add Issue Add Issue
</Button> </Button>
</div> </div>

View File

@ -25,12 +25,13 @@ import {
} from "@/components/inbox"; } from "@/components/inbox";
import { IssueUpdateStatus } from "@/components/issues"; import { IssueUpdateStatus } from "@/components/issues";
// constants // constants
import { INBOX_ISSUE_DELETED, INBOX_ISSUE_UPDATED } from "@/constants/event-tracker";
import { EUserProjectRoles } from "@/constants/project"; import { EUserProjectRoles } from "@/constants/project";
// helpers // helpers
import { EInboxIssueStatus } from "@/helpers/inbox.helper"; import { EInboxIssueStatus } from "@/helpers/inbox.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks // hooks
import { useUser, useProjectInbox, useProject } from "@/hooks/store"; import { useUser, useProjectInbox, useProject, useEventTracker } from "@/hooks/store";
// store types // store types
import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store"; import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
@ -60,6 +61,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
const router = useRouter(); const router = useRouter();
const { getProjectById } = useProject(); const { getProjectById } = useProject();
const { captureEvent } = useEventTracker();
const issue = inboxIssue?.issue; const issue = inboxIssue?.issue;
// derived values // derived values
@ -98,6 +100,10 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
await inboxIssue?.updateInboxIssueStatus(EInboxIssueStatus.ACCEPTED); await inboxIssue?.updateInboxIssueStatus(EInboxIssueStatus.ACCEPTED);
setAcceptIssueModal(false); setAcceptIssueModal(false);
handleRedirection(nextOrPreviousIssueId); handleRedirection(nextOrPreviousIssueId);
captureEvent(INBOX_ISSUE_UPDATED, {
issue_status: "accepted",
issue_id: currentInboxIssueId,
});
}; };
const handleInboxIssueDecline = async () => { const handleInboxIssueDecline = async () => {
@ -105,6 +111,10 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
await inboxIssue?.updateInboxIssueStatus(EInboxIssueStatus.DECLINED); await inboxIssue?.updateInboxIssueStatus(EInboxIssueStatus.DECLINED);
setDeclineIssueModal(false); setDeclineIssueModal(false);
handleRedirection(nextOrPreviousIssueId); handleRedirection(nextOrPreviousIssueId);
captureEvent(INBOX_ISSUE_UPDATED, {
issue_status: "declined",
issue_id: currentInboxIssueId,
});
}; };
const handleInboxSIssueSnooze = async (date: Date) => { const handleInboxSIssueSnooze = async (date: Date) => {
@ -112,14 +122,25 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
await inboxIssue?.updateInboxIssueSnoozeTill(date); await inboxIssue?.updateInboxIssueSnoozeTill(date);
setIsSnoozeDateModalOpen(false); setIsSnoozeDateModalOpen(false);
handleRedirection(nextOrPreviousIssueId); handleRedirection(nextOrPreviousIssueId);
captureEvent(INBOX_ISSUE_UPDATED, {
issue_status: "snoozed",
issue_id: currentInboxIssueId,
});
}; };
const handleInboxIssueDuplicate = async (issueId: string) => { const handleInboxIssueDuplicate = async (issueId: string) => {
await inboxIssue?.updateInboxIssueDuplicateTo(issueId); await inboxIssue?.updateInboxIssueDuplicateTo(issueId);
captureEvent(INBOX_ISSUE_UPDATED, {
issue_status: "mark as duplicate",
issue_id: currentInboxIssueId,
});
}; };
const handleInboxIssueDelete = async () => { const handleInboxIssueDelete = async () => {
if (!inboxIssue || !currentInboxIssueId) return; if (!inboxIssue || !currentInboxIssueId) return;
captureEvent(INBOX_ISSUE_DELETED, {
issue_id: currentInboxIssueId,
});
await deleteInboxIssue(workspaceSlug, projectId, currentInboxIssueId).finally(() => { await deleteInboxIssue(workspaceSlug, projectId, currentInboxIssueId).finally(() => {
router.push(`/${workspaceSlug}/projects/${projectId}/inbox`); router.push(`/${workspaceSlug}/projects/${projectId}/inbox`);
}); });

View File

@ -13,6 +13,8 @@ import {
TIssueOperations, TIssueOperations,
IssueAttachmentRoot, IssueAttachmentRoot,
} from "@/components/issues"; } from "@/components/issues";
// constants
import { E_INBOX, INBOX_ISSUE_UPDATED } from "@/constants/event-tracker";
// hooks // hooks
import { useEventTracker, useProjectInbox, useUser } from "@/hooks/store"; import { useEventTracker, useProjectInbox, useUser } from "@/hooks/store";
import useReloadConfirmations from "@/hooks/use-reload-confirmation"; import useReloadConfirmations from "@/hooks/use-reload-confirmation";
@ -36,7 +38,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
// hooks // hooks
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting"); const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
const { captureIssueEvent } = useEventTracker(); const { captureEvent } = useEventTracker();
const { loader } = useProjectInbox(); const { loader } = useProjectInbox();
useEffect(() => { useEffect(() => {
@ -82,14 +84,12 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => { update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
try { try {
await inboxIssue.updateIssue(data); await inboxIssue.updateIssue(data);
captureIssueEvent({ captureEvent(INBOX_ISSUE_UPDATED, {
eventName: "Inbox issue updated", ...data,
payload: { ...data, state: "SUCCESS", element: "Inbox" },
updates: {
changed_property: Object.keys(data).join(","), changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","), change_details: Object.values(data).join(","),
}, element: E_INBOX,
routePath: router.asPath, state: "SUCCESS",
}); });
} catch (error) { } catch (error) {
setToast({ setToast({
@ -97,14 +97,12 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
message: "Issue update failed", message: "Issue update failed",
}); });
captureIssueEvent({ captureEvent(INBOX_ISSUE_UPDATED, {
eventName: "Inbox issue updated", ...data,
payload: { state: "SUCCESS", element: "Inbox" },
updates: {
changed_property: Object.keys(data).join(","), changed_property: Object.keys(data).join(","),
change_details: Object.values(data).join(","), change_details: Object.values(data).join(","),
}, element: E_INBOX,
routePath: router.asPath, state: "FAILED",
}); });
} }
}, },

View File

@ -2,19 +2,26 @@ import { FC, useState } from "react";
import concat from "lodash/concat"; import concat from "lodash/concat";
import uniq from "lodash/uniq"; import uniq from "lodash/uniq";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TInboxIssueFilterDateKeys } from "@plane/types"; import { TInboxIssueFilter, TInboxIssueFilterDateKeys } from "@plane/types";
// components // components
import { DateFilterModal } from "@/components/core"; import { DateFilterModal } from "@/components/core";
import { FilterHeader, FilterOption } from "@/components/issues"; import { FilterHeader, FilterOption } from "@/components/issues";
// constants // constants
import { PAST_DURATION_FILTER_OPTIONS } from "@/helpers/inbox.helper"; import { PAST_DURATION_FILTER_OPTIONS } from "@/helpers/inbox.helper";
// hooks // hooks
import { useProjectInbox } from "@/hooks/store"; import { useEventTracker } from "@/hooks/store";
type Props = { type Props = {
filterKey: TInboxIssueFilterDateKeys; filterKey: TInboxIssueFilterDateKeys;
label?: string; label?: string;
searchQuery: string; searchQuery: string;
inboxFilters: Partial<TInboxIssueFilter>;
handleFilterUpdate: (
filterKey: keyof TInboxIssueFilter,
filterValue: TInboxIssueFilter[keyof TInboxIssueFilter],
isSelected: boolean,
interactedValue: string
) => void;
}; };
const isDate = (date: string) => { const isDate = (date: string) => {
@ -23,9 +30,7 @@ const isDate = (date: string) => {
}; };
export const FilterDate: FC<Props> = observer((props) => { export const FilterDate: FC<Props> = observer((props) => {
const { filterKey, label, searchQuery } = props; const { filterKey, label, searchQuery, inboxFilters, handleFilterUpdate } = props;
// hooks
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
// state // state
const [previewEnabled, setPreviewEnabled] = useState(true); const [previewEnabled, setPreviewEnabled] = useState(true);
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
@ -46,7 +51,7 @@ export const FilterDate: FC<Props> = observer((props) => {
const handleCustomDate = () => { const handleCustomDate = () => {
if (isCustomDateSelected()) { if (isCustomDateSelected()) {
const updateAppliedFilters = filterValue?.filter((f) => !isDate(f.split(";")[0])) || []; const updateAppliedFilters = filterValue?.filter((f) => !isDate(f.split(";")[0])) || [];
handleInboxIssueFilters(filterKey, updateAppliedFilters); handleFilterUpdate(filterKey, updateAppliedFilters, true, "Custom");
} else { } else {
setIsDateFilterModalOpen(true); setIsDateFilterModalOpen(true);
} }
@ -58,7 +63,7 @@ export const FilterDate: FC<Props> = observer((props) => {
<DateFilterModal <DateFilterModal
handleClose={() => setIsDateFilterModalOpen(false)} handleClose={() => setIsDateFilterModalOpen(false)}
isOpen={isDateFilterModalOpen} isOpen={isDateFilterModalOpen}
onSelect={(val) => handleInboxIssueFilters(filterKey, val)} onSelect={(val) => handleFilterUpdate(filterKey, val, false, "Custom")}
title="Created date" title="Created date"
/> />
)} )}
@ -75,7 +80,14 @@ export const FilterDate: FC<Props> = observer((props) => {
<FilterOption <FilterOption
key={option.value} key={option.value}
isChecked={filterValue?.includes(option.value) ? true : false} isChecked={filterValue?.includes(option.value) ? true : false}
onClick={() => handleInboxIssueFilters(filterKey, handleFilterValue(option.value))} onClick={() =>
handleFilterUpdate(
filterKey,
handleFilterValue(option.value),
filterValue?.includes(option.value),
option.name
)
}
title={option.name} title={option.name}
multiple={false} multiple={false}
/> />

View File

@ -1,6 +1,7 @@
import { FC, useState } from "react"; import { FC, useCallback, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Search, X } from "lucide-react"; import { Search, X } from "lucide-react";
import { TInboxIssueFilter } from "@plane/types";
// components // components
import { import {
FilterStatus, FilterStatus,
@ -10,8 +11,11 @@ import {
FilterLabels, FilterLabels,
FilterState, FilterState,
} from "@/components/inbox/inbox-filter/filters"; } from "@/components/inbox/inbox-filter/filters";
// constants
import { INBOX_FILTERS_APPLIED, INBOX_FILTERS_REMOVED } from "@/constants/event-tracker";
import { INBOX_STATUS } from "@/constants/inbox";
// hooks // hooks
import { useMember, useLabel, useProjectState } from "@/hooks/store"; import { useMember, useLabel, useProjectState, useProjectInbox, useEventTracker } from "@/hooks/store";
export const InboxIssueFilterSelection: FC = observer(() => { export const InboxIssueFilterSelection: FC = observer(() => {
// hooks // hooks
@ -20,9 +24,31 @@ export const InboxIssueFilterSelection: FC = observer(() => {
} = useMember(); } = useMember();
const { projectLabels } = useLabel(); const { projectLabels } = useLabel();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
const { captureEvent } = useEventTracker();
// states // states
const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
const handleFilterUpdate = useCallback(
(
filterKey: keyof TInboxIssueFilter,
filterValue: TInboxIssueFilter[keyof TInboxIssueFilter],
isSelected: boolean,
interactedValue: string
) => {
captureEvent(isSelected ? INBOX_FILTERS_REMOVED : INBOX_FILTERS_APPLIED, {
filter_type: filterKey,
filter_property: interactedValue,
current_filters: {
...inboxFilters,
status: inboxFilters?.status?.map((status) => INBOX_STATUS.find((s) => s.status === status)?.title),
},
});
handleInboxIssueFilters(filterKey, filterValue);
},
[captureEvent, inboxFilters, handleInboxIssueFilters]
);
return ( return (
<div className="flex h-full w-full flex-col overflow-hidden"> <div className="flex h-full w-full flex-col overflow-hidden">
<div className="bg-custom-background-100 p-2.5 pb-0"> <div className="bg-custom-background-100 p-2.5 pb-0">
@ -47,21 +73,36 @@ export const InboxIssueFilterSelection: FC = observer(() => {
<div className="h-full w-full divide-y divide-custom-border-200 overflow-y-auto px-2.5 vertical-scrollbar scrollbar-sm"> <div className="h-full w-full divide-y divide-custom-border-200 overflow-y-auto px-2.5 vertical-scrollbar scrollbar-sm">
{/* status */} {/* status */}
<div className="py-2"> <div className="py-2">
<FilterStatus searchQuery={filtersSearchQuery} /> <FilterStatus
inboxFilters={inboxFilters}
handleFilterUpdate={handleFilterUpdate}
searchQuery={filtersSearchQuery}
/>
</div> </div>
{/* state */} {/* state */}
<div className="py-2"> <div className="py-2">
<FilterState states={projectStates} searchQuery={filtersSearchQuery} /> <FilterState
inboxFilters={inboxFilters}
handleFilterUpdate={handleFilterUpdate}
states={projectStates}
searchQuery={filtersSearchQuery}
/>
</div> </div>
{/* Priority */} {/* Priority */}
<div className="py-2"> <div className="py-2">
<FilterPriority searchQuery={filtersSearchQuery} /> <FilterPriority
inboxFilters={inboxFilters}
handleFilterUpdate={handleFilterUpdate}
searchQuery={filtersSearchQuery}
/>
</div> </div>
{/* assignees */} {/* assignees */}
<div className="py-2"> <div className="py-2">
<FilterMember <FilterMember
filterKey="assignee" filterKey="assignee"
label="Assignee" label="Assignee"
inboxFilters={inboxFilters}
handleFilterUpdate={handleFilterUpdate}
searchQuery={filtersSearchQuery} searchQuery={filtersSearchQuery}
memberIds={projectMemberIds ?? []} memberIds={projectMemberIds ?? []}
/> />
@ -71,21 +112,40 @@ export const InboxIssueFilterSelection: FC = observer(() => {
<FilterMember <FilterMember
filterKey="created_by" filterKey="created_by"
label="Created By" label="Created By"
inboxFilters={inboxFilters}
handleFilterUpdate={handleFilterUpdate}
searchQuery={filtersSearchQuery} searchQuery={filtersSearchQuery}
memberIds={projectMemberIds ?? []} memberIds={projectMemberIds ?? []}
/> />
</div> </div>
{/* Labels */} {/* Labels */}
<div className="py-2"> <div className="py-2">
<FilterLabels searchQuery={filtersSearchQuery} labels={projectLabels ?? []} /> <FilterLabels
inboxFilters={inboxFilters}
handleFilterUpdate={handleFilterUpdate}
searchQuery={filtersSearchQuery}
labels={projectLabels ?? []}
/>
</div> </div>
{/* Created at */} {/* Created at */}
<div className="py-2"> <div className="py-2">
<FilterDate filterKey="created_at" label="Created date" searchQuery={filtersSearchQuery} /> <FilterDate
inboxFilters={inboxFilters}
handleFilterUpdate={handleFilterUpdate}
filterKey="created_at"
label="Created date"
searchQuery={filtersSearchQuery}
/>
</div> </div>
{/* Updated at */} {/* Updated at */}
<div className="py-2"> <div className="py-2">
<FilterDate filterKey="updated_at" label="Last updated date" searchQuery={filtersSearchQuery} /> <FilterDate
inboxFilters={inboxFilters}
handleFilterUpdate={handleFilterUpdate}
filterKey="updated_at"
label="Last updated date"
searchQuery={filtersSearchQuery}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,11 +1,9 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { IIssueLabel } from "@plane/types"; import { IIssueLabel, TInboxIssueFilter } from "@plane/types";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
import { FilterHeader, FilterOption } from "@/components/issues"; import { FilterHeader, FilterOption } from "@/components/issues";
// hooks
import { useProjectInbox } from "@/hooks/store";
const LabelIcons = ({ color }: { color: string }) => ( const LabelIcons = ({ color }: { color: string }) => (
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: color }} /> <span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: color }} />
@ -14,16 +12,21 @@ const LabelIcons = ({ color }: { color: string }) => (
type Props = { type Props = {
labels: IIssueLabel[] | undefined; labels: IIssueLabel[] | undefined;
searchQuery: string; searchQuery: string;
inboxFilters: Partial<TInboxIssueFilter>;
handleFilterUpdate: (
filterKey: keyof TInboxIssueFilter,
filterValue: TInboxIssueFilter[keyof TInboxIssueFilter],
isSelected: boolean,
interactedValue: string
) => void;
}; };
export const FilterLabels: FC<Props> = observer((props) => { export const FilterLabels: FC<Props> = observer((props) => {
const { labels, searchQuery } = props; const { labels, searchQuery, inboxFilters, handleFilterUpdate } = props;
const [itemsToRender, setItemsToRender] = useState(5); const [itemsToRender, setItemsToRender] = useState(5);
const [previewEnabled, setPreviewEnabled] = useState(true); const [previewEnabled, setPreviewEnabled] = useState(true);
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
const filterValue = inboxFilters?.labels || []; const filterValue = inboxFilters?.labels || [];
const appliedFiltersCount = filterValue?.length ?? 0; const appliedFiltersCount = filterValue?.length ?? 0;
@ -56,7 +59,14 @@ export const FilterLabels: FC<Props> = observer((props) => {
<FilterOption <FilterOption
key={label?.id} key={label?.id}
isChecked={filterValue?.includes(label?.id) ? true : false} isChecked={filterValue?.includes(label?.id) ? true : false}
onClick={() => handleInboxIssueFilters("labels", handleFilterValue(label.id))} onClick={() =>
handleFilterUpdate(
"labels",
handleFilterValue(label?.id),
filterValue?.includes(label?.id),
label?.id
)
}
icon={<LabelIcons color={label.color} />} icon={<LabelIcons color={label.color} />}
title={label.name} title={label.name}
/> />

View File

@ -1,24 +1,29 @@
import { FC, useMemo, useState } from "react"; import { FC, useMemo, useState } from "react";
import sortBy from "lodash/sortBy"; import sortBy from "lodash/sortBy";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TInboxIssueFilterMemberKeys } from "@plane/types"; import { TInboxIssueFilter, TInboxIssueFilterMemberKeys } from "@plane/types";
import { Avatar, Loader } from "@plane/ui"; import { Avatar, Loader } from "@plane/ui";
// components // components
import { FilterHeader, FilterOption } from "@/components/issues"; import { FilterHeader, FilterOption } from "@/components/issues";
// hooks // hooks
import { useMember, useProjectInbox, useUser } from "@/hooks/store"; import { useEventTracker, useMember, useUser } from "@/hooks/store";
type Props = { type Props = {
filterKey: TInboxIssueFilterMemberKeys; filterKey: TInboxIssueFilterMemberKeys;
label?: string; label?: string;
memberIds: string[] | undefined; memberIds: string[] | undefined;
searchQuery: string; searchQuery: string;
inboxFilters: Partial<TInboxIssueFilter>;
handleFilterUpdate: (
filterKey: keyof TInboxIssueFilter,
filterValue: TInboxIssueFilter[keyof TInboxIssueFilter],
isSelected: boolean,
interactedValue: string
) => void;
}; };
export const FilterMember: FC<Props> = observer((props: Props) => { export const FilterMember: FC<Props> = observer((props: Props) => {
const { filterKey, label = "Members", memberIds, searchQuery } = props; const { filterKey, label = "Members", memberIds, searchQuery, inboxFilters, handleFilterUpdate } = props;
// hooks
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
const { getUserDetails } = useMember(); const { getUserDetails } = useMember();
const { currentUser } = useUser(); const { currentUser } = useUser();
// states // states
@ -71,7 +76,14 @@ export const FilterMember: FC<Props> = observer((props: Props) => {
<FilterOption <FilterOption
key={`members-${member.id}`} key={`members-${member.id}`}
isChecked={filterValue?.includes(member.id) ? true : false} isChecked={filterValue?.includes(member.id) ? true : false}
onClick={() => handleInboxIssueFilters(filterKey, handleFilterValue(member.id))} onClick={() =>
handleFilterUpdate(
filterKey,
handleFilterValue(member.id),
filterValue?.includes(member.id),
member.id
)
}
icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />} icon={<Avatar name={member.display_name} src={member.avatar} showTooltip={false} size="md" />}
title={currentUser?.id === member.id ? "You" : member?.display_name} title={currentUser?.id === member.id ? "You" : member?.display_name}
/> />

View File

@ -1,22 +1,25 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TIssuePriorities } from "@plane/types"; import { TInboxIssueFilter, TIssuePriorities } from "@plane/types";
import { PriorityIcon } from "@plane/ui"; import { PriorityIcon } from "@plane/ui";
// components // components
import { FilterHeader, FilterOption } from "@/components/issues"; import { FilterHeader, FilterOption } from "@/components/issues";
// constants // constants
import { ISSUE_PRIORITIES } from "@/constants/issue"; import { ISSUE_PRIORITIES } from "@/constants/issue";
// hooks
import { useProjectInbox } from "@/hooks/store/use-project-inbox";
type Props = { type Props = {
searchQuery: string; searchQuery: string;
inboxFilters: Partial<TInboxIssueFilter>;
handleFilterUpdate: (
filterKey: keyof TInboxIssueFilter,
filterValue: TInboxIssueFilter[keyof TInboxIssueFilter],
isSelected: boolean,
interactedValue: string
) => void;
}; };
export const FilterPriority: FC<Props> = observer((props) => { export const FilterPriority: FC<Props> = observer((props) => {
const { searchQuery } = props; const { searchQuery, inboxFilters, handleFilterUpdate } = props;
// hooks
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
// states // states
const [previewEnabled, setPreviewEnabled] = useState(true); const [previewEnabled, setPreviewEnabled] = useState(true);
// derived values // derived values
@ -41,7 +44,14 @@ export const FilterPriority: FC<Props> = observer((props) => {
<FilterOption <FilterOption
key={priority.key} key={priority.key}
isChecked={filterValue?.includes(priority.key) ? true : false} isChecked={filterValue?.includes(priority.key) ? true : false}
onClick={() => handleInboxIssueFilters("priority", handleFilterValue(priority.key))} onClick={() =>
handleFilterUpdate(
"priority",
handleFilterValue(priority.key),
filterValue?.includes(priority.key),
priority.title
)
}
icon={<PriorityIcon priority={priority.key} className="h-3.5 w-3.5" />} icon={<PriorityIcon priority={priority.key} className="h-3.5 w-3.5" />}
title={priority.title} title={priority.title}
/> />

View File

@ -1,25 +1,28 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { IState } from "@plane/types"; import { IState, TInboxIssueFilter } from "@plane/types";
import { Loader, StateGroupIcon } from "@plane/ui"; import { Loader, StateGroupIcon } from "@plane/ui";
// components // components
import { FilterHeader, FilterOption } from "@/components/issues"; import { FilterHeader, FilterOption } from "@/components/issues";
// hooks
import { useProjectInbox } from "@/hooks/store";
type Props = { type Props = {
states: IState[] | undefined; states: IState[] | undefined;
searchQuery: string; searchQuery: string;
inboxFilters: Partial<TInboxIssueFilter>;
handleFilterUpdate: (
filterKey: keyof TInboxIssueFilter,
filterValue: TInboxIssueFilter[keyof TInboxIssueFilter],
isSelected: boolean,
interactedValue: string
) => void;
}; };
export const FilterState: FC<Props> = observer((props) => { export const FilterState: FC<Props> = observer((props) => {
const { states, searchQuery } = props; const { states, inboxFilters, searchQuery, handleFilterUpdate } = props;
const [itemsToRender, setItemsToRender] = useState(5); const [itemsToRender, setItemsToRender] = useState(5);
const [previewEnabled, setPreviewEnabled] = useState(true); const [previewEnabled, setPreviewEnabled] = useState(true);
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
const filterValue = inboxFilters?.state || []; const filterValue = inboxFilters?.state || [];
const appliedFiltersCount = filterValue?.length ?? 0; const appliedFiltersCount = filterValue?.length ?? 0;
@ -52,7 +55,14 @@ export const FilterState: FC<Props> = observer((props) => {
<FilterOption <FilterOption
key={state?.id} key={state?.id}
isChecked={filterValue?.includes(state?.id) ? true : false} isChecked={filterValue?.includes(state?.id) ? true : false}
onClick={() => handleInboxIssueFilters("state", handleFilterValue(state.id))} onClick={() =>
handleFilterUpdate(
"state",
handleFilterValue(state.id),
filterValue?.includes(state.id),
state.id
)
}
icon={<StateGroupIcon color={state.color} stateGroup={state.group} height="12px" width="12px" />} icon={<StateGroupIcon color={state.color} stateGroup={state.group} height="12px" width="12px" />}
title={state.name} title={state.name}
/> />

View File

@ -1,22 +1,29 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// types // types
import { TInboxIssueStatus } from "@plane/types"; import { TInboxIssueFilter, TInboxIssueFilterMemberKeys, TInboxIssueStatus } from "@plane/types";
// components // components
import { FilterHeader, FilterOption } from "@/components/issues"; import { FilterHeader, FilterOption } from "@/components/issues";
// constants // constants
import { INBOX_STATUS } from "@/constants/inbox"; import { INBOX_STATUS } from "@/constants/inbox";
// hooks // hooks
import { useProjectInbox } from "@/hooks/store/use-project-inbox"; import { useProjectInbox } from "@/hooks/store";
type Props = { type Props = {
searchQuery: string; searchQuery: string;
inboxFilters: Partial<TInboxIssueFilter>;
handleFilterUpdate: (
filterKey: keyof TInboxIssueFilter,
filterValue: TInboxIssueFilter[keyof TInboxIssueFilter],
isSelected: boolean,
interactedValue: string
) => void;
}; };
export const FilterStatus: FC<Props> = observer((props) => { export const FilterStatus: FC<Props> = observer((props) => {
const { searchQuery } = props; const { searchQuery, inboxFilters, handleFilterUpdate } = props;
// hooks // hooks
const { currentTab, inboxFilters, handleInboxIssueFilters } = useProjectInbox(); const { currentTab } = useProjectInbox();
// states // states
const [previewEnabled, setPreviewEnabled] = useState(true); const [previewEnabled, setPreviewEnabled] = useState(true);
// derived values // derived values
@ -34,7 +41,13 @@ export const FilterStatus: FC<Props> = observer((props) => {
const handleStatusFilterSelect = (status: TInboxIssueStatus) => { const handleStatusFilterSelect = (status: TInboxIssueStatus) => {
const selectedStatus = handleFilterValue(status); const selectedStatus = handleFilterValue(status);
if (selectedStatus.length >= 1) handleInboxIssueFilters("status", selectedStatus); if (selectedStatus.length >= 1)
handleFilterUpdate(
"status",
selectedStatus,
filterValue?.includes(status),
INBOX_STATUS.find((s) => s.status === status)?.title ?? ""
);
}; };
return ( return (

View File

@ -1,20 +1,35 @@
import { FC } from "react"; import { FC, useCallback } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { ArrowDownWideNarrow, ArrowUpWideNarrow, Check, ChevronDown } from "lucide-react"; import { ArrowDownWideNarrow, ArrowUpWideNarrow, Check, ChevronDown } from "lucide-react";
import { TInboxIssueSorting } from "@plane/types";
import { CustomMenu, getButtonStyling } from "@plane/ui"; import { CustomMenu, getButtonStyling } from "@plane/ui";
// constants // constants
import { INBOX_SORT_UPDATED } from "@/constants/event-tracker";
import { INBOX_ISSUE_ORDER_BY_OPTIONS, INBOX_ISSUE_SORT_BY_OPTIONS } from "@/constants/inbox"; import { INBOX_ISSUE_ORDER_BY_OPTIONS, INBOX_ISSUE_SORT_BY_OPTIONS } from "@/constants/inbox";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// hooks // hooks
import { useProjectInbox } from "@/hooks/store"; import { useProjectInbox, useEventTracker } from "@/hooks/store";
export const InboxIssueOrderByDropdown: FC = observer(() => { export const InboxIssueOrderByDropdown: FC = observer(() => {
// hooks // hooks
const { inboxSorting, handleInboxIssueSorting } = useProjectInbox(); const { inboxSorting, handleInboxIssueSorting } = useProjectInbox();
const { captureEvent } = useEventTracker();
const orderByDetails = const orderByDetails =
INBOX_ISSUE_ORDER_BY_OPTIONS.find((option) => inboxSorting?.order_by?.includes(option.key)) || undefined; INBOX_ISSUE_ORDER_BY_OPTIONS.find((option) => inboxSorting?.order_by?.includes(option.key)) || undefined;
const handleIssueSorting = useCallback(
(filterKey: keyof TInboxIssueSorting, filterValue: TInboxIssueSorting[keyof TInboxIssueSorting]) => {
captureEvent(INBOX_SORT_UPDATED, {
changed_property: filterKey,
changed_details: filterValue,
current_sort: inboxSorting,
});
handleInboxIssueSorting(filterKey, filterValue);
},
[handleInboxIssueSorting, captureEvent, inboxSorting]
);
return ( return (
<CustomMenu <CustomMenu
customButton={ customButton={
@ -36,7 +51,7 @@ export const InboxIssueOrderByDropdown: FC = observer(() => {
<CustomMenu.MenuItem <CustomMenu.MenuItem
key={option.key} key={option.key}
className="flex items-center justify-between gap-2" className="flex items-center justify-between gap-2"
onClick={() => handleInboxIssueSorting("order_by", option.key)} onClick={() => handleIssueSorting("order_by", option.key)}
> >
{option.label} {option.label}
{inboxSorting?.order_by?.includes(option.key) && <Check className="h-3 w-3" />} {inboxSorting?.order_by?.includes(option.key) && <Check className="h-3 w-3" />}
@ -47,7 +62,7 @@ export const InboxIssueOrderByDropdown: FC = observer(() => {
<CustomMenu.MenuItem <CustomMenu.MenuItem
key={option.key} key={option.key}
className="flex items-center justify-between gap-2" className="flex items-center justify-between gap-2"
onClick={() => handleInboxIssueSorting("sort_by", option.key)} onClick={() => handleIssueSorting("sort_by", option.key)}
> >
{option.label} {option.label}
{inboxSorting?.sort_by?.includes(option.key) && <Check className="h-3 w-3" />} {inboxSorting?.sort_by?.includes(option.key) && <Check className="h-3 w-3" />}

View File

@ -11,7 +11,7 @@ import {
InboxIssueProperties, InboxIssueProperties,
} from "@/components/inbox/modals/create-edit-modal"; } from "@/components/inbox/modals/create-edit-modal";
// constants // constants
import { ISSUE_CREATED } from "@/constants/event-tracker"; import { INBOX_ISSUE_CREATED, getIssueEventPayload } from "@/constants/event-tracker";
// helpers // helpers
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// hooks // hooks
@ -41,7 +41,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
// refs // refs
const descriptionEditorRef = useRef<EditorRefApi>(null); const descriptionEditorRef = useRef<EditorRefApi>(null);
// hooks // hooks
const { captureIssueEvent } = useEventTracker(); const { captureEvent } = useEventTracker();
const { createInboxIssue } = useProjectInbox(); const { createInboxIssue } = useProjectInbox();
const { getWorkspaceBySlug } = useWorkspace(); const { getWorkspaceBySlug } = useWorkspace();
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id; const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id;
@ -81,14 +81,13 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
descriptionEditorRef?.current?.clearEditor(); descriptionEditorRef?.current?.clearEditor();
setFormData(defaultIssueData); setFormData(defaultIssueData);
} }
captureIssueEvent({ captureEvent(INBOX_ISSUE_CREATED, {
eventName: ISSUE_CREATED, issue_id: res?.issue?.id,
payload: { properties: getIssueEventPayload({
...formData, eventName: INBOX_ISSUE_CREATED,
payload: res?.issue,
}),
state: "SUCCESS", state: "SUCCESS",
element: "Inbox page",
},
routePath: router.pathname,
}); });
setToast({ setToast({
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
@ -98,14 +97,12 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
}) })
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
captureIssueEvent({ captureEvent(INBOX_ISSUE_CREATED, {
eventName: ISSUE_CREATED, properties: getIssueEventPayload({
payload: { eventName: INBOX_ISSUE_CREATED,
...formData, payload: formData,
}),
state: "FAILED", state: "FAILED",
element: "Inbox page",
},
routePath: router.pathname,
}); });
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,

View File

@ -5,11 +5,13 @@ import { useRouter } from "next/router";
import { Tooltip, PriorityIcon } from "@plane/ui"; import { Tooltip, PriorityIcon } from "@plane/ui";
// components // components
import { InboxIssueStatus } from "@/components/inbox"; import { InboxIssueStatus } from "@/components/inbox";
// constants
import { INBOX_ISSUE_OPENED } from "@/constants/event-tracker";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { renderFormattedDate } from "@/helpers/date-time.helper"; import { renderFormattedDate } from "@/helpers/date-time.helper";
// hooks // hooks
import { useLabel, useProjectInbox } from "@/hooks/store"; import { useEventTracker, useLabel, useProjectInbox } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// store // store
import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store"; import { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
@ -31,6 +33,7 @@ export const InboxIssueListItem: FC<InboxIssueListItemProps> = observer((props)
const { currentTab } = useProjectInbox(); const { currentTab } = useProjectInbox();
const { projectLabels } = useLabel(); const { projectLabels } = useLabel();
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const { captureEvent } = useEventTracker();
const issue = inboxIssue.issue; const issue = inboxIssue.issue;
const handleIssueRedirection = (event: MouseEvent, currentIssueId: string | undefined) => { const handleIssueRedirection = (event: MouseEvent, currentIssueId: string | undefined) => {
@ -45,7 +48,12 @@ export const InboxIssueListItem: FC<InboxIssueListItemProps> = observer((props)
id={`inbox-issue-list-item-${issue.id}`} id={`inbox-issue-list-item-${issue.id}`}
key={`${projectId}_${issue.id}`} key={`${projectId}_${issue.id}`}
href={`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}&inboxIssueId=${issue.id}`} href={`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${currentTab}&inboxIssueId=${issue.id}`}
onClick={(e) => handleIssueRedirection(e, issue.id)} onClick={(e) => {
handleIssueRedirection(e, issue.id);
captureEvent(INBOX_ISSUE_OPENED, {
issueId: issue.id,
});
}}
> >
<div <div
className={cn( className={cn(

View File

@ -9,11 +9,12 @@ import { FiltersRoot, InboxIssueAppliedFilters, InboxIssueList } from "@/compone
import { InboxSidebarLoader } from "@/components/ui"; import { InboxSidebarLoader } from "@/components/ui";
// constants // constants
import { EmptyStateType } from "@/constants/empty-state"; import { EmptyStateType } from "@/constants/empty-state";
import { INBOX_TAB_CHANGED } from "@/constants/event-tracker";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
// hooks // hooks
import { useProject, useProjectInbox } from "@/hooks/store"; import { useEventTracker, useProject, useProjectInbox } from "@/hooks/store";
import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
type IInboxSidebarProps = { type IInboxSidebarProps = {
@ -49,6 +50,7 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
fetchInboxPaginationIssues, fetchInboxPaginationIssues,
getAppliedFiltersCount, getAppliedFiltersCount,
} = useProjectInbox(); } = useProjectInbox();
const {captureEvent} = useEventTracker();
const router = useRouter(); const router = useRouter();
@ -78,6 +80,9 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
onClick={() => { onClick={() => {
if (currentTab != option?.key) handleCurrentTab(option?.key); if (currentTab != option?.key) handleCurrentTab(option?.key);
router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${option?.key}`); router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${option?.key}`);
captureEvent(INBOX_TAB_CHANGED, {
tab: option?.key,
});
}} }}
> >
<div>{option?.label}</div> <div>{option?.label}</div>

View File

@ -9,7 +9,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { IssueActivityCommentRoot, IssueCommentRoot, IssueCommentCreate } from "@/components/issues"; import { IssueActivityCommentRoot, IssueCommentRoot, IssueCommentCreate } from "@/components/issues";
// constants // constants
import { COMMENT_CREATED, COMMENT_DELETED, COMMENT_UPDATED } from "@/constants/event-tracker"; import { COMMENT_CREATED, COMMENT_DELETED, COMMENT_UPDATED, E_INBOX, E_ISSUE_DETAILS } from "@/constants/event-tracker";
// hooks // hooks
import { useIssueDetail, useProject, useEventTracker } from "@/hooks/store"; import { useIssueDetail, useProject, useEventTracker } from "@/hooks/store";
@ -45,7 +45,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props; const { workspaceSlug, projectId, issueId, disabled = false } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { inboxId } = router.query; const { inboxIssueId } = router.query;
// hooks // hooks
const { createComment, updateComment, removeComment } = useIssueDetail(); const { createComment, updateComment, removeComment } = useIssueDetail();
const { captureEvent } = useEventTracker(); const { captureEvent } = useEventTracker();
@ -59,11 +59,12 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
createComment: async (data: Partial<TIssueComment>) => { createComment: async (data: Partial<TIssueComment>) => {
try { try {
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields"); if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
await createComment(workspaceSlug, projectId, issueId, data); const res = await createComment(workspaceSlug, projectId, issueId, data);
captureEvent(COMMENT_CREATED, { captureEvent(COMMENT_CREATED, {
issue_id: issueId, issue_id: issueId,
comment_id: res?.id,
is_public: data.access === "EXTERNAL", is_public: data.access === "EXTERNAL",
element: peekIssue ? "Peek issue" : inboxId ? "Inbox issue" : "Issue detail", element: peekIssue ? "Peek issue" : inboxIssueId ? E_INBOX : E_ISSUE_DETAILS,
}); });
setToast({ setToast({
title: "Comment created successfully.", title: "Comment created successfully.",
@ -84,8 +85,9 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
await updateComment(workspaceSlug, projectId, issueId, commentId, data); await updateComment(workspaceSlug, projectId, issueId, commentId, data);
captureEvent(COMMENT_UPDATED, { captureEvent(COMMENT_UPDATED, {
issue_id: issueId, issue_id: issueId,
comment_id: commentId,
is_public: data.access === "EXTERNAL", is_public: data.access === "EXTERNAL",
element: peekIssue ? "Peek issue" : inboxId ? "Inbox issue" : "Issue detail", element: peekIssue ? "Peek issue" : inboxIssueId ? E_INBOX : E_ISSUE_DETAILS,
}); });
setToast({ setToast({
title: "Comment updated successfully.", title: "Comment updated successfully.",
@ -106,7 +108,8 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
await removeComment(workspaceSlug, projectId, issueId, commentId); await removeComment(workspaceSlug, projectId, issueId, commentId);
captureEvent(COMMENT_DELETED, { captureEvent(COMMENT_DELETED, {
issue_id: issueId, issue_id: issueId,
element: peekIssue ? "Peek issue" : inboxId ? "Inbox issue" : "Issue detail", comment_id: commentId,
element: peekIssue ? "Peek issue" : inboxIssueId ? E_INBOX : E_ISSUE_DETAILS,
}); });
setToast({ setToast({
title: "Comment removed successfully.", title: "Comment removed successfully.",

View File

@ -98,7 +98,8 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
module_id: payload.module_id, module_id: payload.module_id,
archived_at: payload.archived_at, archived_at: payload.archived_at,
state: payload.state, state: payload.state,
view_id: routePath?.includes("workspace-views") || routePath?.includes("views") ? routePath.split("/").pop() : "", view_id:
routePath?.includes("workspace-views") || routePath?.includes("views") ? routePath.split("/").pop() : undefined,
}; };
if (eventName === ISSUE_UPDATED) { if (eventName === ISSUE_UPDATED) {
@ -214,6 +215,15 @@ export const ISSUE_UPDATED = "Issue updated";
export const ISSUE_DELETED = "Issue deleted"; export const ISSUE_DELETED = "Issue deleted";
export const ISSUE_ARCHIVED = "Issue archived"; export const ISSUE_ARCHIVED = "Issue archived";
export const ISSUE_RESTORED = "Issue restored"; export const ISSUE_RESTORED = "Issue restored";
// Inbox Events
export const INBOX_ISSUE_CREATED = "Inbox issue created";
export const INBOX_ISSUE_UPDATED = "Inbox issue updated";
export const INBOX_ISSUE_DELETED = "Inbox issue deleted";
export const INBOX_FILTERS_APPLIED = "Inbox filters applied";
export const INBOX_FILTERS_REMOVED = "Inbox filters removed";
export const INBOX_SORT_UPDATED= "Inbox sort updated";
export const INBOX_ISSUE_OPENED = "Inbox issue opened";
export const INBOX_TAB_CHANGED = "Inbox tab changed";
// Comment Events // Comment Events
export const COMMENT_CREATED = "Comment created"; export const COMMENT_CREATED = "Comment created";
export const COMMENT_UPDATED = "Comment updated"; export const COMMENT_UPDATED = "Comment updated";