chore: added issues filters & layout related events

This commit is contained in:
LAKHAN BAHETI 2024-02-23 17:10:15 +05:30
parent b354eb836a
commit 7143c98b2e
9 changed files with 445 additions and 59 deletions

View File

@ -31,6 +31,16 @@ import { renderEmoji } from "helpers/emoji.helper";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import {
DP_APPLIED,
DP_REMOVED,
elementFromPath,
FILTER_APPLIED,
FILTER_REMOVED,
FILTER_SEARCHED,
LAYOUT_CHANGED,
LP_UPDATED,
} from "constants/event-tracker";
import { EUserProjectRoles } from "constants/project";
import { cn } from "helpers/common.helper";
import { CycleMobileHeader } from "components/cycles/cycle-mobile-header";
@ -74,7 +84,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
const {
commandPalette: { toggleCreateIssueModal },
} = useApplication();
const { setTrackElement } = useEventTracker();
const { setTrackElement, captureEvent, captureIssuesFilterEvent, captureIssuesDisplayFilterEvent } =
useEventTracker();
const {
membership: { currentProjectRole },
} = useUser();
@ -97,7 +108,13 @@ export const CycleIssuesHeader: React.FC = observer(() => {
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId);
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId).then(() =>
captureEvent(LAYOUT_CHANGED, {
layout: layout,
element: elementFromPath(router.asPath),
element_id: cycleId,
})
);
},
[workspaceSlug, projectId, cycleId, updateFilters]
);
@ -106,17 +123,31 @@ export const CycleIssuesHeader: React.FC = observer(() => {
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
let isFilterRemoved = false;
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else isFilterRemoved = true;
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
if (issueFilters?.filters?.[key]?.includes(value)) {
newValues.splice(newValues.indexOf(value), 1);
isFilterRemoved = true;
} else newValues.push(value);
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, cycleId);
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, cycleId).then(() =>
captureIssuesFilterEvent({
eventName: isFilterRemoved ? FILTER_REMOVED : FILTER_APPLIED,
payload: {
path: router.asPath,
filters: issueFilters,
element_id: cycleId,
filter_property: value,
filter_type: key,
},
})
);
},
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
);
@ -124,17 +155,39 @@ export const CycleIssuesHeader: React.FC = observer(() => {
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId);
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId).then(
() =>
captureIssuesDisplayFilterEvent({
eventName: LP_UPDATED,
payload: {
property_type: Object.keys(updatedDisplayFilter).join(","),
property: Object.values(updatedDisplayFilter)?.[0],
path: router.asPath,
filters: issueFilters,
element_id: cycleId,
},
[workspaceSlug, projectId, cycleId, updateFilters]
})
);
},
[workspaceSlug, projectId, cycleId, updateFilters, issueFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, cycleId);
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, cycleId).then(() =>
captureIssuesDisplayFilterEvent({
eventName: Object.values(property)?.[0] === true ? DP_APPLIED : DP_REMOVED,
payload: {
display_property: Object.keys(property).join(","),
path: router.asPath,
filters: issueFilters,
element_id: cycleId,
},
[workspaceSlug, projectId, cycleId, updateFilters]
})
);
},
[workspaceSlug, projectId, cycleId, updateFilters, issueFilters]
);
// derived values
@ -233,6 +286,17 @@ export const CycleIssuesHeader: React.FC = observer(() => {
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
onSearchCapture={() =>
captureIssuesFilterEvent({
eventName: FILTER_SEARCHED,
payload: {
path: router.asPath,
current_filters: issueFilters?.filters,
layout: issueFilters?.displayFilters?.layout,
element_id: cycleId,
},
})
}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">

View File

@ -3,7 +3,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useLabel, useMember, useUser, useIssues } from "hooks/store";
import { useLabel, useMember, useUser, useIssues, useEventTracker } from "hooks/store";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "components/issues";
import { CreateUpdateWorkspaceViewModal } from "components/workspace";
@ -18,6 +18,16 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EUserWorkspaceRoles } from "constants/workspace";
import {
DP_APPLIED,
DP_REMOVED,
elementFromPath,
FILTER_APPLIED,
FILTER_REMOVED,
FILTER_SEARCHED,
LAYOUT_CHANGED,
LP_UPDATED,
} from "constants/event-tracker";
const GLOBAL_VIEW_LAYOUTS = [
{ key: "list", title: "List", link: "/workspace-views", icon: List },
@ -46,6 +56,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const {
workspace: { workspaceMemberIds },
} = useMember();
const { captureIssuesFilterEvent, captureEvent, captureIssuesDisplayFilterEvent } = useEventTracker();
const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined;
@ -53,14 +64,18 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !globalViewId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
let isFilterRemoved = false;
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else isFilterRemoved = true;
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
if (issueFilters?.filters?.[key]?.includes(value)) {
isFilterRemoved = true;
newValues.splice(newValues.indexOf(value), 1);
} else newValues.push(value);
}
updateFilters(
@ -69,7 +84,18 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
EIssueFilterType.FILTERS,
{ [key]: newValues },
globalViewId.toString()
);
).then(() => {
captureIssuesFilterEvent({
eventName: isFilterRemoved ? FILTER_REMOVED : FILTER_APPLIED,
payload: {
path: router.asPath,
filters: issueFilters,
element_id: globalViewId,
filter_property: value,
filter_type: key,
},
});
});
},
[workspaceSlug, issueFilters, updateFilters, globalViewId]
);
@ -83,9 +109,20 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
EIssueFilterType.DISPLAY_FILTERS,
updatedDisplayFilter,
globalViewId.toString()
).then(() =>
captureIssuesDisplayFilterEvent({
eventName: LP_UPDATED,
payload: {
property_type: Object.keys(updatedDisplayFilter).join(","),
property: Object.values(updatedDisplayFilter)?.[0],
path: router.asPath,
filters: issueFilters,
element_id: globalViewId,
},
})
);
},
[workspaceSlug, updateFilters, globalViewId]
[workspaceSlug, updateFilters, globalViewId, issueFilters]
);
const handleDisplayProperties = useCallback(
@ -97,9 +134,19 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
EIssueFilterType.DISPLAY_PROPERTIES,
property,
globalViewId.toString()
).then(() =>
captureIssuesDisplayFilterEvent({
eventName: Object.values(property)?.[0] === true ? DP_APPLIED : DP_REMOVED,
payload: {
display_property: Object.keys(property).join(","),
path: router.asPath,
filters: issueFilters,
element_id: globalViewId,
},
})
);
},
[workspaceSlug, updateFilters, globalViewId]
[workspaceSlug, updateFilters, globalViewId, issueFilters]
);
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
@ -160,6 +207,17 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
handleFiltersUpdate={handleFiltersUpdate}
labels={workspaceLabels ?? undefined}
memberIds={workspaceMemberIds ?? undefined}
onSearchCapture={() =>
captureIssuesFilterEvent({
eventName: FILTER_SEARCHED,
payload: {
path: router.asPath,
current_filters: issueFilters?.filters,
layout: issueFilters?.displayFilters?.layout,
element_id: globalViewId,
},
})
}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">

