This commit is contained in:
chamikaJ
2025-04-17 18:28:54 +05:30
parent f583291d8a
commit 8825b0410a
2837 changed files with 241385 additions and 127578 deletions

View File

@@ -0,0 +1,27 @@
.table-tag:hover {
text-decoration: underline;
cursor: pointer;
}
.custom-row .hover-button {
visibility: hidden;
transition:
visibility 0s,
opacity ease-in-out;
opacity: 0;
}
.custom-row:hover .hover-button {
visibility: visible;
opacity: 1;
}
@media (max-width: 1000px) {
.table-tag {
font-size: 10px;
}
.project-progress {
font-size: 10px;
}
}

View File

@@ -0,0 +1,132 @@
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAuthService } from '@/hooks/useAuth';
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
import { ColumnsType } from 'antd/es/table';
import { ColumnFilterItem } from 'antd/es/table/interface';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { NavigateFunction } from 'react-router-dom';
import Avatars from '../avatars/avatars';
import { ActionButtons } from './project-list-table/project-list-actions/project-list-actions';
import { CategoryCell } from './project-list-table/project-list-category/project-list-category';
import { ProgressListProgress } from './project-list-table/project-list-progress/progress-list-progress';
import { ProjectListUpdatedAt } from './project-list-table/project-list-updated-at/project-list-updated';
import { ProjectNameCell } from './project-list-table/project-name/project-name-cell';
import { useAppSelector } from '@/hooks/useAppSelector';
import { ProjectRateCell } from './project-list-table/project-list-favorite/project-rate-cell';
const createFilters = (items: { id: string; name: string }[]) =>
items.map(item => ({ text: item.name, value: item.id })) as ColumnFilterItem[];
interface ITableColumnsProps {
navigate: NavigateFunction;
filteredInfo: any;
}
const TableColumns = ({
navigate,
filteredInfo,
}: ITableColumnsProps): ColumnsType<IProjectViewModel> => {
const { t } = useTranslation('all-project-list');
const dispatch = useAppDispatch();
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
const { projectStatuses } = useAppSelector(state => state.projectStatusesReducer);
const { projectCategories } = useAppSelector(state => state.projectCategoriesReducer);
const { filteredCategories, filteredStatuses } = useAppSelector(
state => state.projectsReducer
);
const columns = useMemo(
() => [
{
title: '',
dataIndex: 'favorite',
key: 'favorite',
render: (text: string, record: IProjectViewModel) => (
<ProjectRateCell key={record.id} t={t} record={record} />
),
},
{
title: t('name'),
dataIndex: 'name',
key: 'name',
sorter: true,
showSorterTooltip: false,
defaultSortOrder: 'ascend',
render: (text: string, record: IProjectViewModel) => (
<ProjectNameCell navigate={navigate} key={record.id} t={t} record={record} />
),
},
{
title: t('client'),
dataIndex: 'client_name',
key: 'client_name',
sorter: true,
showSorterTooltip: false,
},
{
title: t('category'),
dataIndex: 'category',
key: 'category_id',
filters: createFilters(
projectCategories.map(category => ({ id: category.id || '', name: category.name || '' }))
),
filteredValue: filteredInfo.category_id || filteredCategories || [],
filterMultiple: true,
render: (text: string, record: IProjectViewModel) => (
<CategoryCell key={record.id} t={t} record={record} />
),
sorter: true,
},
{
title: t('status'),
dataIndex: 'status',
key: 'status_id',
filters: createFilters(
projectStatuses.map(status => ({ id: status.id || '', name: status.name || '' }))
),
filteredValue: filteredInfo.status_id || [],
filterMultiple: true,
sorter: true,
},
{
title: t('tasksProgress'),
dataIndex: 'tasksProgress',
key: 'tasksProgress',
render: (_: string, record: IProjectViewModel) => <ProgressListProgress record={record} />,
},
{
title: t('updated_at'),
dataIndex: 'updated_at',
key: 'updated_at',
sorter: true,
showSorterTooltip: false,
render: (_: string, record: IProjectViewModel) => <ProjectListUpdatedAt record={record} />,
},
{
title: t('members'),
dataIndex: 'names',
key: 'members',
render: (members: InlineMember[]) => <Avatars members={members} />,
},
{
title: '',
key: 'button',
dataIndex: '',
render: (record: IProjectViewModel) => (
<ActionButtons
t={t}
record={record}
dispatch={dispatch}
isOwnerOrAdmin={isOwnerOrAdmin}
/>
),
},
],
[t, projectCategories, projectStatuses, filteredInfo, filteredCategories, filteredStatuses]
);
return columns as ColumnsType<IProjectViewModel>;
};
export default TableColumns;

