This commit is contained in:
Omindu Hirushka
2025-07-03 08:36:41 +05:30
parent 5d0777f67c
commit 08ee87da17
15 changed files with 219 additions and 35 deletions

View File

@@ -23,12 +23,15 @@ import { fetchBoardTaskGroups } from '@/features/board/board-slice';
import { setImportTaskTemplateDrawerOpen } from '@/features/project/project.slice'; import { setImportTaskTemplateDrawerOpen } from '@/features/project/project.slice';
import useTabSearchParam from '@/hooks/useTabSearchParam'; import useTabSearchParam from '@/hooks/useTabSearchParam';
import { fetchTaskGroups } from '@/features/tasks/tasks.slice'; import { fetchTaskGroups } from '@/features/tasks/tasks.slice';
import { evt_project_import_tasks } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const ImportTaskTemplate = () => { const ImportTaskTemplate = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { t } = useTranslation('project-view/import-task-templates'); const { t } = useTranslation('project-view/import-task-templates');
const { tab } = useTabSearchParam(); const { tab } = useTabSearchParam();
const { trackMixpanelEvent } = useMixpanelTracking();
const { importTaskTemplateDrawerOpen, projectId } = useAppSelector(state => state.projectReducer); const { importTaskTemplateDrawerOpen, projectId } = useAppSelector(state => state.projectReducer);
const [templates, setTemplates] = useState<ITaskTemplatesGetResponse[]>([]); const [templates, setTemplates] = useState<ITaskTemplatesGetResponse[]>([]);
@@ -86,6 +89,7 @@ const ImportTaskTemplate = () => {
if (!projectId || tasks.length === 0) return; if (!projectId || tasks.length === 0) return;
try { try {
trackMixpanelEvent(evt_project_import_tasks);
setImporting(true); setImporting(true);
const res = await taskTemplatesApiService.importTemplate(projectId, tasks); const res = await taskTemplatesApiService.importTemplate(projectId, tasks);
if (res.done) { if (res.done) {
@@ -117,7 +121,12 @@ const ImportTaskTemplate = () => {
footer={ footer={
<Flex justify="end" gap={10}> <Flex justify="end" gap={10}>
<Button onClick={handleClose}>{t('cancel')}</Button> <Button onClick={handleClose}>{t('cancel')}</Button>
<Button type="primary" onClick={handleImport} loading={importing} disabled={tasks.length === 0}> <Button
type="primary"
onClick={handleImport}
loading={importing}
disabled={tasks.length === 0}
>
{t('import')} {t('import')}
</Button> </Button>
</Flex> </Flex>

View File

@@ -8,6 +8,8 @@ import logger from '@/utils/errorLogger';
import { ITaskTemplateGetResponse } from '@/types/settings/task-templates.types'; import { ITaskTemplateGetResponse } from '@/types/settings/task-templates.types';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { setSelectedTasks } from '@/features/project/project.slice'; import { setSelectedTasks } from '@/features/project/project.slice';
import { evt_project_task_create } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
interface TaskTemplateDrawerProps { interface TaskTemplateDrawerProps {
showDrawer: boolean; showDrawer: boolean;
@@ -21,6 +23,7 @@ const TaskTemplateDrawer = ({
onClose, onClose,
}: TaskTemplateDrawerProps) => { }: TaskTemplateDrawerProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const {trackMixpanelEvent} = useMixpanelTracking();
const { t } = useTranslation('task-template-drawer'); const { t } = useTranslation('task-template-drawer');
const [form] = Form.useForm(); const [form] = Form.useForm();
const [templateData, setTemplateData] = useState<ITaskTemplateGetResponse>({}); const [templateData, setTemplateData] = useState<ITaskTemplateGetResponse>({});
@@ -75,6 +78,8 @@ const TaskTemplateDrawer = ({
const values = form.getFieldsValue(); const values = form.getFieldsValue();
if (!values.name || !templateData.tasks) return; if (!values.name || !templateData.tasks) return;
try { try {
trackMixpanelEvent(evt_project_task_create);
setCreatingTemplate(true); setCreatingTemplate(true);
const res = await taskTemplatesApiService.createTemplate({ const res = await taskTemplatesApiService.createTemplate({
name: values.name || '', name: values.name || '',

View File

@@ -6,6 +6,8 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
import { NewTaskType, updateTaskDate } from '@features/roadmap/roadmap-slice'; import { NewTaskType, updateTaskDate } from '@features/roadmap/roadmap-slice';
import { colors } from '@/styles/colors'; import { colors } from '@/styles/colors';
import RoadmapTaskCell from './roadmap-task-cell'; import RoadmapTaskCell from './roadmap-task-cell';
import { evt_roadmap_drag_change_date } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const RoadmapTable = () => { const RoadmapTable = () => {
// Get task list and expanded tasks from roadmap slice // Get task list and expanded tasks from roadmap slice
@@ -15,11 +17,18 @@ const RoadmapTable = () => {
const themeMode = useAppSelector(state => state.themeReducer.mode); const themeMode = useAppSelector(state => state.themeReducer.mode);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { trackMixpanelEvent } = useMixpanelTracking();
// function to handle date changes // function to handle date changes
const handleDateChange = (taskId: string, dateType: 'start' | 'end', date: Dayjs) => { const handleDateChange = (taskId: string, dateType: 'start' | 'end', date: Dayjs) => {
const updatedDate = date.toDate(); const updatedDate = date.toDate();
trackMixpanelEvent(evt_roadmap_drag_change_date, {
task_id: taskId,
date_type: dateType,
new_date: updatedDate.toISOString(),
});
dispatch( dispatch(
updateTaskDate({ updateTaskDate({
taskId, taskId,

View File

@@ -37,7 +37,7 @@ import logger from '@/utils/errorLogger';
// Components // Components
import EmptyListPlaceholder from '../../../../components/EmptyListPlaceholder'; import EmptyListPlaceholder from '../../../../components/EmptyListPlaceholder';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { evt_project_members_visit } from '@/shared/worklenz-analytics-events'; import { evt_project_members_visit, evt_people_delete } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking'; import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
interface PaginationType { interface PaginationType {
@@ -60,7 +60,7 @@ const ProjectViewMembers = () => {
const { trackMixpanelEvent } = useMixpanelTracking(); const { trackMixpanelEvent } = useMixpanelTracking();
const { refreshTimestamp } = useAppSelector(state => state.projectReducer); const { refreshTimestamp } = useAppSelector(state => state.projectReducer);
// State // State
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [members, setMembers] = useState<IProjectMembersViewModel>(); const [members, setMembers] = useState<IProjectMembersViewModel>();
@@ -104,6 +104,11 @@ const ProjectViewMembers = () => {
try { try {
const res = await projectMembersApiService.deleteProjectMember(memberId, projectId); const res = await projectMembersApiService.deleteProjectMember(memberId, projectId);
if (res.done) { if (res.done) {
trackMixpanelEvent(evt_people_delete, {
project_id: projectId,
member_id: memberId,
});
void getProjectMembers(); void getProjectMembers();
} }
} catch (error) { } catch (error) {
@@ -138,7 +143,14 @@ const ProjectViewMembers = () => {
// Effects // Effects
useEffect(() => { useEffect(() => {
void getProjectMembers(); void getProjectMembers();
}, [refreshTimestamp, projectId, pagination.current, pagination.pageSize, pagination.field, pagination.order]); }, [
refreshTimestamp,
projectId,
pagination.current,
pagination.pageSize,
pagination.field,
pagination.order,
]);
useEffect(() => { useEffect(() => {
trackMixpanelEvent(evt_project_members_visit); trackMixpanelEvent(evt_project_members_visit);
@@ -151,9 +163,13 @@ const ProjectViewMembers = () => {
title: t('nameColumn'), title: t('nameColumn'),
dataIndex: 'name', dataIndex: 'name',
sorter: true, sorter: true,
sortOrder: pagination.order === 'ascend' && pagination.field === 'name' ? 'ascend' : sortOrder:
pagination.order === 'descend' && pagination.field === 'name' ? 'descend' : null, pagination.order === 'ascend' && pagination.field === 'name'
render: (_,record: IProjectMemberViewModel) => ( ? 'ascend'
: pagination.order === 'descend' && pagination.field === 'name'
? 'descend'
: null,
render: (_, record: IProjectMemberViewModel) => (
<Flex gap={8} align="center"> <Flex gap={8} align="center">
<Avatar size={28} src={record.avatar_url}> <Avatar size={28} src={record.avatar_url}>
{record.name?.charAt(0)} {record.name?.charAt(0)}
@@ -167,8 +183,12 @@ const ProjectViewMembers = () => {
title: t('jobTitleColumn'), title: t('jobTitleColumn'),
dataIndex: 'job_title', dataIndex: 'job_title',
sorter: true, sorter: true,
sortOrder: pagination.order === 'ascend' && pagination.field === 'job_title' ? 'ascend' : sortOrder:
pagination.order === 'descend' && pagination.field === 'job_title' ? 'descend' : null, pagination.order === 'ascend' && pagination.field === 'job_title'
? 'ascend'
: pagination.order === 'descend' && pagination.field === 'job_title'
? 'descend'
: null,
render: (_, record: IProjectMemberViewModel) => ( render: (_, record: IProjectMemberViewModel) => (
<Typography.Text style={{ marginInlineStart: 12 }}> <Typography.Text style={{ marginInlineStart: 12 }}>
{record?.job_title || '-'} {record?.job_title || '-'}
@@ -180,8 +200,12 @@ const ProjectViewMembers = () => {
title: t('emailColumn'), title: t('emailColumn'),
dataIndex: 'email', dataIndex: 'email',
sorter: true, sorter: true,
sortOrder: pagination.order === 'ascend' && pagination.field === 'email' ? 'ascend' : sortOrder:
pagination.order === 'descend' && pagination.field === 'email' ? 'descend' : null, pagination.order === 'ascend' && pagination.field === 'email'
? 'ascend'
: pagination.order === 'descend' && pagination.field === 'email'
? 'descend'
: null,
render: (_, record: IProjectMemberViewModel) => ( render: (_, record: IProjectMemberViewModel) => (
<Typography.Text>{record.email}</Typography.Text> <Typography.Text>{record.email}</Typography.Text>
), ),
@@ -210,8 +234,12 @@ const ProjectViewMembers = () => {
title: t('accessColumn'), title: t('accessColumn'),
dataIndex: 'access', dataIndex: 'access',
sorter: true, sorter: true,
sortOrder: pagination.order === 'ascend' && pagination.field === 'access' ? 'ascend' : sortOrder:
pagination.order === 'descend' && pagination.field === 'access' ? 'descend' : null, pagination.order === 'ascend' && pagination.field === 'access'
? 'ascend'
: pagination.order === 'descend' && pagination.field === 'access'
? 'descend'
: null,
render: (_, record: IProjectMemberViewModel) => ( render: (_, record: IProjectMemberViewModel) => (
<Typography.Text style={{ textTransform: 'capitalize' }}>{record.access}</Typography.Text> <Typography.Text style={{ textTransform: 'capitalize' }}>{record.access}</Typography.Text>
), ),

View File

@@ -22,8 +22,17 @@ import { useAppSelector } from '@/hooks/useAppSelector';
import { SocketEvents } from '@/shared/socket-events'; import { SocketEvents } from '@/shared/socket-events';
import { useAuthService } from '@/hooks/useAuth'; import { useAuthService } from '@/hooks/useAuth';
import { useSocket } from '@/socket/socketContext'; import { useSocket } from '@/socket/socketContext';
import { setProject, setImportTaskTemplateDrawerOpen, setRefreshTimestamp } from '@features/project/project.slice'; import {
import { addTask, fetchTaskGroups, fetchTaskListColumns, IGroupBy } from '@features/tasks/tasks.slice'; setProject,
setImportTaskTemplateDrawerOpen,
setRefreshTimestamp,
} from '@features/project/project.slice';
import {
addTask,
fetchTaskGroups,
fetchTaskListColumns,
IGroupBy,
} from '@features/tasks/tasks.slice';
import ProjectStatusIcon from '@/components/common/project-status-icon/project-status-icon'; import ProjectStatusIcon from '@/components/common/project-status-icon/project-status-icon';
import { formatDate } from '@/utils/timeUtils'; import { formatDate } from '@/utils/timeUtils';
import { toggleSaveAsTemplateDrawer } from '@/features/projects/projectsSlice'; import { toggleSaveAsTemplateDrawer } from '@/features/projects/projectsSlice';
@@ -49,6 +58,14 @@ import useTabSearchParam from '@/hooks/useTabSearchParam';
import { addTaskCardToTheTop, fetchBoardTaskGroups } from '@/features/board/board-slice'; import { addTaskCardToTheTop, fetchBoardTaskGroups } from '@/features/board/board-slice';
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice'; import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
import {
evt_project_task_create,
evt_project_refresh_click,
evt_project_settings_click,
evt_project_import_tasks_click,
} from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const ProjectViewHeader = () => { const ProjectViewHeader = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation('project-view/project-view-header'); const { t } = useTranslation('project-view/project-view-header');
@@ -56,24 +73,29 @@ const ProjectViewHeader = () => {
const currentSession = useAuthService().getCurrentSession(); const currentSession = useAuthService().getCurrentSession();
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin(); const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
const isProjectManager = useIsProjectManager(); const isProjectManager = useIsProjectManager();
const { trackMixpanelEvent } = useMixpanelTracking();
const { tab } = useTabSearchParam(); const { tab } = useTabSearchParam();
const { socket } = useSocket(); const { socket } = useSocket();
const { const { project: selectedProject, projectId } = useAppSelector(state => state.projectReducer);
project: selectedProject,
projectId,
} = useAppSelector(state => state.projectReducer);
const { loadingGroups, groupBy } = useAppSelector(state => state.taskReducer); const { loadingGroups, groupBy } = useAppSelector(state => state.taskReducer);
const [creatingTask, setCreatingTask] = useState(false); const [creatingTask, setCreatingTask] = useState(false);
const handleRefresh = () => { const handleRefresh = () => {
if (!projectId) return; if (!projectId) return;
trackMixpanelEvent(evt_project_refresh_click, {
project_id: projectId,
tab: tab,
project_name: selectedProject?.name,
});
switch (tab) { switch (tab) {
case 'tasks-list': case 'tasks-list':
dispatch(fetchTaskListColumns(projectId)); dispatch(fetchTaskListColumns(projectId));
dispatch(fetchPhasesByProjectId(projectId)) dispatch(fetchPhasesByProjectId(projectId));
dispatch(fetchTaskGroups(projectId)); dispatch(fetchTaskGroups(projectId));
break; break;
case 'board': case 'board':
@@ -113,6 +135,11 @@ const ProjectViewHeader = () => {
const handleSettingsClick = () => { const handleSettingsClick = () => {
if (selectedProject?.id) { if (selectedProject?.id) {
trackMixpanelEvent(evt_project_settings_click, {
project_id: selectedProject.id,
project_name: selectedProject.name,
});
dispatch(setProjectId(selectedProject.id)); dispatch(setProjectId(selectedProject.id));
dispatch(fetchProjectData(selectedProject.id)); dispatch(fetchProjectData(selectedProject.id));
dispatch(toggleProjectDrawer()); dispatch(toggleProjectDrawer());
@@ -123,6 +150,14 @@ const ProjectViewHeader = () => {
try { try {
setCreatingTask(true); setCreatingTask(true);
trackMixpanelEvent(evt_project_task_create, {
project_id: selectedProject?.id,
project_name: selectedProject?.name,
reporter_id: currentSession?.id,
team_id: currentSession?.team_id,
creation_method: 'quick_create',
});
const body: ITaskCreateRequest = { const body: ITaskCreateRequest = {
name: DEFAULT_TASK_NAME, name: DEFAULT_TASK_NAME,
project_id: selectedProject?.id, project_id: selectedProject?.id,
@@ -155,6 +190,11 @@ const ProjectViewHeader = () => {
}; };
const handleImportTaskTemplate = () => { const handleImportTaskTemplate = () => {
trackMixpanelEvent(evt_project_import_tasks_click, {
project_id: selectedProject?.id,
project_name: selectedProject?.name,
});
dispatch(setImportTaskTemplateDrawerOpen(true)); dispatch(setImportTaskTemplateDrawerOpen(true));
}; };
@@ -221,7 +261,7 @@ const ProjectViewHeader = () => {
/> />
</Tooltip> </Tooltip>
{(isOwnerOrAdmin) && ( {isOwnerOrAdmin && (
<Tooltip title="Save as template"> <Tooltip title="Save as template">
<Button <Button
shape="circle" shape="circle"
@@ -298,10 +338,9 @@ const ProjectViewHeader = () => {
style={{ paddingInline: 0, marginBlockEnd: 12 }} style={{ paddingInline: 0, marginBlockEnd: 12 }}
extra={renderHeaderActions()} extra={renderHeaderActions()}
/> />
{createPortal(<ProjectDrawer onClose={() => { }} />, document.body, 'project-drawer')} {createPortal(<ProjectDrawer onClose={() => {}} />, document.body, 'project-drawer')}
{createPortal(<ImportTaskTemplate />, document.body, 'import-task-template')} {createPortal(<ImportTaskTemplate />, document.body, 'import-task-template')}
{createPortal(<SaveProjectAsTemplate />, document.body, 'save-project-as-template')} {createPortal(<SaveProjectAsTemplate />, document.body, 'save-project-as-template')}
</> </>
); );
}; };

View File

@@ -12,11 +12,14 @@ import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSli
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice'; import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
import { Empty } from 'antd'; import { Empty } from 'antd';
import useTabSearchParam from '@/hooks/useTabSearchParam'; import useTabSearchParam from '@/hooks/useTabSearchParam';
import { evt_project_task_list_visit } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const ProjectViewTaskList = () => { const ProjectViewTaskList = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { projectView } = useTabSearchParam(); const { projectView } = useTabSearchParam();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const { trackMixpanelEvent } = useMixpanelTracking();
const { projectId } = useAppSelector(state => state.projectReducer); const { projectId } = useAppSelector(state => state.projectReducer);
const { taskGroups, loadingGroups, groupBy, archived, fields, search } = useAppSelector( const { taskGroups, loadingGroups, groupBy, archived, fields, search } = useAppSelector(
@@ -38,6 +41,8 @@ const ProjectViewTaskList = () => {
}, [projectView, searchParams, setSearchParams]); }, [projectView, searchParams, setSearchParams]);
useEffect(() => { useEffect(() => {
trackMixpanelEvent(evt_project_task_list_visit);
if (projectId && groupBy) { if (projectId && groupBy) {
if (!loadingColumns) dispatch(fetchTaskListColumns(projectId)); if (!loadingColumns) dispatch(fetchTaskListColumns(projectId));
if (!loadingPhases) dispatch(fetchPhasesByProjectId(projectId)); if (!loadingPhases) dispatch(fetchPhasesByProjectId(projectId));
@@ -54,10 +59,10 @@ const ProjectViewTaskList = () => {
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}> <Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
<TaskListFilters position="list" /> <TaskListFilters position="list" />
{(taskGroups.length === 0 && !loadingGroups) ? ( {taskGroups.length === 0 && !loadingGroups ? (
<Empty description="No tasks group found" /> <Empty description="No tasks group found" />
) : ( ) : (
<Skeleton active loading={loadingGroups} className='mt-4 p-4'> <Skeleton active loading={loadingGroups} className="mt-4 p-4">
<TaskGroupWrapper taskGroups={taskGroups} groupBy={groupBy} /> <TaskGroupWrapper taskGroups={taskGroups} groupBy={groupBy} />
</Skeleton> </Skeleton>
)} )}

View File

@@ -1,5 +1,5 @@
import { Button, DatePicker, DatePickerProps, Flex, Select, Space } from 'antd'; import { Button, DatePicker, DatePickerProps, Flex, Select, Space } from 'antd';
import React, { useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { SettingOutlined } from '@ant-design/icons'; import { SettingOutlined } from '@ant-design/icons';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { setDate, setType, toggleSettingsDrawer } from '@/features/schedule/scheduleSlice'; import { setDate, setType, toggleSettingsDrawer } from '@/features/schedule/scheduleSlice';
@@ -11,6 +11,8 @@ import ScheduleDrawer from '@/features/schedule/ScheduleDrawer';
import GranttChart from '@/components/schedule/grant-chart/grantt-chart'; import GranttChart from '@/components/schedule/grant-chart/grantt-chart';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { PickerType } from '@/types/schedule/schedule-v2.types'; import { PickerType } from '@/types/schedule/schedule-v2.types';
import { evt_schedule_page_visit } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const { Option } = Select; const { Option } = Select;
@@ -31,6 +33,7 @@ const Schedule: React.FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const granttChartRef = useRef<any>(null); const granttChartRef = useRef<any>(null);
const { date, type } = useAppSelector(state => state.scheduleReducer); const { date, type } = useAppSelector(state => state.scheduleReducer);
const { trackMixpanelEvent } = useMixpanelTracking();
useDocumentTitle('Schedule'); useDocumentTitle('Schedule');
@@ -53,6 +56,10 @@ const Schedule: React.FC = () => {
console.log('Today:', today); console.log('Today:', today);
}; };
useEffect(() => {
trackMixpanelEvent(evt_schedule_page_visit);
}, []);
return ( return (
<div style={{ marginBlockStart: 65, minHeight: '90vh' }}> <div style={{ marginBlockStart: 65, minHeight: '90vh' }}>
<Flex align="center" justify="space-between"> <Flex align="center" justify="space-between">

View File

@@ -20,6 +20,11 @@ import { categoriesApiService } from '@/api/settings/categories/categories.api.s
import { IProjectCategory, IProjectCategoryViewModel } from '@/types/project/projectCategory.types'; import { IProjectCategory, IProjectCategoryViewModel } from '@/types/project/projectCategory.types';
import { useDocumentTitle } from '@/hooks/useDoumentTItle'; import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import {
evt_settings_categories_visit,
evt_settings_category_delete,
} from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const CategoriesSettings = () => { const CategoriesSettings = () => {
// localization // localization
@@ -28,6 +33,7 @@ const CategoriesSettings = () => {
useDocumentTitle('Manage Categories'); useDocumentTitle('Manage Categories');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { trackMixpanelEvent } = useMixpanelTracking();
// get currently hover row // get currently hover row
const [hoverRow, setHoverRow] = useState<string | null>(null); const [hoverRow, setHoverRow] = useState<string | null>(null);
const [categories, setCategories] = useState<IProjectCategoryViewModel[]>([]); const [categories, setCategories] = useState<IProjectCategoryViewModel[]>([]);
@@ -56,6 +62,10 @@ const CategoriesSettings = () => {
}; };
}, []); }, []);
useEffect(() => {
trackMixpanelEvent(evt_settings_categories_visit);
}, [trackMixpanelEvent]);
useEffect(() => { useEffect(() => {
getCategories(); getCategories();
}, [getCategories]); }, [getCategories]);
@@ -70,7 +80,9 @@ const CategoriesSettings = () => {
{ {
key: 'associatedTask', key: 'associatedTask',
title: t('associatedTaskColumn'), title: t('associatedTaskColumn'),
render: (record: IProjectCategoryViewModel) => <Typography.Text>{record.usage}</Typography.Text>, render: (record: IProjectCategoryViewModel) => (
<Typography.Text>{record.usage}</Typography.Text>
),
}, },
{ {
key: 'actionBtns', key: 'actionBtns',
@@ -82,7 +94,10 @@ const CategoriesSettings = () => {
icon={<ExclamationCircleFilled style={{ color: colors.vibrantOrange }} />} icon={<ExclamationCircleFilled style={{ color: colors.vibrantOrange }} />}
okText={t('deleteConfirmationOk')} okText={t('deleteConfirmationOk')}
cancelText={t('deleteConfirmationCancel')} cancelText={t('deleteConfirmationCancel')}
onConfirm={() => record.id && dispatch(deleteCategory(record.id))} onConfirm={() => {
trackMixpanelEvent(evt_settings_category_delete, { categoryId: record.id });
record.id && dispatch(deleteCategory(record.id));
}}
> >
<Tooltip title="Delete"> <Tooltip title="Delete">
<Button shape="default" icon={<DeleteOutlined />} size="small" /> <Button shape="default" icon={<DeleteOutlined />} size="small" />

View File

@@ -31,11 +31,14 @@ import { DEFAULT_PAGE_SIZE } from '@/shared/constants';
import ClientDrawer from './client-drawer'; import ClientDrawer from './client-drawer';
import { useDocumentTitle } from '@/hooks/useDoumentTItle'; import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import logger from '@/utils/errorLogger'; import logger from '@/utils/errorLogger';
import { evt_settings_clients_visit, evt_settings_clients_create } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const ClientsSettings: React.FC = () => { const ClientsSettings: React.FC = () => {
const { t } = useTranslation('settings/clients'); const { t } = useTranslation('settings/clients');
const { clients } = useAppSelector(state => state.clientReducer); const { clients } = useAppSelector(state => state.clientReducer);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { trackMixpanelEvent } = useMixpanelTracking();
useDocumentTitle('Manage Clients'); useDocumentTitle('Manage Clients');
@@ -62,6 +65,10 @@ const ClientsSettings: React.FC = () => {
}; };
}, [pagination, searchQuery, dispatch]); }, [pagination, searchQuery, dispatch]);
useEffect(() => {
trackMixpanelEvent(evt_settings_clients_visit);
}, [trackMixpanelEvent]);
useEffect(() => { useEffect(() => {
getClients(); getClients();
}, [searchQuery]); }, [searchQuery]);

View File

@@ -27,6 +27,11 @@ import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import JobTitleDrawer from './job-titles-drawer'; import JobTitleDrawer from './job-titles-drawer';
import logger from '@/utils/errorLogger'; import logger from '@/utils/errorLogger';
import {
evt_settings_job_titles_visit,
evt_settings_job_titles_create,
} from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
interface PaginationType { interface PaginationType {
current: number; current: number;
@@ -42,6 +47,7 @@ const JobTitlesSettings = () => {
const { t } = useTranslation('settings/job-titles'); const { t } = useTranslation('settings/job-titles');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useDocumentTitle('Manage Job Titles'); useDocumentTitle('Manage Job Titles');
const { trackMixpanelEvent } = useMixpanelTracking();
const [selectedJobId, setSelectedJobId] = useState<string | null>(null); const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
const [showDrawer, setShowDrawer] = useState(false); const [showDrawer, setShowDrawer] = useState(false);
@@ -73,6 +79,10 @@ const JobTitlesSettings = () => {
}; };
}, [pagination.current, pagination.pageSize, pagination.field, pagination.order, searchQuery]); }, [pagination.current, pagination.pageSize, pagination.field, pagination.order, searchQuery]);
useEffect(() => {
trackMixpanelEvent(evt_settings_job_titles_visit);
}, [trackMixpanelEvent]);
useEffect(() => { useEffect(() => {
getJobTitles(); getJobTitles();
}, [getJobTitles]); }, [getJobTitles]);
@@ -83,6 +93,8 @@ const JobTitlesSettings = () => {
}; };
const handleCreateClick = () => { const handleCreateClick = () => {
trackMixpanelEvent(evt_settings_job_titles_create);
setSelectedJobId(null); setSelectedJobId(null);
setShowDrawer(true); setShowDrawer(true);
}; };

View File

@@ -19,6 +19,11 @@ import { labelsApiService } from '@/api/taskAttributes/labels/labels.api.service
import CustomColorLabel from '@components/task-list-common/labelsSelector/custom-color-label'; import CustomColorLabel from '@components/task-list-common/labelsSelector/custom-color-label';
import { useDocumentTitle } from '@/hooks/useDoumentTItle'; import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import logger from '@/utils/errorLogger'; import logger from '@/utils/errorLogger';
import {
evt_settings_labels_visit,
evt_settings_labels_delete,
} from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const LabelsSettings = () => { const LabelsSettings = () => {
const { t } = useTranslation('settings/labels'); const { t } = useTranslation('settings/labels');
@@ -27,6 +32,7 @@ const LabelsSettings = () => {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [labels, setLabels] = useState<ITaskLabel[]>([]); const [labels, setLabels] = useState<ITaskLabel[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { trackMixpanelEvent } = useMixpanelTracking();
const filteredData = useMemo( const filteredData = useMemo(
() => () =>
@@ -49,12 +55,18 @@ const LabelsSettings = () => {
}; };
}, []); }, []);
useEffect(() => {
trackMixpanelEvent(evt_settings_labels_visit);
}, [trackMixpanelEvent]);
useEffect(() => { useEffect(() => {
getLabels(); getLabels();
}, [getLabels]); }, [getLabels]);
const deleteLabel = async (id: string) => { const deleteLabel = async (id: string) => {
try { try {
trackMixpanelEvent(evt_settings_labels_delete, { labelId: id });
const response = await labelsApiService.deleteById(id); const response = await labelsApiService.deleteById(id);
if (response.done) { if (response.done) {
getLabels(); getLabels();

View File

@@ -6,17 +6,24 @@ import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import { INotificationSettings } from '@/types/settings/notifications.types'; import { INotificationSettings } from '@/types/settings/notifications.types';
import { profileSettingsApiService } from '@/api/settings/profile/profile-settings.api.service'; import { profileSettingsApiService } from '@/api/settings/profile/profile-settings.api.service';
import logger from '@/utils/errorLogger'; import logger from '@/utils/errorLogger';
import { evt_settings_notifications_visit } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const NotificationsSettings = () => { const NotificationsSettings = () => {
const { t } = useTranslation('settings/notifications'); const { t } = useTranslation('settings/notifications');
const [form] = Form.useForm(); const [form] = Form.useForm();
const themeMode = useAppSelector(state => state.themeReducer.mode); const themeMode = useAppSelector(state => state.themeReducer.mode);
const { trackMixpanelEvent } = useMixpanelTracking();
const [notificationsSettings, setNotificationsSettings] = useState<INotificationSettings>({}); const [notificationsSettings, setNotificationsSettings] = useState<INotificationSettings>({});
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useDocumentTitle(t('title')); useDocumentTitle(t('title'));
useEffect(() => {
trackMixpanelEvent(evt_settings_notifications_visit);
}, [trackMixpanelEvent]);
const fetchNotificationsSettings = async () => { const fetchNotificationsSettings = async () => {
try { try {
setIsLoading(true); setIsLoading(true);

View File

@@ -11,10 +11,16 @@ import { ITaskTemplatesGetResponse } from '@/types/settings/task-templates.types
import logger from '@/utils/errorLogger'; import logger from '@/utils/errorLogger';
import { taskTemplatesApiService } from '@/api/task-templates/task-templates.api.service'; import { taskTemplatesApiService } from '@/api/task-templates/task-templates.api.service';
import { calculateTimeGap } from '@/utils/calculate-time-gap'; import { calculateTimeGap } from '@/utils/calculate-time-gap';
import {
evt_settings_task_templates_visit,
evt_settings_task_templates_delete,
} from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const TaskTemplatesSettings = () => { const TaskTemplatesSettings = () => {
const { t } = useTranslation('settings/task-templates'); const { t } = useTranslation('settings/task-templates');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { trackMixpanelEvent } = useMixpanelTracking();
const themeMode = useAppSelector(state => state.themeReducer.mode); const themeMode = useAppSelector(state => state.themeReducer.mode);
const [taskTemplates, setTaskTemplates] = useState<ITaskTemplatesGetResponse[]>([]); const [taskTemplates, setTaskTemplates] = useState<ITaskTemplatesGetResponse[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -34,12 +40,18 @@ const TaskTemplatesSettings = () => {
} }
}; };
useEffect(() => {
trackMixpanelEvent(evt_settings_task_templates_visit);
}, [trackMixpanelEvent]);
useEffect(() => { useEffect(() => {
fetchTaskTemplates(); fetchTaskTemplates();
}, []); }, []);
const handleDeleteTemplate = async (id: string) => { const handleDeleteTemplate = async (id: string) => {
try { try {
trackMixpanelEvent(evt_settings_task_templates_delete, { templateId: id });
setIsLoading(true); setIsLoading(true);
await taskTemplatesApiService.deleteTemplate(id); await taskTemplatesApiService.deleteTemplate(id);
await fetchTaskTemplates(); await fetchTaskTemplates();
@@ -111,10 +123,10 @@ const TaskTemplatesSettings = () => {
<Table <Table
loading={isLoading} loading={isLoading}
size="small" size="small"
pagination={{ pagination={{
size: 'small', size: 'small',
showSizeChanger: true, showSizeChanger: true,
showTotal: (total) => t('totalItems', { total }) showTotal: total => t('totalItems', { total }),
}} }}
columns={columns} columns={columns}
dataSource={taskTemplates} dataSource={taskTemplates}

View File

@@ -37,11 +37,15 @@ import { DEFAULT_PAGE_SIZE, PAGE_SIZE_OPTIONS } from '@/shared/constants';
import { teamMembersApiService } from '@/api/team-members/teamMembers.api.service'; import { teamMembersApiService } from '@/api/team-members/teamMembers.api.service';
import { colors } from '@/styles/colors'; import { colors } from '@/styles/colors';
import { evt_people_refresh_click, evt_people_delete, evt_settings_teams_visit } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const TeamMembersSettings = () => { const TeamMembersSettings = () => {
const { t } = useTranslation('settings/team-members'); const { t } = useTranslation('settings/team-members');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { socket } = useSocket(); const { socket } = useSocket();
const refreshTeamMembers = useAppSelector(state => state.memberReducer.refreshTeamMembers); // Listen to refresh flag const refreshTeamMembers = useAppSelector(state => state.memberReducer.refreshTeamMembers); // Listen to refresh flag
const { trackMixpanelEvent } = useMixpanelTracking();
const [model, setModel] = useState<ITeamMembersViewModel>({ total: 0, data: [] }); const [model, setModel] = useState<ITeamMembersViewModel>({ total: 0, data: [] });
const [searchQuery, setSearchQuery] = useState<string>(''); const [searchQuery, setSearchQuery] = useState<string>('');
@@ -96,6 +100,11 @@ const TeamMembersSettings = () => {
setIsLoading(true); setIsLoading(true);
const res = await teamMembersApiService.delete(record.id); const res = await teamMembersApiService.delete(record.id);
if (res.done) { if (res.done) {
trackMixpanelEvent(evt_people_delete, {
member_id: record.id,
member_name: record.name,
});
await getTeamMembers(); await getTeamMembers();
} }
} finally { } finally {
@@ -114,6 +123,9 @@ const TeamMembersSettings = () => {
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
setIsLoading(true); setIsLoading(true);
trackMixpanelEvent(evt_people_refresh_click);
getTeamMembers().finally(() => setIsLoading(false)); getTeamMembers().finally(() => setIsLoading(false));
}, [getTeamMembers]); }, [getTeamMembers]);
@@ -152,6 +164,8 @@ const TeamMembersSettings = () => {
}, [refreshTeamMembers, handleRefresh]); }, [refreshTeamMembers, handleRefresh]);
useEffect(() => { useEffect(() => {
trackMixpanelEvent(evt_settings_teams_visit);
getTeamMembers(); getTeamMembers();
}, [getTeamMembers]); }, [getTeamMembers]);
@@ -340,14 +354,11 @@ const TeamMembersSettings = () => {
/> />
</Card> </Card>
{createPortal( {createPortal(
<UpdateMemberDrawer <UpdateMemberDrawer selectedMemberId={selectedMemberId} onRoleUpdate={handleRoleUpdate} />,
selectedMemberId={selectedMemberId}
onRoleUpdate={handleRoleUpdate}
/>,
document.body document.body
)} )}
</div> </div>
); );
}; };
export default TeamMembersSettings; export default TeamMembersSettings;

View File

@@ -11,6 +11,9 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useDocumentTitle } from '@/hooks/useDoumentTItle'; import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import { ITeamGetResponse } from '@/types/teams/team.type'; import { ITeamGetResponse } from '@/types/teams/team.type';
import { evt_settings_teams_visit } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
const TeamsSettings = () => { const TeamsSettings = () => {
useDocumentTitle('Teams'); useDocumentTitle('Teams');
@@ -18,8 +21,11 @@ const TeamsSettings = () => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const { teamsList } = useAppSelector(state => state.teamReducer); const { teamsList } = useAppSelector(state => state.teamReducer);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { trackMixpanelEvent } = useMixpanelTracking();
useEffect(() => { useEffect(() => {
trackMixpanelEvent(evt_settings_teams_visit);
dispatch(fetchTeams()); dispatch(fetchTeams());
}, [dispatch]); }, [dispatch]);