View File

@ -31,6 +31,16 @@ import { renderEmoji } from "helpers/emoji.helper";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
// constants
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import {
DP_APPLIED,
DP_REMOVED,
elementFromPath,
FILTER_APPLIED,
FILTER_REMOVED,
FILTER_SEARCHED,
LAYOUT_CHANGED,
LP_UPDATED,
} from "constants/event-tracker";
import { EUserProjectRoles } from "constants/project";
import { cn } from "helpers/common.helper";
import { ModuleMobileHeader } from "components/modules/module-mobile-header";
@ -77,7 +87,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
const {
commandPalette: { toggleCreateIssueModal },
} = useApplication();
const { setTrackElement } = useEventTracker();
const { setTrackElement, captureEvent, captureIssuesFilterEvent, captureIssuesDisplayFilterEvent } =
useEventTracker();
const {
membership: { currentProjectRole },
} = useUser();
@ -100,7 +111,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId);
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId).then(() =>
captureEvent(LAYOUT_CHANGED, {
layout: layout,
element: elementFromPath(router.asPath),
element_id: moduleId,
})
);
},
[workspaceSlug, projectId, moduleId, updateFilters]
);
@ -109,17 +126,31 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
let isFilterRemoved = false;
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else isFilterRemoved = true;
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
if (issueFilters?.filters?.[key]?.includes(value)) {
isFilterRemoved = true;
newValues.splice(newValues.indexOf(value), 1);
} else newValues.push(value);
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId);
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId).then(() => {
captureIssuesFilterEvent({
eventName: isFilterRemoved ? FILTER_REMOVED : FILTER_APPLIED,
payload: {
path: router.asPath,
filters: issueFilters,
element_id: moduleId,
filter_property: value,
filter_type: key,
},
});
});
},
[workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
);
@ -127,17 +158,39 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId);
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId).then(
() =>
captureIssuesDisplayFilterEvent({
eventName: LP_UPDATED,
payload: {
property_type: Object.keys(updatedDisplayFilter).join(","),
property: Object.values(updatedDisplayFilter)?.[0],
path: router.asPath,
filters: issueFilters,
element_id: moduleId,
},
[workspaceSlug, projectId, moduleId, updateFilters]
})
);
},
[workspaceSlug, projectId, moduleId, updateFilters, issueFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId);
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId).then(() =>
captureIssuesDisplayFilterEvent({
eventName: Object.values(property)?.[0] === true ? DP_APPLIED : DP_REMOVED,
payload: {
display_property: Object.keys(property).join(","),
path: router.asPath,
filters: issueFilters,
element_id: moduleId,
},
[workspaceSlug, projectId, moduleId, updateFilters]
})
);
},
[workspaceSlug, projectId, moduleId, updateFilters, issueFilters]
);
// derived values
@ -237,6 +290,17 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
onSearchCapture={() =>
captureIssuesFilterEvent({
eventName: FILTER_SEARCHED,
payload: {
path: router.asPath,
current_filters: issueFilters?.filters,
layout: issueFilters?.displayFilters?.layout,
element_id: moduleId,
},
})
}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">