View File

@@ -0,0 +1,93 @@
import { useGetProjectsQuery } from '@/api/projects/projects.v1.api.service';
import { AppDispatch } from '@/app/store';
import { fetchProjectData, setProjectId, toggleProjectDrawer } from '@/features/project/project-drawer.slice';
import {
toggleArchiveProjectForAll,
toggleArchiveProject,
} from '@/features/projects/projectsSlice';
import { useAppSelector } from '@/hooks/useAppSelector';
import useIsProjectManager from '@/hooks/useIsProjectManager';
import { useAuthService } from '@/hooks/useAuth';
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import logger from '@/utils/errorLogger';
import { SettingOutlined, InboxOutlined } from '@ant-design/icons';
import { Tooltip, Button, Popconfirm, Space } from 'antd';
import { evt_projects_archive, evt_projects_archive_all, evt_projects_settings_click } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
interface ActionButtonsProps {
t: (key: string) => string;
record: IProjectViewModel;
dispatch: AppDispatch;
isOwnerOrAdmin: boolean;
}
export const ActionButtons: React.FC<ActionButtonsProps> = ({
t,
record,
dispatch,
isOwnerOrAdmin,
}) => {
// Add permission hooks
const isProjectManager = useIsProjectManager();
const isEditable = isOwnerOrAdmin;
const { trackMixpanelEvent } = useMixpanelTracking();
const { requestParams } = useAppSelector(state => state.projectsReducer);
const { refetch: refetchProjects } = useGetProjectsQuery(requestParams);
const handleSettingsClick = () => {
if (record.id) {
trackMixpanelEvent(evt_projects_settings_click);
dispatch(setProjectId(record.id));
dispatch(fetchProjectData(record.id));
dispatch(toggleProjectDrawer());
}
};
const handleArchiveClick = async () => {
if (!record.id) return;
try {
if (isOwnerOrAdmin) {
trackMixpanelEvent(evt_projects_archive_all);
await dispatch(toggleArchiveProjectForAll(record.id));
} else {
trackMixpanelEvent(evt_projects_archive);
await dispatch(toggleArchiveProject(record.id));
}
refetchProjects();
} catch (error) {
logger.error('Failed to archive project:', error);
}
};
return (
<Space onClick={e => e.stopPropagation()}>
<Tooltip title={t('setting')}>
<Button
className="action-button"
size="small"
onClick={handleSettingsClick}
icon={<SettingOutlined />}
/>
</Tooltip>
<Tooltip title={isEditable ? (record.archived ? t('unarchive') : t('archive')) : t('noPermission')}>
<Popconfirm
title={record.archived ? t('unarchive') : t('archive')}
description={record.archived ? t('unarchiveConfirm') : t('archiveConfirm')}
onConfirm={handleArchiveClick}
okText={t('yes')}
cancelText={t('no')}
disabled={!isEditable}
>
<Button
className="action-button"
size="small"
icon={<InboxOutlined />}
disabled={!isEditable}
/>
</Popconfirm>
</Tooltip>
</Space>
);
};

View File

@@ -0,0 +1,41 @@
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import { Tooltip, Tag } from 'antd';
import { TFunction } from 'i18next';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { setFilteredCategories, setRequestParams } from '@/features/projects/projectsSlice';
import '../../TableColumns.css';
import { useAppSelector } from '@/hooks/useAppSelector';
export const CategoryCell: React.FC<{
record: IProjectViewModel;
t: TFunction;
}> = ({ record, t }) => {
if (!record.category_name) return '-';
const { requestParams } = useAppSelector(
state => state.projectsReducer
);
const dispatch = useAppDispatch();
const newParams: Partial<typeof requestParams> = {};
const filterByCategory = (categoryId: string | undefined) => {
if (!categoryId) return;
newParams.categories = categoryId;
dispatch(setFilteredCategories([categoryId]));
dispatch(setRequestParams(newParams));
};
return (
<Tooltip title={`${t('clickToFilter')} "${record.category_name}"`}>
<Tag
color={record.category_color}
className="rounded-full table-tag"
onClick={e => {
e.stopPropagation();
filterByCategory(record.category_id);
}}
>
{record.category_name}
</Tag>
</Tooltip>
);
};

View File