View File

@ -23,6 +23,16 @@ import { Breadcrumbs, Button, LayersIcon } from "@plane/ui";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import {
DP_APPLIED,
DP_REMOVED,
elementFromPath,
FILTER_APPLIED,
FILTER_REMOVED,
FILTER_SEARCHED,
LAYOUT_CHANGED,
LP_UPDATED,
} from "constants/event-tracker";
// helper
import { renderEmoji } from "helpers/emoji.helper";
import { EUserProjectRoles } from "constants/project";
@ -45,7 +55,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
const {
commandPalette: { toggleCreateIssueModal },
} = useApplication();
const { setTrackElement } = useEventTracker();
const { captureEvent, setTrackElement, captureIssuesFilterEvent, captureIssuesDisplayFilterEvent } =
useEventTracker();
const {
membership: { currentProjectRole },
} = useUser();
@ -59,17 +70,31 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
let isFilterRemoved = false;
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else isFilterRemoved = true;
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
if (issueFilters?.filters?.[key]?.includes(value)) {
newValues.splice(newValues.indexOf(value), 1);
isFilterRemoved = true;
} else newValues.push(value);
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues });
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }).then(() =>
captureIssuesFilterEvent({
eventName: isFilterRemoved ? FILTER_REMOVED : FILTER_APPLIED,
payload: {
path: router.asPath,
filters: issueFilters,
element_id: projectId,
filter_property: value,
filter_type: key,
},
})
);
},
[workspaceSlug, projectId, issueFilters, updateFilters]
);
@ -77,7 +102,13 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }).then(() =>
captureEvent(LAYOUT_CHANGED, {
layout: layout,
element: elementFromPath(router.asPath),
element_id: projectId,
})
);
},
[workspaceSlug, projectId, updateFilters]
);
@ -85,17 +116,38 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter).then(() =>
captureIssuesDisplayFilterEvent({
eventName: LP_UPDATED,
payload: {
property_type: Object.keys(updatedDisplayFilter).join(","),
property: Object.values(updatedDisplayFilter)?.[0],
path: router.asPath,
filters: issueFilters,
element_id: projectId,
},
[workspaceSlug, projectId, updateFilters]
})
);
},
[workspaceSlug, projectId, updateFilters, issueFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property);
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property).then(() => {
captureIssuesDisplayFilterEvent({
eventName: Object.values(property)?.[0] === true ? DP_APPLIED : DP_REMOVED,
payload: {
display_property: Object.keys(property).join(","),
path: router.asPath,
filters: issueFilters,
element_id: projectId,
},
[workspaceSlug, projectId, updateFilters]
});
});
},
[workspaceSlug, projectId, updateFilters, issueFilters]
);
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
@ -183,6 +235,17 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
onSearchCapture={() =>
captureIssuesFilterEvent({
eventName: FILTER_SEARCHED,
payload: {
path: router.asPath,
current_filters: issueFilters?.filters,
layout: issueFilters?.displayFilters?.layout,
element_id: projectId,
},
})
}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">

View File

@ -29,6 +29,16 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
// constants
import { EIssuesStoreType, EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EUserProjectRoles } from "constants/project";
import {
DP_APPLIED,
DP_REMOVED,
elementFromPath,
FILTER_APPLIED,
FILTER_REMOVED,
FILTER_SEARCHED,
LAYOUT_CHANGED,
LP_UPDATED,
} from "constants/event-tracker";
export const ProjectViewIssuesHeader: React.FC = observer(() => {
// router
@ -42,7 +52,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
const { setTrackElement } = useEventTracker();
const { setTrackElement, captureEvent, captureIssuesFilterEvent, captureIssuesDisplayFilterEvent } =
useEventTracker();
const {
commandPalette: { toggleCreateIssueModal },
} = useApplication();
@ -62,7 +73,13 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, viewId);
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, viewId).then(() =>
captureEvent(LAYOUT_CHANGED, {
layout: layout,
element: elementFromPath(router.asPath),
element_id: viewId,
})
);
},
[workspaceSlug, projectId, viewId, updateFilters]
);
@ -71,17 +88,31 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
let isFilterRemoved = false;
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else isFilterRemoved = true;
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
if (issueFilters?.filters?.[key]?.includes(value)) {
isFilterRemoved = true;
newValues.splice(newValues.indexOf(value), 1);
} else newValues.push(value);
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, viewId);
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, viewId).then(() => {
captureIssuesFilterEvent({
eventName: isFilterRemoved ? FILTER_REMOVED : FILTER_APPLIED,
payload: {
path: router.asPath,
filters: issueFilters,
element_id: viewId,
filter_property: value,
filter_type: key,
},
});
});
},
[workspaceSlug, projectId, viewId, issueFilters, updateFilters]
);
@ -89,17 +120,38 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, viewId);
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, viewId).then(() =>
captureIssuesDisplayFilterEvent({
eventName: LP_UPDATED,
payload: {
property_type: Object.keys(updatedDisplayFilter).join(","),
property: Object.values(updatedDisplayFilter)?.[0],
path: router.asPath,
filters: issueFilters,
element_id: viewId,
},
[workspaceSlug, projectId, viewId, updateFilters]
})
);
},
[workspaceSlug, projectId, viewId, updateFilters, issueFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, viewId);
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, viewId).then(() =>
captureIssuesDisplayFilterEvent({
eventName: Object.values(property)?.[0] === true ? DP_APPLIED : DP_REMOVED,
payload: {
display_property: Object.keys(property).join(","),
path: router.asPath,
filters: issueFilters,
element_id: viewId,
},
[workspaceSlug, projectId, viewId, updateFilters]
})
);
},
[workspaceSlug, projectId, viewId, updateFilters, issueFilters]
);
const viewDetails = viewId ? getViewById(viewId.toString()) : null;
@ -198,6 +250,17 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
onSearchCapture={() =>
captureIssuesFilterEvent({
eventName: FILTER_SEARCHED,
payload: {
path: router.asPath,
current_filters: issueFilters?.filters,
layout: issueFilters?.displayFilters?.layout,
element_id: projectId,
},
})
}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">