@@ -0,0 +1,59 @@
import { useEffect, useState } from 'react';
import {
useGetProjectsQuery,
useToggleFavoriteProjectMutation,
} from '@/api/projects/projects.v1.api.service';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { colors } from '@/styles/colors';
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import { StarFilled } from '@ant-design/icons';
import { Button, ConfigProvider, Tooltip } from 'antd';
import { TFunction } from 'i18next';
import { useCallback, useMemo } from 'react';
export const ProjectRateCell: React.FC<{
record: IProjectViewModel;
t: TFunction;
}> = ({ record, t }) => {
const dispatch = useAppDispatch();
const [toggleFavoriteProject] = useToggleFavoriteProjectMutation();
const { requestParams } = useAppSelector(state => state.projectsReducer);
const { refetch: refetchProjects } = useGetProjectsQuery(requestParams);
const [isFavorite, setIsFavorite] = useState(record.favorite);
const handleFavorite = useCallback(async () => {
if (record.id) {
setIsFavorite(prev => !prev);
await toggleFavoriteProject(record.id);
// refetchProjects();
}
}, [dispatch, record.id]);
const checkIconColor = useMemo(
() => (isFavorite ? colors.yellow : colors.lightGray),
[isFavorite]
);
useEffect(() => {
setIsFavorite(record.favorite);}, [record.favorite]);
return (
<ConfigProvider wave={{ disabled: true }}>
<Tooltip title={record.favorite ? 'Remove from favorites' : 'Add to favourites'}>
<Button
type="text"
className="borderless-icon-btn"
style={{ backgroundColor: colors.transparent }}
shape="circle"
icon={<StarFilled style={{ color: checkIconColor, fontSize: '20px' }} />}
onClick={(e) => {
e.stopPropagation();
handleFavorite();
}}
/>
</Tooltip>
</ConfigProvider>
);
};

View File

@@ -0,0 +1,11 @@
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import { getTaskProgressTitle } from '@/utils/project-list-utils';
import { Tooltip, Progress } from 'antd';
export const ProgressListProgress: React.FC<{ record: IProjectViewModel }> = ({ record }) => {
return (
<Tooltip title={getTaskProgressTitle(record)}>
<Progress percent={record.progress} className="project-progress" />
</Tooltip>
);
};

View File

@@ -0,0 +1,12 @@
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import { calculateTimeDifference } from '@/utils/calculate-time-difference';
import { formatDateTimeWithLocale } from '@/utils/format-date-time-with-locale';
import { Tooltip } from 'antd';
export const ProjectListUpdatedAt: React.FC<{ record: IProjectViewModel }> = ({ record }) => {
return (
<Tooltip title={record.updated_at ? formatDateTimeWithLocale(record.updated_at) : ''}>
{record.updated_at ? calculateTimeDifference(record.updated_at) : ''}
</Tooltip>
);
};

View File

@@ -0,0 +1,69 @@
import {
useGetProjectsQuery,
useToggleFavoriteProjectMutation,
} from '@/api/projects/projects.v1.api.service';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
import { formatDateRange } from '@/utils/project-list-utils';
import { CalendarOutlined } from '@ant-design/icons';
import { Badge, Tooltip } from 'antd';
import { TFunction } from 'i18next';
import { NavigateFunction } from 'react-router-dom';
export const ProjectNameCell: React.FC<{
record: IProjectViewModel;
t: TFunction;
navigate: NavigateFunction;
}> = ({ record, t, navigate }) => {
const dispatch = useAppDispatch();
const [toggleFavoriteProject] = useToggleFavoriteProjectMutation();
const { requestParams } = useAppSelector(state => state.projectsReducer);
const { refetch: refetchProjects } = useGetProjectsQuery(requestParams);
const selectProject = (record: IProjectViewModel) => {
if (!record.id) return;
let viewTab = 'tasks-list';
switch (record.team_member_default_view) {
case 'TASK_LIST':
viewTab = 'tasks-list';
break;
case 'BOARD':
viewTab = 'board';
break;
default:
viewTab = 'tasks-list';
}
const searchParams = new URLSearchParams({
tab: viewTab,
pinned_tab: viewTab,
});
navigate({
pathname: `/worklenz/projects/${record.id}`,
search: searchParams.toString(),
});
};
return (
<div className="flex items-center">
<Badge color="geekblue" className="mr-2" />
<span className="cursor-pointer">
<span onClick={() => selectProject(record)}>{record.name}</span>
{(record.start_date || record.end_date) && (
<Tooltip
title={formatDateRange({
startDate: record.start_date || null,
endDate: record.end_date || null,
})}
overlayStyle={{ width: '200px' }}
>
<CalendarOutlined className="ml-2" />
</Tooltip>
)}
</span>
</div>
);
};