View File

@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Search, X } from "lucide-react";
// components
@ -14,6 +14,8 @@ import {
FilterStateGroup,
FilterTargetDate,
} from "components/issues";
// hooks
import useDebounce from "hooks/use-debounce";
// types
import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types";
// constants
@ -26,14 +28,21 @@ type Props = {
labels?: IIssueLabel[] | undefined;
memberIds?: string[] | undefined;
states?: IState[] | undefined;
onSearchCapture?: () => void;
};
export const FilterSelection: React.FC<Props> = observer((props) => {
const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, memberIds, states } = props;
const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, memberIds, states, onSearchCapture } =
props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions?.filters.includes(filter);
const debouncedValue = useDebounce(filtersSearchQuery, 1500);
useEffect(() => {
if (debouncedValue && onSearchCapture) onSearchCapture();
}, [debouncedValue]);
return (
<div className="flex h-full w-full flex-col overflow-hidden">

View File

@ -44,7 +44,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
} = useUser();
const { fetchAllGlobalViews } = useGlobalView();
const { workspaceProjectIds } = useProject();
const { setTrackElement } = useEventTracker();
const { setTrackElement, captureIssuesListOpenedEvent } = useEventTracker();
const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(groupedIssueIds.dataViewId);
const currentView = isDefaultView ? groupedIssueIds.dataViewId : "custom-view";
@ -100,6 +100,10 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
if (workspaceSlug && globalViewId) {
await fetchAllGlobalViews(workspaceSlug.toString());
await fetchFilters(workspaceSlug.toString(), globalViewId.toString());
captureIssuesListOpenedEvent({
path: router.asPath,
element_id: globalViewId,
});
await fetchIssues(workspaceSlug.toString(), globalViewId.toString(), issueIds ? "mutation" : "init-loader");
routerFilterParams();
}

View File

@ -1,3 +1,5 @@
import { ISSUE_ORDER_BY_OPTIONS } from "./issue";
export type IssueEventProps = {
eventName: string;
payload: any;
@ -124,23 +126,40 @@ export const getProjectStateEventPayload = (payload: any) => {
export const getIssuesListOpenedPayload = (payload: any) => ({
element: elementFromPath(payload.path),
element_id: payload.element_id,
type: payload.project_id ? "Project" : "Workspace",
layout: payload?.displayFilters?.layout,
filters: payload?.filters,
display_properties: payload?.displayProperties,
});
export const getIssuesFilterEventPayload = (payload: any) => ({
filter_type: payload?.filter_type,
filter_property: payload?.filter_property,
layout: payload?.filters?.displayFilters?.layout,
current_filters: payload?.filters?.filters,
element: elementFromPath(payload.path),
element_id: payload.element_id,
type: payload.project_id ? "Project" : "Workspace",
layout: payload?.displayFilters?.layout,
filters: payload?.filters,
display_properties: payload?.displayProperties,
});
const elementFromPath = (path?: string) => {
if (path?.includes("workspace-views")) return "Workspace view";
export const getIssuesDisplayFilterPayload = (payload: any) => {
const property =
payload.property_type == "order_by"
? ISSUE_ORDER_BY_OPTIONS?.filter((option) => option.key === payload.property)?.[0]
.title.toLocaleLowerCase()
.replaceAll(" ", "_")
: payload.property;
return {
layout: payload?.filters?.displayFilters?.layout,
current_display_properties: payload?.filters?.displayProperties,
element: elementFromPath(payload.path),
element_id: payload.element_id,
display_property: payload.display_property,
property: property,
property_type: payload.property_type,
};
};
export const elementFromPath = (path?: string) => {
if (path?.includes("workspace-views")) return "Global view";
if (path?.includes("cycles")) return "Cycle";
if (path?.includes("modules")) return "Module";
if (path?.includes("views")) return "Project view";
@ -190,6 +209,12 @@ export const ISSUE_OPENED = "Issue opened";
export const FILTER_APPLIED = "Filter applied";
export const FILTER_REMOVED = "Filter removed";
export const FILTER_SEARCHED = "Filter searched";
// Issues Display Property Events
export const DP_APPLIED = "Display property applied";
export const DP_REMOVED = "Display property removed";
// Issues Layout Property Event
export const LP_UPDATED = "Layout property updated";
export const LAYOUT_CHANGED = "Layout changed";
// Project State Events
export const STATE_CREATED = "State created";
export const STATE_UPDATED = "State updated";

View File

@ -19,6 +19,9 @@ import {
getPageEventPayload,
ISSUES_LIST_OPENED,
getIssuesListOpenedPayload,
getIssuesFilterEventPayload,
getIssuesDisplayFilterPayload,
LP_UPDATED,
} from "constants/event-tracker";
export interface IEventTrackerStore {
@ -40,6 +43,8 @@ export interface IEventTrackerStore {
captureIssueEvent: (props: IssueEventProps) => void;
captureProjectStateEvent: (props: EventProps) => void;
captureIssuesListOpenedEvent: (payload: any) => void;
captureIssuesFilterEvent: (props: EventProps) => void;
captureIssuesDisplayFilterEvent: (props: EventProps) => void;
}
export class EventTrackerStore implements IEventTrackerStore {
@ -245,15 +250,46 @@ export class EventTrackerStore implements IEventTrackerStore {
/**
* @description: Captures the event whenever the issues list is opened.
* @param {string} path
* @param {any} filters
* @param {any} payload
*/
captureIssuesListOpenedEvent = (payload: any) => {
const eventPayload = {
...getIssuesListOpenedPayload(payload),
...this.getRequiredProperties,
type: this.getRequiredProperties.project_id ? "Project" : "Workspace",
};
posthog?.capture(ISSUES_LIST_OPENED, eventPayload);
this.setTrackElement(undefined);
};
/**
* @description: Captures the event whenever the issues filters are changed.
* @param {IssueEventProps} props
*/
captureIssuesFilterEvent = (props: EventProps) => {
const { eventName, payload } = props;
const eventPayload = {
...getIssuesFilterEventPayload(payload),
...this.getRequiredProperties,
type: this.getRequiredProperties.project_id ? "Project" : "Workspace",
};
posthog?.capture(eventName, eventPayload);
this.setTrackElement(undefined);
};
/**
* @description: Captures the event whenever the issues display-filters are changed.
* @param {IssueEventProps} props
*/
captureIssuesDisplayFilterEvent = (props: EventProps) => {
const { eventName, payload } = props;
const eventPayload = {
...getIssuesDisplayFilterPayload(payload),
...this.getRequiredProperties,
type: this.getRequiredProperties.project_id ? "Project" : "Workspace",
current_display_filter: eventName === LP_UPDATED ? payload?.filters?.displayFilters : undefined,
};
posthog?.capture(eventName, eventPayload);
this.setTrackElement(undefined);
};
}