expand sub tasks
This commit is contained in:
@@ -22,9 +22,9 @@ const TaskDrawerActivityLog = () => {
|
||||
const { mode: themeMode } = useAppSelector(state => state.themeReducer);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(()=>{
|
||||
useEffect(() => {
|
||||
fetchActivityLogs();
|
||||
},[taskFormViewModel]);
|
||||
}, [taskFormViewModel]);
|
||||
|
||||
const fetchActivityLogs = async () => {
|
||||
if (!selectedTaskId) return;
|
||||
@@ -59,7 +59,8 @@ const TaskDrawerActivityLog = () => {
|
||||
name={activity.assigned_user?.name}
|
||||
/>
|
||||
<Typography.Text>{truncateText(activity.assigned_user?.name)}</Typography.Text>
|
||||
<ArrowRightOutlined />
|
||||
<ArrowRightOutlined />
|
||||
|
||||
<Tag color={'default'}>{truncateText(activity.log_type?.toUpperCase())}</Tag>
|
||||
</Flex>
|
||||
);
|
||||
@@ -67,8 +68,11 @@ const TaskDrawerActivityLog = () => {
|
||||
case IActivityLogAttributeTypes.LABEL:
|
||||
return (
|
||||
<Flex gap={4} align="center">
|
||||
<Tag color={activity.label_data?.color_code}>{truncateText(activity.label_data?.name)}</Tag>
|
||||
<ArrowRightOutlined />
|
||||
<Tag color={activity.label_data?.color_code}>
|
||||
{truncateText(activity.label_data?.name)}
|
||||
</Tag>
|
||||
<ArrowRightOutlined />
|
||||
|
||||
<Tag color={'default'}>{activity.log_type === 'create' ? 'ADD' : 'REMOVE'}</Tag>
|
||||
</Flex>
|
||||
);
|
||||
@@ -76,11 +80,24 @@ const TaskDrawerActivityLog = () => {
|
||||
case IActivityLogAttributeTypes.STATUS:
|
||||
return (
|
||||
<Flex gap={4} align="center">
|
||||
<Tag color={themeMode === 'dark' ? activity.previous_status?.color_code_dark : activity.previous_status?.color_code}>
|
||||
<Tag
|
||||
color={
|
||||
themeMode === 'dark'
|
||||
? activity.previous_status?.color_code_dark
|
||||
: activity.previous_status?.color_code
|
||||
}
|
||||
>
|
||||
{truncateText(activity.previous_status?.name) || 'None'}
|
||||
</Tag>
|
||||
<ArrowRightOutlined />
|
||||
<Tag color={themeMode === 'dark' ? activity.next_status?.color_code_dark : activity.next_status?.color_code}>
|
||||
<ArrowRightOutlined />
|
||||
|
||||
<Tag
|
||||
color={
|
||||
themeMode === 'dark'
|
||||
? activity.next_status?.color_code_dark
|
||||
: activity.next_status?.color_code
|
||||
}
|
||||
>
|
||||
{truncateText(activity.next_status?.name) || 'None'}
|
||||
</Tag>
|
||||
</Flex>
|
||||
@@ -89,11 +106,24 @@ const TaskDrawerActivityLog = () => {
|
||||
case IActivityLogAttributeTypes.PRIORITY:
|
||||
return (
|
||||
<Flex gap={4} align="center">
|
||||
<Tag color={themeMode === 'dark' ? activity.previous_priority?.color_code_dark : activity.previous_priority?.color_code}>
|
||||
<Tag
|
||||
color={
|
||||
themeMode === 'dark'
|
||||
? activity.previous_priority?.color_code_dark
|
||||
: activity.previous_priority?.color_code
|
||||
}
|
||||
>
|
||||
{truncateText(activity.previous_priority?.name) || 'None'}
|
||||
</Tag>
|
||||
<ArrowRightOutlined />
|
||||
<Tag color={themeMode === 'dark' ? activity.next_priority?.color_code_dark : activity.next_priority?.color_code}>
|
||||
<ArrowRightOutlined />
|
||||
|
||||
<Tag
|
||||
color={
|
||||
themeMode === 'dark'
|
||||
? activity.next_priority?.color_code_dark
|
||||
: activity.next_priority?.color_code
|
||||
}
|
||||
>
|
||||
{truncateText(activity.next_priority?.name) || 'None'}
|
||||
</Tag>
|
||||
</Flex>
|
||||
@@ -105,36 +135,31 @@ const TaskDrawerActivityLog = () => {
|
||||
<Tag color={activity.previous_phase?.color_code}>
|
||||
{truncateText(activity.previous_phase?.name) || 'None'}
|
||||
</Tag>
|
||||
<ArrowRightOutlined />
|
||||
<ArrowRightOutlined />
|
||||
|
||||
<Tag color={activity.next_phase?.color_code}>
|
||||
{truncateText(activity.next_phase?.name) || 'None'}
|
||||
</Tag>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
|
||||
case IActivityLogAttributeTypes.PROGRESS:
|
||||
return (
|
||||
<Flex gap={4} align="center">
|
||||
<Tag color="blue">
|
||||
{activity.previous || '0'}%
|
||||
</Tag>
|
||||
<ArrowRightOutlined />
|
||||
<Tag color="blue">
|
||||
{activity.current || '0'}%
|
||||
</Tag>
|
||||
<Tag color="blue">{activity.previous || '0'}%</Tag>
|
||||
<ArrowRightOutlined />
|
||||
|
||||
<Tag color="blue">{activity.current || '0'}%</Tag>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
|
||||
case IActivityLogAttributeTypes.WEIGHT:
|
||||
return (
|
||||
<Flex gap={4} align="center">
|
||||
<Tag color="purple">
|
||||
Weight: {activity.previous || '100'}
|
||||
</Tag>
|
||||
<ArrowRightOutlined />
|
||||
<Tag color="purple">
|
||||
Weight: {activity.current || '100'}
|
||||
</Tag>
|
||||
<Tag color="purple">Weight: {activity.previous || '100'}</Tag>
|
||||
<ArrowRightOutlined />
|
||||
|
||||
<Tag color="purple">Weight: {activity.current || '100'}</Tag>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -142,7 +167,8 @@ const TaskDrawerActivityLog = () => {
|
||||
return (
|
||||
<Flex gap={4} align="center">
|
||||
<Tag color={'default'}>{truncateText(activity.previous) || 'None'}</Tag>
|
||||
<ArrowRightOutlined />
|
||||
<ArrowRightOutlined />
|
||||
|
||||
<Tag color={'default'}>{truncateText(activity.current) || 'None'}</Tag>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { useState } from "react";
|
||||
import { ITaskAttachmentViewModel } from "@/types/tasks/task-attachment-view-model";
|
||||
import { Button, Modal, Spin, Tooltip, Typography, Popconfirm, message } from "antd";
|
||||
import { EyeOutlined, DownloadOutlined, DeleteOutlined, QuestionCircleOutlined, LoadingOutlined } from "@ant-design/icons";
|
||||
import { attachmentsApiService } from "@/api/attachments/attachments.api.service";
|
||||
import { IconsMap } from "@/shared/constants";
|
||||
import { useState } from 'react';
|
||||
import { ITaskAttachmentViewModel } from '@/types/tasks/task-attachment-view-model';
|
||||
import { Button, Modal, Spin, Tooltip, Typography, Popconfirm, message } from 'antd';
|
||||
import {
|
||||
EyeOutlined,
|
||||
DownloadOutlined,
|
||||
DeleteOutlined,
|
||||
QuestionCircleOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { attachmentsApiService } from '@/api/attachments/attachments.api.service';
|
||||
import { IconsMap } from '@/shared/constants';
|
||||
import './attachments-preview.css';
|
||||
import taskAttachmentsApiService from "@/api/tasks/task-attachments.api.service";
|
||||
import logger from "@/utils/errorLogger";
|
||||
import taskCommentsApiService from "@/api/tasks/task-comments.api.service";
|
||||
import { useAppSelector } from "@/hooks/useAppSelector";
|
||||
import taskAttachmentsApiService from '@/api/tasks/task-attachments.api.service';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import taskCommentsApiService from '@/api/tasks/task-comments.api.service';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
interface AttachmentsPreviewProps {
|
||||
attachment: ITaskAttachmentViewModel;
|
||||
@@ -16,10 +22,10 @@ interface AttachmentsPreviewProps {
|
||||
isCommentAttachment?: boolean;
|
||||
}
|
||||
|
||||
const AttachmentsPreview = ({
|
||||
attachment,
|
||||
const AttachmentsPreview = ({
|
||||
attachment,
|
||||
onDelete,
|
||||
isCommentAttachment = false
|
||||
isCommentAttachment = false,
|
||||
}: AttachmentsPreviewProps) => {
|
||||
const { selectedTaskId } = useAppSelector(state => state.taskDrawerReducer);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
@@ -32,12 +38,12 @@ const AttachmentsPreview = ({
|
||||
const [previewedFileName, setPreviewedFileName] = useState<string | null>(null);
|
||||
|
||||
const getFileIcon = (type?: string) => {
|
||||
if (!type) return "search.png";
|
||||
return IconsMap[type] || "search.png";
|
||||
if (!type) return 'search.png';
|
||||
return IconsMap[type] || 'search.png';
|
||||
};
|
||||
|
||||
const isImageFile = (): boolean => {
|
||||
const imageTypes = ["jpeg", "jpg", "bmp", "gif", "webp", "png", "ico"];
|
||||
const imageTypes = ['jpeg', 'jpg', 'bmp', 'gif', 'webp', 'png', 'ico'];
|
||||
const type = attachment?.type;
|
||||
if (type) return imageTypes.includes(type);
|
||||
return false;
|
||||
@@ -53,12 +59,12 @@ const AttachmentsPreview = ({
|
||||
if (!id || !name) return;
|
||||
try {
|
||||
setDownloading(true);
|
||||
const api = isCommentAttachment
|
||||
const api = isCommentAttachment
|
||||
? attachmentsApiService.downloadAttachment // Assuming this exists, adjust as needed
|
||||
: attachmentsApiService.downloadAttachment;
|
||||
|
||||
|
||||
const res = await api(id, name);
|
||||
|
||||
|
||||
if (res && res.done) {
|
||||
const link = document.createElement('a');
|
||||
link.href = res.body || '';
|
||||
@@ -126,7 +132,7 @@ const AttachmentsPreview = ({
|
||||
|
||||
if (!extension) return;
|
||||
setIsVisible(true);
|
||||
|
||||
|
||||
if (isImage(extension)) {
|
||||
setCurrentFileType('image');
|
||||
} else if (isVideo(extension)) {
|
||||
@@ -149,9 +155,7 @@ const AttachmentsPreview = ({
|
||||
<>
|
||||
<div className="ant-upload-list-picture-card-container">
|
||||
{attachment && (
|
||||
<div
|
||||
className="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-picture-card"
|
||||
>
|
||||
<div className="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-picture-card">
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
@@ -165,26 +169,26 @@ const AttachmentsPreview = ({
|
||||
placement="bottom"
|
||||
>
|
||||
<div className="ant-upload-list-item-info">
|
||||
<img
|
||||
src={`/file-types/${getFileIcon(attachment.type)}`}
|
||||
className="file-icon"
|
||||
alt=""
|
||||
<img
|
||||
src={`/file-types/${getFileIcon(attachment.type)}`}
|
||||
className="file-icon"
|
||||
alt=""
|
||||
/>
|
||||
<div
|
||||
className="ant-upload-span"
|
||||
style={{
|
||||
backgroundImage: isImageFile() ? `url(${attachment.url})` : ''
|
||||
<div
|
||||
className="ant-upload-span"
|
||||
style={{
|
||||
backgroundImage: isImageFile() ? `url(${attachment.url})` : '',
|
||||
}}
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ant-upload-list-item-thumbnail"
|
||||
href={attachment.url}
|
||||
>
|
||||
{!isImageFile() && (
|
||||
<span
|
||||
className="anticon anticon-file-unknown"
|
||||
<span
|
||||
className="anticon anticon-file-unknown"
|
||||
style={{ fontSize: 34, color: '#cccccc' }}
|
||||
/>
|
||||
)}
|
||||
@@ -194,18 +198,18 @@ const AttachmentsPreview = ({
|
||||
</Tooltip>
|
||||
|
||||
<span className="ant-upload-list-item-actions">
|
||||
<Button
|
||||
type="text"
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
title="Preview file"
|
||||
title="Preview file"
|
||||
onClick={() => previewFile(attachment.url, attachment.id, attachment.name)}
|
||||
className="ant-upload-list-item-card-actions-btn"
|
||||
>
|
||||
<EyeOutlined />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
title="Download file"
|
||||
onClick={() => download(attachment.id, attachment.name)}
|
||||
@@ -223,8 +227,8 @@ const AttachmentsPreview = ({
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
title="Remove file"
|
||||
loading={deleting}
|
||||
@@ -247,23 +251,23 @@ const AttachmentsPreview = ({
|
||||
className="attachment-preview-modal"
|
||||
footer={[
|
||||
previewedFileId && previewedFileName && (
|
||||
<Button
|
||||
<Button
|
||||
key="download"
|
||||
onClick={() => download(previewedFileId, previewedFileName)}
|
||||
onClick={() => download(previewedFileId, previewedFileName)}
|
||||
loading={downloading}
|
||||
>
|
||||
<DownloadOutlined /> Download
|
||||
</Button>
|
||||
)
|
||||
),
|
||||
]}
|
||||
>
|
||||
<div className="preview-container text-center position-relative">
|
||||
<div className="preview-container text-center position-relative">
|
||||
{currentFileType === 'image' && (
|
||||
<>
|
||||
<img src={currentFileUrl || ''} className="img-fluid position-relative" alt="" />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{currentFileType === 'video' && (
|
||||
<>
|
||||
<video className="position-relative" controls>
|
||||
@@ -271,7 +275,7 @@ const AttachmentsPreview = ({
|
||||
</video>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{currentFileType === 'audio' && (
|
||||
<>
|
||||
<audio className="position-relative" controls>
|
||||
@@ -279,23 +283,21 @@ const AttachmentsPreview = ({
|
||||
</audio>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{currentFileType === 'document' && (
|
||||
<>
|
||||
{currentFileUrl && (
|
||||
<iframe
|
||||
<iframe
|
||||
src={`https://docs.google.com/viewer?url=${encodeURIComponent(currentFileUrl)}&embedded=true`}
|
||||
width="100%"
|
||||
height="500px"
|
||||
width="100%"
|
||||
height="500px"
|
||||
style={{ border: 'none' }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentFileType === 'unknown' && (
|
||||
<p>The preview for this file type is not available.</p>
|
||||
)}
|
||||
|
||||
{currentFileType === 'unknown' && <p>The preview for this file type is not available.</p>}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
.ant-upload-list.focused {
|
||||
border-color: #1890ff;
|
||||
background-color: #FAFAFA;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.ant-upload-select-picture-card {
|
||||
@@ -36,4 +36,4 @@
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { TFunction } from 'i18next';
|
||||
import './attachments-upload.css';
|
||||
import './attachments-upload.css';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
interface AttachmentsUploadProps {
|
||||
@@ -11,11 +11,11 @@ interface AttachmentsUploadProps {
|
||||
onFilesSelected: (files: File[]) => void;
|
||||
}
|
||||
|
||||
const AttachmentsUpload = ({
|
||||
t,
|
||||
loadingTask,
|
||||
uploading,
|
||||
onFilesSelected
|
||||
const AttachmentsUpload = ({
|
||||
t,
|
||||
loadingTask,
|
||||
uploading,
|
||||
onFilesSelected,
|
||||
}: AttachmentsUploadProps) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
@@ -46,7 +46,7 @@ const AttachmentsUpload = ({
|
||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
|
||||
if (!loadingTask && !uploading && e.dataTransfer.files.length > 0) {
|
||||
const filesArray = Array.from(e.dataTransfer.files);
|
||||
onFilesSelected(filesArray);
|
||||
@@ -54,41 +54,45 @@ const AttachmentsUpload = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className={`ant-upload-list ant-upload-list-picture-card ${isDragOver ? 'focused' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<div className="ant-upload ant-upload-select ant-upload-select-picture-card">
|
||||
<div
|
||||
className="ant-upload"
|
||||
tabIndex={0}
|
||||
<div
|
||||
className="ant-upload"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
backgroundColor: themeMode === 'dark' ? '#292929' : '#fafafa',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileChange}
|
||||
disabled={loadingTask || uploading}
|
||||
multiple
|
||||
/>
|
||||
<div>
|
||||
{uploading ? <LoadingOutlined spin /> : <PlusOutlined />}
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
fontSize: '11px',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
paddingLeft: '8px',
|
||||
paddingRight: '8px'
|
||||
}}>
|
||||
{uploading ? t('taskInfoTab.attachments.uploading') : t('taskInfoTab.attachments.chooseOrDropFileToUpload')}
|
||||
<div
|
||||
style={{
|
||||
marginTop: '8px',
|
||||
fontSize: '11px',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
paddingLeft: '8px',
|
||||
paddingRight: '8px',
|
||||
}}
|
||||
>
|
||||
{uploading
|
||||
? t('taskInfoTab.attachments.uploading')
|
||||
: t('taskInfoTab.attachments.chooseOrDropFileToUpload')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,4 +101,4 @@ const AttachmentsUpload = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default AttachmentsUpload;
|
||||
export default AttachmentsUpload;
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
.focused {
|
||||
border-color: #1890ff;
|
||||
background-color: #FAFAFA;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* Conversation-like styles */
|
||||
@@ -250,7 +250,7 @@
|
||||
}
|
||||
|
||||
.comment-time-separator::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
|
||||
@@ -61,7 +61,7 @@ const processMentions = (content: string) => {
|
||||
const linkify = (text: string) => {
|
||||
if (!text) return '';
|
||||
// Regex to match URLs (http, https, www)
|
||||
return text.replace(/(https?:\/\/[^\s]+|www\.[^\s]+)/g, (url) => {
|
||||
return text.replace(/(https?:\/\/[^\s]+|www\.[^\s]+)/g, url => {
|
||||
let href = url;
|
||||
if (!href.startsWith('http')) {
|
||||
href = 'http://' + href;
|
||||
@@ -83,7 +83,7 @@ const processContent = (content: string) => {
|
||||
return sanitizeHtml(processed);
|
||||
};
|
||||
|
||||
const TaskComments = ({ taskId, t }: { taskId?: string, t: TFunction }) => {
|
||||
const TaskComments = ({ taskId, t }: { taskId?: string; t: TFunction }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [comments, setComments] = useState<ITaskCommentViewModel[]>([]);
|
||||
const commentsViewRef = useRef<HTMLDivElement>(null);
|
||||
@@ -290,9 +290,7 @@ const TaskComments = ({ taskId, t }: { taskId?: string, t: TFunction }) => {
|
||||
key={item.id}
|
||||
author={<span style={authorStyle}>{item.member_name}</span>}
|
||||
datetime={<span style={dateStyle}>{fromNow(item.created_at || '')}</span>}
|
||||
avatar={
|
||||
<SingleAvatar name={item.member_name} avatarUrl={item.avatar_url}/>
|
||||
}
|
||||
avatar={<SingleAvatar name={item.member_name} avatarUrl={item.avatar_url} />}
|
||||
content={
|
||||
item.edit ? (
|
||||
<TaskViewCommentEdit commentData={item} onUpdated={commentUpdated} />
|
||||
|
||||
@@ -15,10 +15,10 @@ interface TaskViewCommentEditProps {
|
||||
// Helper function to prepare content for editing by removing HTML tags
|
||||
const prepareContentForEditing = (content: string): string => {
|
||||
if (!content) return '';
|
||||
|
||||
|
||||
// Replace mention spans with plain @mentions
|
||||
const withoutMentionSpans = content.replace(/<span class="mentions">@(\w+)<\/span>/g, '@$1');
|
||||
|
||||
|
||||
// Remove any other HTML tags
|
||||
return withoutMentionSpans.replace(/<[^>]*>/g, '');
|
||||
};
|
||||
@@ -27,7 +27,7 @@ const TaskViewCommentEdit = ({ commentData, onUpdated }: TaskViewCommentEditProp
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
|
||||
// Initialize content when component mounts
|
||||
useEffect(() => {
|
||||
if (commentData.content) {
|
||||
@@ -42,18 +42,18 @@ const TaskViewCommentEdit = ({ commentData, onUpdated }: TaskViewCommentEditProp
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!commentData.id || !commentData.task_id) return;
|
||||
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await taskCommentsApiService.update(commentData.id, {
|
||||
...commentData,
|
||||
content: content,
|
||||
});
|
||||
|
||||
|
||||
if (res.done) {
|
||||
commentData.content = content;
|
||||
onUpdated(commentData);
|
||||
|
||||
|
||||
// Dispatch event to notify that a comment was updated
|
||||
document.dispatchEvent(new Event('task-comment-update'));
|
||||
}
|
||||
@@ -77,7 +77,7 @@ const TaskViewCommentEdit = ({ commentData, onUpdated }: TaskViewCommentEditProp
|
||||
<Form.Item>
|
||||
<Input.TextArea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onChange={e => setContent(e.target.value)}
|
||||
autoSize={{ minRows: 3, maxRows: 6 }}
|
||||
style={textAreaStyle}
|
||||
placeholder="Type your comment here... Use @username to mention someone"
|
||||
@@ -96,4 +96,4 @@ const TaskViewCommentEdit = ({ commentData, onUpdated }: TaskViewCommentEditProp
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskViewCommentEdit;
|
||||
export default TaskViewCommentEdit;
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
|
||||
.custom-two-colors-row-table .ant-table-row:hover .dependency-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,13 @@ const DependenciesTable = ({
|
||||
render: record => (
|
||||
<Select
|
||||
value={record.dependency_type}
|
||||
options={[{ key: IDependencyType.BLOCKED_BY, value: IDependencyType.BLOCKED_BY, label: 'Blocked By' }]}
|
||||
options={[
|
||||
{
|
||||
key: IDependencyType.BLOCKED_BY,
|
||||
value: IDependencyType.BLOCKED_BY,
|
||||
label: 'Blocked By',
|
||||
},
|
||||
]}
|
||||
size="small"
|
||||
/>
|
||||
),
|
||||
@@ -176,7 +182,13 @@ const DependenciesTable = ({
|
||||
<Col span={6}>
|
||||
<Form.Item name="blockedBy" style={{ marginBottom: 0 }}>
|
||||
<Select
|
||||
options={[{ key: IDependencyType.BLOCKED_BY, value: IDependencyType.BLOCKED_BY, label: 'Blocked By' }]}
|
||||
options={[
|
||||
{
|
||||
key: IDependencyType.BLOCKED_BY,
|
||||
value: IDependencyType.BLOCKED_BY,
|
||||
label: 'Blocked By',
|
||||
},
|
||||
]}
|
||||
size="small"
|
||||
disabled
|
||||
/>
|
||||
|
||||
@@ -31,17 +31,20 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
||||
link.as = 'script';
|
||||
document.head.appendChild(link);
|
||||
};
|
||||
|
||||
|
||||
preloadTinyMCE();
|
||||
}, []);
|
||||
|
||||
const handleDescriptionChange = () => {
|
||||
if (!taskId) return;
|
||||
socket?.emit(SocketEvents.TASK_DESCRIPTION_CHANGE.toString(), JSON.stringify({
|
||||
task_id: taskId,
|
||||
description: content || null,
|
||||
parent_task: parentTaskId,
|
||||
}));
|
||||
socket?.emit(
|
||||
SocketEvents.TASK_DESCRIPTION_CHANGE.toString(),
|
||||
JSON.stringify({
|
||||
task_id: taskId,
|
||||
description: content || null,
|
||||
parent_task: parentTaskId,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -51,7 +54,9 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
||||
|
||||
const isClickedInsideWrapper = wrapper && wrapper.contains(target);
|
||||
const isClickedInsideEditor = document.querySelector('.tox-tinymce')?.contains(target);
|
||||
const isClickedInsideToolbarPopup = document.querySelector('.tox-menu, .tox-pop, .tox-collection')?.contains(target);
|
||||
const isClickedInsideToolbarPopup = document
|
||||
.querySelector('.tox-menu, .tox-pop, .tox-collection')
|
||||
?.contains(target);
|
||||
|
||||
if (
|
||||
isEditorOpen &&
|
||||
@@ -96,7 +101,9 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
||||
setIsEditorLoading(true);
|
||||
};
|
||||
|
||||
const darkModeStyles = themeMode === 'dark' ? `
|
||||
const darkModeStyles =
|
||||
themeMode === 'dark'
|
||||
? `
|
||||
body {
|
||||
background-color: #1e1e1e !important;
|
||||
color: #ffffff !important;
|
||||
@@ -104,27 +111,33 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
||||
body.mce-content-body[data-mce-placeholder]:not([contenteditable="false"]):before {
|
||||
color: #666666 !important;
|
||||
}
|
||||
` : '';
|
||||
`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef}>
|
||||
{isEditorOpen ? (
|
||||
<div style={{
|
||||
minHeight: '200px',
|
||||
backgroundColor: themeMode === 'dark' ? '#1e1e1e' : '#ffffff'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
minHeight: '200px',
|
||||
backgroundColor: themeMode === 'dark' ? '#1e1e1e' : '#ffffff',
|
||||
}}
|
||||
>
|
||||
{isEditorLoading && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
zIndex: 10,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '200px',
|
||||
backgroundColor: themeMode === 'dark' ? 'rgba(30, 30, 30, 0.8)' : 'rgba(255, 255, 255, 0.8)',
|
||||
color: themeMode === 'dark' ? '#ffffff' : '#000000'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: 10,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '200px',
|
||||
backgroundColor:
|
||||
themeMode === 'dark' ? 'rgba(30, 30, 30, 0.8)' : 'rgba(255, 255, 255, 0.8)',
|
||||
color: themeMode === 'dark' ? '#ffffff' : '#000000',
|
||||
}}
|
||||
>
|
||||
<div>Loading editor...</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -138,11 +151,25 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
||||
menubar: false,
|
||||
branding: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'charmap', 'preview',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||
'insertdatetime', 'media', 'table', 'code', 'wordcount' // Added wordcount
|
||||
'advlist',
|
||||
'autolink',
|
||||
'lists',
|
||||
'link',
|
||||
'charmap',
|
||||
'preview',
|
||||
'anchor',
|
||||
'searchreplace',
|
||||
'visualblocks',
|
||||
'code',
|
||||
'fullscreen',
|
||||
'insertdatetime',
|
||||
'media',
|
||||
'table',
|
||||
'code',
|
||||
'wordcount', // Added wordcount
|
||||
],
|
||||
toolbar: 'blocks |' +
|
||||
toolbar:
|
||||
'blocks |' +
|
||||
'bold italic underline strikethrough | ' +
|
||||
'bullist numlist | link | removeformat | help',
|
||||
content_style: `
|
||||
@@ -157,30 +184,34 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
||||
skin_url: `/tinymce/skins/ui/${themeMode === 'dark' ? 'oxide-dark' : 'oxide'}`,
|
||||
content_css_cors: true,
|
||||
auto_focus: true,
|
||||
init_instance_callback: (editor) => {
|
||||
editor.dom.setStyle(editor.getBody(), 'backgroundColor', themeMode === 'dark' ? '#1e1e1e' : '#ffffff');
|
||||
}
|
||||
init_instance_callback: editor => {
|
||||
editor.dom.setStyle(
|
||||
editor.getBody(),
|
||||
'backgroundColor',
|
||||
themeMode === 'dark' ? '#1e1e1e' : '#ffffff'
|
||||
);
|
||||
},
|
||||
}}
|
||||
onEditorChange={handleEditorChange}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
<div
|
||||
onClick={handleOpenEditor}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
style={{
|
||||
minHeight: '32px',
|
||||
style={{
|
||||
minHeight: '32px',
|
||||
padding: '4px 11px',
|
||||
border: `1px solid ${isHovered ? (themeMode === 'dark' ? '#177ddc' : '#40a9ff') : 'transparent'}`,
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
color: themeMode === 'dark' ? '#ffffff' : '#000000',
|
||||
transition: 'border-color 0.3s ease'
|
||||
transition: 'border-color 0.3s ease',
|
||||
}}
|
||||
>
|
||||
{content ? (
|
||||
<div
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}
|
||||
style={{ color: 'inherit' }}
|
||||
/>
|
||||
@@ -195,4 +226,4 @@ const DescriptionEditor = ({ description, taskId, parentTaskId }: DescriptionEdi
|
||||
);
|
||||
};
|
||||
|
||||
export default DescriptionEditor;
|
||||
export default DescriptionEditor;
|
||||
|
||||
@@ -86,17 +86,17 @@ const TaskDrawerAssigneeSelector = ({ task }: TaskDrawerAssigneeSelectorProps) =
|
||||
|
||||
socket?.emit(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), JSON.stringify(body));
|
||||
socket?.once(
|
||||
SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(),
|
||||
(data: ITaskAssigneesUpdateResponse) => {
|
||||
dispatch(setTaskAssignee(data));
|
||||
if (tab === 'tasks-list') {
|
||||
dispatch(updateTasksListTaskAssignees(data));
|
||||
}
|
||||
if (tab === 'board') {
|
||||
dispatch(updateEnhancedKanbanTaskAssignees(data));
|
||||
}
|
||||
}
|
||||
);
|
||||
SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(),
|
||||
(data: ITaskAssigneesUpdateResponse) => {
|
||||
dispatch(setTaskAssignee(data));
|
||||
if (tab === 'tasks-list') {
|
||||
dispatch(updateTasksListTaskAssignees(data));
|
||||
}
|
||||
if (tab === 'board') {
|
||||
dispatch(updateEnhancedKanbanTaskAssignees(data));
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error updating assignee:', error);
|
||||
}
|
||||
@@ -148,16 +148,16 @@ const TaskDrawerAssigneeSelector = ({ task }: TaskDrawerAssigneeSelectorProps) =
|
||||
/>
|
||||
</div>
|
||||
<Flex vertical>
|
||||
<Typography.Text>{member.name}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{member.email}
|
||||
{member.pending_invitation && (
|
||||
<Typography.Text type="danger" style={{ fontSize: 10 }}>
|
||||
({t('pendingInvitation')})
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
<Typography.Text>{member.name}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{member.email}
|
||||
{member.pending_invitation && (
|
||||
<Typography.Text type="danger" style={{ fontSize: 10 }}>
|
||||
({t('pendingInvitation')})
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
</List.Item>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -14,7 +14,10 @@ import { ITaskViewModel } from '@/types/tasks/task.types';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { setStartDate, setTaskEndDate } from '@/features/task-drawer/task-drawer.slice';
|
||||
import { updateEnhancedKanbanTaskStartDate, updateEnhancedKanbanTaskEndDate } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||
import {
|
||||
updateEnhancedKanbanTaskStartDate,
|
||||
updateEnhancedKanbanTaskEndDate,
|
||||
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||
interface TaskDrawerDueDateProps {
|
||||
task: ITaskViewModel;
|
||||
@@ -61,18 +64,15 @@ const TaskDrawerDueDate = ({ task, t, form }: TaskDrawerDueDateProps) => {
|
||||
: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
})
|
||||
);
|
||||
socket?.once(
|
||||
SocketEvents.TASK_START_DATE_CHANGE.toString(),
|
||||
(data: IProjectTask) => {
|
||||
dispatch(setStartDate(data));
|
||||
|
||||
// Also update enhanced kanban if on board tab
|
||||
if (tab === 'board') {
|
||||
dispatch(updateEnhancedKanbanTaskStartDate({ task: data }));
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
socket?.once(SocketEvents.TASK_START_DATE_CHANGE.toString(), (data: IProjectTask) => {
|
||||
dispatch(setStartDate(data));
|
||||
|
||||
// Also update enhanced kanban if on board tab
|
||||
if (tab === 'board') {
|
||||
dispatch(updateEnhancedKanbanTaskStartDate({ task: data }));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to update start date:', error);
|
||||
}
|
||||
};
|
||||
@@ -90,17 +90,14 @@ const TaskDrawerDueDate = ({ task, t, form }: TaskDrawerDueDateProps) => {
|
||||
: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
})
|
||||
);
|
||||
socket?.once(
|
||||
SocketEvents.TASK_END_DATE_CHANGE.toString(),
|
||||
(data: IProjectTask) => {
|
||||
dispatch(setTaskEndDate(data));
|
||||
|
||||
// Also update enhanced kanban if on board tab
|
||||
if (tab === 'board') {
|
||||
dispatch(updateEnhancedKanbanTaskEndDate({ task: data }));
|
||||
}
|
||||
socket?.once(SocketEvents.TASK_END_DATE_CHANGE.toString(), (data: IProjectTask) => {
|
||||
dispatch(setTaskEndDate(data));
|
||||
|
||||
// Also update enhanced kanban if on board tab
|
||||
if (tab === 'board') {
|
||||
dispatch(updateEnhancedKanbanTaskEndDate({ task: data }));
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to update due date:', error);
|
||||
}
|
||||
@@ -134,7 +131,9 @@ const TaskDrawerDueDate = ({ task, t, form }: TaskDrawerDueDateProps) => {
|
||||
onClick={() => setIsShowStartDate(prev => !prev)}
|
||||
style={{ color: isShowStartDate ? 'red' : colors.skyBlue }}
|
||||
>
|
||||
{isShowStartDate ? t('taskInfoTab.details.hide-start-date') : t('taskInfoTab.details.show-start-date')}
|
||||
{isShowStartDate
|
||||
? t('taskInfoTab.details.hide-start-date')
|
||||
: t('taskInfoTab.details.show-start-date')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
|
||||
@@ -19,11 +19,11 @@ const TaskDrawerEstimation = ({ t, task, form }: TaskDrawerEstimationProps) => {
|
||||
|
||||
const handleTimeEstimationBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (!connected || !task.id) return;
|
||||
|
||||
|
||||
// Get current form values instead of using state
|
||||
const currentHours = form.getFieldValue('hours') || 0;
|
||||
const currentMinutes = form.getFieldValue('minutes') || 0;
|
||||
|
||||
|
||||
socket?.emit(
|
||||
SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString(),
|
||||
JSON.stringify({
|
||||
@@ -52,7 +52,7 @@ const TaskDrawerEstimation = ({ t, task, form }: TaskDrawerEstimationProps) => {
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={24}
|
||||
placeholder={t('taskInfoTab.details.hours')}
|
||||
placeholder={t('taskInfoTab.details.hours')}
|
||||
onBlur={handleTimeEstimationBlur}
|
||||
onChange={value => setHours(value || 0)}
|
||||
/>
|
||||
|
||||
@@ -58,18 +58,15 @@ const TaskDrawerLabels = ({ task, t }: TaskDrawerLabelsProps) => {
|
||||
team_id: currentSession?.team_id,
|
||||
};
|
||||
socket?.emit(SocketEvents.TASK_LABELS_CHANGE.toString(), JSON.stringify(labelData));
|
||||
socket?.once(
|
||||
SocketEvents.TASK_LABELS_CHANGE.toString(),
|
||||
(data: ILabelsChangeResponse) => {
|
||||
dispatch(setTaskLabels(data));
|
||||
if (tab === 'tasks-list') {
|
||||
dispatch(updateTaskLabel(data));
|
||||
}
|
||||
if (tab === 'board') {
|
||||
dispatch(updateEnhancedKanbanTaskLabels(data));
|
||||
}
|
||||
socket?.once(SocketEvents.TASK_LABELS_CHANGE.toString(), (data: ILabelsChangeResponse) => {
|
||||
dispatch(setTaskLabels(data));
|
||||
if (tab === 'tasks-list') {
|
||||
dispatch(updateTaskLabel(data));
|
||||
}
|
||||
);
|
||||
if (tab === 'board') {
|
||||
dispatch(updateEnhancedKanbanTaskLabels(data));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error changing label:', error);
|
||||
}
|
||||
@@ -84,18 +81,15 @@ const TaskDrawerLabels = ({ task, t }: TaskDrawerLabelsProps) => {
|
||||
team_id: currentSession?.team_id,
|
||||
};
|
||||
socket?.emit(SocketEvents.CREATE_LABEL.toString(), JSON.stringify(labelData));
|
||||
socket?.once(
|
||||
SocketEvents.CREATE_LABEL.toString(),
|
||||
(data: ILabelsChangeResponse) => {
|
||||
dispatch(setTaskLabels(data));
|
||||
if (tab === 'tasks-list') {
|
||||
dispatch(updateTaskLabel(data));
|
||||
}
|
||||
if (tab === 'board') {
|
||||
dispatch(updateEnhancedKanbanTaskLabels(data));
|
||||
}
|
||||
}
|
||||
);
|
||||
socket?.once(SocketEvents.CREATE_LABEL.toString(), (data: ILabelsChangeResponse) => {
|
||||
dispatch(setTaskLabels(data));
|
||||
if (tab === 'tasks-list') {
|
||||
dispatch(updateTaskLabel(data));
|
||||
}
|
||||
if (tab === 'board') {
|
||||
dispatch(updateEnhancedKanbanTaskLabels(data));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -119,57 +113,57 @@ const TaskDrawerLabels = ({ task, t }: TaskDrawerLabelsProps) => {
|
||||
onChange={e => setSearchQuery(e.currentTarget.value)}
|
||||
placeholder={t('taskInfoTab.labels.labelInputPlaceholder')}
|
||||
onKeyDown={e => {
|
||||
const isLabel = filteredLabelData.findIndex(
|
||||
label => label.name?.toLowerCase() === searchQuery.toLowerCase()
|
||||
);
|
||||
if (isLabel === -1) {
|
||||
if (e.key === 'Enter') {
|
||||
handleCreateLabel();
|
||||
setSearchQuery('');
|
||||
}
|
||||
}
|
||||
const isLabel = filteredLabelData.findIndex(
|
||||
label => label.name?.toLowerCase() === searchQuery.toLowerCase()
|
||||
);
|
||||
if (isLabel === -1) {
|
||||
if (e.key === 'Enter') {
|
||||
handleCreateLabel();
|
||||
setSearchQuery('');
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<List style={{ padding: 0, maxHeight: 300, overflow: 'scroll' }}>
|
||||
{filteredLabelData.length ? (
|
||||
filteredLabelData.map(label => (
|
||||
<List.Item
|
||||
className={themeMode === 'dark' ? 'custom-list-item dark' : 'custom-list-item'}
|
||||
key={label.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
gap: 8,
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => handleLabelChange(label)}
|
||||
>
|
||||
<Checkbox
|
||||
id={label.id}
|
||||
checked={
|
||||
task?.labels
|
||||
? task?.labels.some(existingLabel => existingLabel.id === label.id)
|
||||
: false
|
||||
}
|
||||
onChange={e => e.preventDefault()}
|
||||
>
|
||||
<Flex gap={8}>
|
||||
<Badge color={label.color_code} />
|
||||
{label.name}
|
||||
</Flex>
|
||||
</Checkbox>
|
||||
</List.Item>
|
||||
))
|
||||
filteredLabelData.map(label => (
|
||||
<List.Item
|
||||
className={themeMode === 'dark' ? 'custom-list-item dark' : 'custom-list-item'}
|
||||
key={label.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
gap: 8,
|
||||
padding: '4px 8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => handleLabelChange(label)}
|
||||
>
|
||||
<Checkbox
|
||||
id={label.id}
|
||||
checked={
|
||||
task?.labels
|
||||
? task?.labels.some(existingLabel => existingLabel.id === label.id)
|
||||
: false
|
||||
}
|
||||
onChange={e => e.preventDefault()}
|
||||
>
|
||||
<Flex gap={8}>
|
||||
<Badge color={label.color_code} />
|
||||
{label.name}
|
||||
</Flex>
|
||||
</Checkbox>
|
||||
</List.Item>
|
||||
))
|
||||
) : (
|
||||
<Typography.Text
|
||||
style={{ color: colors.lightGray }}
|
||||
onClick={() => handleCreateLabel()}
|
||||
>
|
||||
{t('taskInfoTab.labels.labelsSelectorInputTip')}
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
style={{ color: colors.lightGray }}
|
||||
onClick={() => handleCreateLabel()}
|
||||
>
|
||||
{t('taskInfoTab.labels.labelsSelectorInputTip')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</List>
|
||||
</Flex>
|
||||
|
||||
@@ -14,7 +14,7 @@ interface TaskDrawerPhaseSelectorProps {
|
||||
|
||||
const TaskDrawerPhaseSelector = ({ phases, task }: TaskDrawerPhaseSelectorProps) => {
|
||||
const { socket, connected } = useSocket();
|
||||
|
||||
|
||||
const phaseMenuItems = phases?.map(phase => ({
|
||||
key: phase.id,
|
||||
value: phase.id,
|
||||
@@ -25,7 +25,7 @@ const TaskDrawerPhaseSelector = ({ phases, task }: TaskDrawerPhaseSelectorProps)
|
||||
socket?.emit(SocketEvents.TASK_PHASE_CHANGE.toString(), {
|
||||
task_id: task.id,
|
||||
phase_id: value,
|
||||
parent_task: task.parent_task_id || null
|
||||
parent_task: task.parent_task_id || null,
|
||||
});
|
||||
|
||||
// socket?.once(SocketEvents.TASK_PHASE_CHANGE.toString(), () => {
|
||||
|
||||
@@ -29,7 +29,7 @@ const PriorityDropdown = ({ task }: PriorityDropdownProps) => {
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const dispatch = useAppDispatch();
|
||||
const { tab } = useTabSearchParam();
|
||||
|
||||
|
||||
const handlePriorityChange = (priorityId: string) => {
|
||||
if (!task.id || !priorityId) return;
|
||||
|
||||
@@ -93,7 +93,7 @@ const PriorityDropdown = ({ task }: PriorityDropdownProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{(
|
||||
{
|
||||
<Select
|
||||
value={task?.priority_id}
|
||||
onChange={handlePriorityChange}
|
||||
@@ -105,10 +105,9 @@ const PriorityDropdown = ({ task }: PriorityDropdownProps) => {
|
||||
? selectedPriority?.color_code_dark
|
||||
: selectedPriority?.color_code + ALPHA_CHANNEL,
|
||||
}}
|
||||
|
||||
options={options}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,10 @@ import { setTaskStatus } from '@/features/task-drawer/task-drawer.slice';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { updateBoardTaskStatus } from '@/features/board/board-slice';
|
||||
import { updateTaskProgress, updateTaskStatus } from '@/features/tasks/tasks.slice';
|
||||
import { updateEnhancedKanbanTaskStatus, updateEnhancedKanbanTaskProgress } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||
import {
|
||||
updateEnhancedKanbanTaskStatus,
|
||||
updateEnhancedKanbanTaskProgress,
|
||||
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||
|
||||
interface TaskDrawerProgressProps {
|
||||
@@ -325,7 +328,12 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
||||
okText={t('taskProgress.confirmMarkAsDone', 'Yes, mark as done')}
|
||||
cancelText={t('taskProgress.cancelMarkAsDone', 'No, keep current status')}
|
||||
>
|
||||
<p>{t('taskProgress.markAsDoneDescription', 'You\'ve set the progress to 100%. Would you like to update the task status to "Done"?')}</p>
|
||||
<p>
|
||||
{t(
|
||||
'taskProgress.markAsDoneDescription',
|
||||
'You\'ve set the progress to 100%. Would you like to update the task status to "Done"?'
|
||||
)}
|
||||
</p>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,12 @@ import {
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { IRepeatOption, ITaskRecurring, ITaskRecurringSchedule, ITaskRecurringScheduleData } from '@/types/tasks/task-recurring-schedule';
|
||||
import {
|
||||
IRepeatOption,
|
||||
ITaskRecurring,
|
||||
ITaskRecurringSchedule,
|
||||
ITaskRecurringScheduleData,
|
||||
} from '@/types/tasks/task-recurring-schedule';
|
||||
import { ITaskViewModel } from '@/types/tasks/task.types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
@@ -47,7 +52,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
||||
{ label: t('wed'), value: 3, checked: false },
|
||||
{ label: t('thu'), value: 4, checked: false },
|
||||
{ label: t('fri'), value: 5, checked: false },
|
||||
{ label: t('sat'), value: 6, checked: false }
|
||||
{ label: t('sat'), value: 6, checked: false },
|
||||
];
|
||||
|
||||
const weekOptions = [
|
||||
@@ -55,7 +60,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
||||
{ label: t('second'), value: 2 },
|
||||
{ label: t('third'), value: 3 },
|
||||
{ label: t('fourth'), value: 4 },
|
||||
{ label: t('last'), value: 5 }
|
||||
{ label: t('last'), value: 5 },
|
||||
];
|
||||
|
||||
const dayOptions = daysOfWeek.map(d => ({ label: d.label, value: d.value }));
|
||||
@@ -91,7 +96,9 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
||||
if (selected) setRepeatOption(selected);
|
||||
}
|
||||
dispatch(updateRecurringChange(schedule));
|
||||
dispatch(setTaskRecurringSchedule({ schedule_id: schedule.id as string, task_id: task.id }));
|
||||
dispatch(
|
||||
setTaskRecurringSchedule({ schedule_id: schedule.id as string, task_id: task.id })
|
||||
);
|
||||
|
||||
setRecurring(checked);
|
||||
if (!checked) setShowConfig(false);
|
||||
@@ -114,16 +121,16 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
||||
|
||||
const getSelectedDays = () => {
|
||||
return daysOfWeek
|
||||
.filter(day => day.checked) // Get only the checked days
|
||||
.map(day => day.value); // Extract their numeric values
|
||||
}
|
||||
.filter(day => day.checked) // Get only the checked days
|
||||
.map(day => day.value); // Extract their numeric values
|
||||
};
|
||||
|
||||
const getUpdateBody = () => {
|
||||
if (!task.id || !task.schedule_id || !repeatOption.value) return;
|
||||
|
||||
const body: ITaskRecurringSchedule = {
|
||||
id: task.id,
|
||||
schedule_type: repeatOption.value
|
||||
schedule_type: repeatOption.value,
|
||||
};
|
||||
|
||||
switch (repeatOption.value) {
|
||||
@@ -156,7 +163,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
||||
break;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!task.id || !task.schedule_id) return;
|
||||
@@ -172,7 +179,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
||||
configVisibleChange(false);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("handleSave", e);
|
||||
logger.error('handleSave', e);
|
||||
} finally {
|
||||
setUpdatingData(false);
|
||||
}
|
||||
@@ -207,14 +214,13 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
||||
updateDaysOfWeek();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("getScheduleData", e);
|
||||
}
|
||||
finally {
|
||||
logger.error('getScheduleData', e);
|
||||
} finally {
|
||||
setLoadingData(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleResponse = (response: ITaskRecurringScheduleData) => {
|
||||
if (!task || !response.task_id) return;
|
||||
@@ -259,7 +265,7 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => {
|
||||
<Checkbox.Group
|
||||
options={daysOfWeek.map(day => ({
|
||||
label: day.label,
|
||||
value: day.value
|
||||
value: day.value,
|
||||
}))}
|
||||
value={selectedDays}
|
||||
onChange={handleDayCheckboxChange}
|
||||
|
||||
@@ -51,7 +51,9 @@ const InfoTabFooter = () => {
|
||||
const [members, setMembers] = useState<ITeamMember[]>([]);
|
||||
const [membersLoading, setMembersLoading] = useState<boolean>(false);
|
||||
|
||||
const [selectedMembers, setSelectedMembers] = useState<{ team_member_id: string; name: string }[]>([]);
|
||||
const [selectedMembers, setSelectedMembers] = useState<
|
||||
{ team_member_id: string; name: string }[]
|
||||
>([]);
|
||||
const [commentValue, setCommentValue] = useState<string>('');
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
|
||||
@@ -102,20 +104,23 @@ const InfoTabFooter = () => {
|
||||
key: member.id,
|
||||
})) ?? [];
|
||||
|
||||
const memberSelectHandler = useCallback((member: IMentionMemberSelectOption) => {
|
||||
if (!member?.value || !member?.label) return;
|
||||
|
||||
// Find the member ID from the members list using the name
|
||||
const selectedMember = members.find(m => m.name === member.value);
|
||||
if (!selectedMember) return;
|
||||
|
||||
// Add to selected members if not already present
|
||||
setSelectedMembers(prev =>
|
||||
prev.some(mention => mention.team_member_id === selectedMember.id)
|
||||
? prev
|
||||
: [...prev, { team_member_id: selectedMember.id!, name: selectedMember.name! }]
|
||||
);
|
||||
}, [members]);
|
||||
const memberSelectHandler = useCallback(
|
||||
(member: IMentionMemberSelectOption) => {
|
||||
if (!member?.value || !member?.label) return;
|
||||
|
||||
// Find the member ID from the members list using the name
|
||||
const selectedMember = members.find(m => m.name === member.value);
|
||||
if (!selectedMember) return;
|
||||
|
||||
// Add to selected members if not already present
|
||||
setSelectedMembers(prev =>
|
||||
prev.some(mention => mention.team_member_id === selectedMember.id)
|
||||
? prev
|
||||
: [...prev, { team_member_id: selectedMember.id!, name: selectedMember.name! }]
|
||||
);
|
||||
},
|
||||
[members]
|
||||
);
|
||||
|
||||
const handleCommentChange = useCallback((value: string) => {
|
||||
setCommentValue(value);
|
||||
@@ -149,7 +154,7 @@ const InfoTabFooter = () => {
|
||||
setAttachmentComment(false);
|
||||
setIsCommentBoxExpand(false);
|
||||
setCommentValue('');
|
||||
|
||||
|
||||
// Dispatch event to notify that a comment was created
|
||||
// This will trigger scrolling to the new comment
|
||||
document.dispatchEvent(new Event('task-comment-create'));
|
||||
|
||||
@@ -98,12 +98,9 @@ const NotifyMemberSelector = ({ task, t }: NotifyMemberSelectorProps) => {
|
||||
mode: checked ? 0 : 1,
|
||||
};
|
||||
socket?.emit(SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(), body);
|
||||
socket?.once(
|
||||
SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(),
|
||||
(data: InlineMember[]) => {
|
||||
dispatch(setTaskSubscribers(data));
|
||||
}
|
||||
);
|
||||
socket?.once(SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(), (data: InlineMember[]) => {
|
||||
dispatch(setTaskSubscribers(data));
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error notifying member:', error);
|
||||
}
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
|
||||
.task-dependency:hover .task-dependency-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ import {
|
||||
} from '@/features/tasks/tasks.slice';
|
||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import {
|
||||
setShowTaskDrawer,
|
||||
setSelectedTaskId,
|
||||
fetchTask
|
||||
import {
|
||||
setShowTaskDrawer,
|
||||
setSelectedTaskId,
|
||||
fetchTask,
|
||||
} from '@/features/task-drawer/task-drawer.slice';
|
||||
import { updateSubtask } from '@/features/board/board-slice';
|
||||
|
||||
@@ -53,7 +53,7 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
||||
|
||||
const createRequestBody = (taskName: string): ITaskCreateRequest | null => {
|
||||
if (!projectId || !currentSession) return null;
|
||||
|
||||
|
||||
const body: ITaskCreateRequest = {
|
||||
project_id: projectId,
|
||||
name: taskName,
|
||||
@@ -63,7 +63,7 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
||||
|
||||
const groupBy = getCurrentGroup();
|
||||
const task = taskFormViewModel?.task;
|
||||
|
||||
|
||||
if (groupBy.value === GROUP_BY_STATUS_VALUE) {
|
||||
body.status_id = task?.status_id;
|
||||
} else if (groupBy.value === GROUP_BY_PRIORITY_VALUE) {
|
||||
@@ -75,7 +75,7 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
||||
if (selectedTaskId) {
|
||||
body.parent_task_id = selectedTaskId;
|
||||
}
|
||||
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
@@ -86,13 +86,13 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
||||
setCreatingTask(true);
|
||||
const body = createRequestBody(taskName);
|
||||
if (!body) return;
|
||||
|
||||
|
||||
socket?.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
|
||||
socket?.once(SocketEvents.QUICK_TASK.toString(), (task: IProjectTask) => {
|
||||
if (task.parent_task_id) {
|
||||
refreshSubTasks();
|
||||
dispatch(updateSubtask({ sectionId: '', subtask: task, mode: 'add' }));
|
||||
|
||||
|
||||
// Note: Enhanced kanban updates are now handled by the global socket handler
|
||||
// No need to dispatch here as it will be handled by useTaskSocketHandlers
|
||||
}
|
||||
@@ -111,18 +111,24 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
||||
|
||||
try {
|
||||
await tasksApiService.deleteTask(taskId);
|
||||
dispatch(updateSubtask({ sectionId: '', subtask: { id: taskId, parent_task_id: selectedTaskId || '' }, mode: 'delete' }));
|
||||
|
||||
dispatch(
|
||||
updateSubtask({
|
||||
sectionId: '',
|
||||
subtask: { id: taskId, parent_task_id: selectedTaskId || '' },
|
||||
mode: 'delete',
|
||||
})
|
||||
);
|
||||
|
||||
// Note: Enhanced kanban updates are now handled by the global socket handler
|
||||
// No need to dispatch here as it will be handled by useTaskSocketHandlers
|
||||
|
||||
|
||||
refreshSubTasks();
|
||||
} catch (error) {
|
||||
logger.error('Error deleting subtask:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnBlur = () => {
|
||||
const handleOnBlur = () => {
|
||||
if (newTaskName.trim() === '') {
|
||||
setIsEdit(true);
|
||||
return;
|
||||
@@ -150,15 +156,15 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
||||
|
||||
const handleEditSubTask = (taskId: string) => {
|
||||
if (!taskId || !projectId) return;
|
||||
|
||||
|
||||
// Close the current drawer and open the drawer for the selected sub task
|
||||
dispatch(setShowTaskDrawer(false));
|
||||
|
||||
|
||||
// Small delay to ensure the current drawer is closed before opening the new one
|
||||
setTimeout(() => {
|
||||
dispatch(setSelectedTaskId(taskId));
|
||||
dispatch(setShowTaskDrawer(true));
|
||||
|
||||
|
||||
// Fetch task data for the subtask
|
||||
dispatch(fetchTask({ taskId, projectId }));
|
||||
}, 100);
|
||||
@@ -167,67 +173,77 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
||||
const getSubTasksProgress = () => {
|
||||
const ratio = taskFormViewModel?.task?.complete_ratio || 0;
|
||||
return ratio == Infinity ? 0 : ratio;
|
||||
}
|
||||
};
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
key: 'name',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
key: 'priority',
|
||||
render: (record: IProjectTask) => (
|
||||
<Tag
|
||||
color={themeMode === 'dark' ? record.priority_color_dark : record.priority_color}
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
{record.priority_name}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
render: (record: IProjectTask) => (
|
||||
<Tag
|
||||
color={themeMode === 'dark' ? record.status_color_dark : record.status_color}
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
{record.status_name}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'assignee',
|
||||
render: (record: ISubTask) => <Avatars members={record.names || []} />,
|
||||
},
|
||||
{
|
||||
key: 'actionBtns',
|
||||
width: 80,
|
||||
render: (record: IProjectTask) => (
|
||||
<Flex gap={8} align="center" className="action-buttons">
|
||||
<Tooltip title={typeof t === 'function' ? t('taskInfoTab.subTasks.edit') : 'Edit'}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => record.id && handleEditSubTask(record.id)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="Are you sure?"
|
||||
icon={<ExclamationCircleFilled style={{ color: colors.vibrantOrange }} />}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onPopupClick={(e) => e.stopPropagation()}
|
||||
onConfirm={(e) => {handleDeleteSubTask(record.id)}}
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'name',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
key: 'priority',
|
||||
render: (record: IProjectTask) => (
|
||||
<Tag
|
||||
color={themeMode === 'dark' ? record.priority_color_dark : record.priority_color}
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
<Tooltip title="Delete">
|
||||
<Button shape="default" icon={<DeleteOutlined />} size="small" onClick={(e)=> e.stopPropagation()} />
|
||||
{record.priority_name}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
render: (record: IProjectTask) => (
|
||||
<Tag
|
||||
color={themeMode === 'dark' ? record.status_color_dark : record.status_color}
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
{record.status_name}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'assignee',
|
||||
render: (record: ISubTask) => <Avatars members={record.names || []} />,
|
||||
},
|
||||
{
|
||||
key: 'actionBtns',
|
||||
width: 80,
|
||||
render: (record: IProjectTask) => (
|
||||
<Flex gap={8} align="center" className="action-buttons">
|
||||
<Tooltip title={typeof t === 'function' ? t('taskInfoTab.subTasks.edit') : 'Edit'}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => record.id && handleEditSubTask(record.id)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
], [themeMode, t]);
|
||||
<Popconfirm
|
||||
title="Are you sure?"
|
||||
icon={<ExclamationCircleFilled style={{ color: colors.vibrantOrange }} />}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onPopupClick={e => e.stopPropagation()}
|
||||
onConfirm={e => {
|
||||
handleDeleteSubTask(record.id);
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Delete">
|
||||
<Button
|
||||
shape="default"
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
onClick={e => e.stopPropagation()}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
],
|
||||
[themeMode, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex vertical gap={12}>
|
||||
@@ -247,12 +263,12 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
||||
cursor: 'pointer',
|
||||
height: 36,
|
||||
},
|
||||
onClick: () => record.id && handleEditSubTask(record.id)
|
||||
onClick: () => record.id && handleEditSubTask(record.id),
|
||||
})}
|
||||
loading={loadingSubTasks}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
<div>
|
||||
{isEdit ? (
|
||||
<Input
|
||||
@@ -264,7 +280,11 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
||||
boxShadow: 'none',
|
||||
height: 38,
|
||||
}}
|
||||
placeholder={typeof t === 'function' ? t('taskInfoTab.subTasks.addSubTaskInputPlaceholder') : 'Type your task and hit enter'}
|
||||
placeholder={
|
||||
typeof t === 'function'
|
||||
? t('taskInfoTab.subTasks.addSubTaskInputPlaceholder')
|
||||
: 'Type your task and hit enter'
|
||||
}
|
||||
onBlur={handleInputBlur}
|
||||
onPressEnter={handleOnBlur}
|
||||
size="small"
|
||||
|
||||
@@ -49,7 +49,7 @@ const TaskDrawerTimeLog = ({ t, refreshTrigger = 0 }: TaskDrawerTimeLogProps) =>
|
||||
|
||||
for (const log of logs) {
|
||||
const timeSpentInSeconds = Number(log.time_spent || '0');
|
||||
|
||||
|
||||
// Calculate hours, minutes, seconds for individual time log
|
||||
const hours = Math.floor(timeSpentInSeconds / 3600);
|
||||
const minutes = Math.floor((timeSpentInSeconds % 3600) / 60);
|
||||
|
||||
@@ -19,11 +19,11 @@ interface TimeLogFormProps {
|
||||
mode?: 'create' | 'edit';
|
||||
}
|
||||
|
||||
const TimeLogForm = ({
|
||||
onCancel,
|
||||
const TimeLogForm = ({
|
||||
onCancel,
|
||||
onSubmitSuccess,
|
||||
initialValues,
|
||||
mode = 'create'
|
||||
initialValues,
|
||||
mode = 'create',
|
||||
}: TimeLogFormProps) => {
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const { socket, connected } = useSocket();
|
||||
@@ -44,23 +44,23 @@ const TimeLogForm = ({
|
||||
React.useEffect(() => {
|
||||
if (initialValues && mode === 'edit') {
|
||||
const createdAt = dayjs(initialValues.created_at);
|
||||
|
||||
|
||||
const startTime = dayjs(initialValues.start_time || initialValues.created_at);
|
||||
|
||||
|
||||
let endTime;
|
||||
if (initialValues.time_spent) {
|
||||
endTime = dayjs(startTime).add(initialValues.time_spent, 'second');
|
||||
} else {
|
||||
endTime = dayjs(initialValues.end_time || initialValues.created_at);
|
||||
}
|
||||
|
||||
|
||||
form.setFieldsValue({
|
||||
date: createdAt,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
description: initialValues.description || '',
|
||||
});
|
||||
|
||||
|
||||
setFormValues({
|
||||
date: createdAt,
|
||||
startTime: startTime,
|
||||
@@ -165,7 +165,7 @@ const TimeLogForm = ({
|
||||
await taskTimeLogsApiService.create(requestBody);
|
||||
}
|
||||
console.log('Received values:', values);
|
||||
|
||||
|
||||
// Call onSubmitSuccess if provided, otherwise just cancel
|
||||
if (onSubmitSuccess) {
|
||||
onSubmitSuccess();
|
||||
@@ -194,7 +194,7 @@ const TimeLogForm = ({
|
||||
justifySelf: 'flex-end',
|
||||
paddingTop: 16,
|
||||
paddingBottom: 0,
|
||||
overflow: 'visible'
|
||||
overflow: 'visible',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -19,7 +19,15 @@ type TimeLogItemProps = {
|
||||
};
|
||||
|
||||
const TimeLogItem = ({ log, onDelete }: TimeLogItemProps) => {
|
||||
const { user_name, avatar_url, time_spent_text, logged_by_timer, created_at, user_id, description } = log;
|
||||
const {
|
||||
user_name,
|
||||
avatar_url,
|
||||
time_spent_text,
|
||||
logged_by_timer,
|
||||
created_at,
|
||||
user_id,
|
||||
description,
|
||||
} = log;
|
||||
const { selectedTaskId } = useAppSelector(state => state.taskDrawerReducer);
|
||||
const dispatch = useAppDispatch();
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
@@ -57,10 +65,14 @@ const TimeLogItem = ({ log, onDelete }: TimeLogItemProps) => {
|
||||
|
||||
const renderActionButtons = () => {
|
||||
if (!canDelete) return null;
|
||||
|
||||
|
||||
return (
|
||||
<Space size={8}>
|
||||
<Button type="link" onClick={handleEdit} style={{ padding: '0', height: 'auto', fontSize: '14px' }}>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={handleEdit}
|
||||
style={{ padding: '0', height: 'auto', fontSize: '14px' }}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Popconfirm
|
||||
@@ -84,7 +96,9 @@ const TimeLogItem = ({ log, onDelete }: TimeLogItemProps) => {
|
||||
<Flex justify="space-between" align="start">
|
||||
<Flex vertical>
|
||||
<Typography.Text>
|
||||
<Typography.Text strong>{user_name}</Typography.Text> logged <Typography.Text strong>{time_spent_text}</Typography.Text> {renderLoggedByTimer()} {calculateTimeGap(created_at || '')}
|
||||
<Typography.Text strong>{user_name}</Typography.Text> logged{' '}
|
||||
<Typography.Text strong>{time_spent_text}</Typography.Text>{' '}
|
||||
{renderLoggedByTimer()} {calculateTimeGap(created_at || '')}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{formatDateTimeWithLocale(created_at || '')}
|
||||
@@ -92,7 +106,7 @@ const TimeLogItem = ({ log, onDelete }: TimeLogItemProps) => {
|
||||
</Flex>
|
||||
{renderActionButtons()}
|
||||
</Flex>
|
||||
|
||||
|
||||
{description && (
|
||||
<Typography.Text style={{ marginTop: 8, display: 'block' }}>
|
||||
{description}
|
||||
|
||||
@@ -12,11 +12,7 @@ const TimeLogList = ({ timeLoggedList, onRefresh }: TimeLogListProps) => {
|
||||
return (
|
||||
<Flex vertical gap={6}>
|
||||
{timeLoggedList.map(log => (
|
||||
<TimeLogItem
|
||||
key={log.id}
|
||||
log={log}
|
||||
onDelete={onRefresh}
|
||||
/>
|
||||
<TimeLogItem key={log.id} log={log} onDelete={onRefresh} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -47,26 +47,29 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
|
||||
|
||||
const handleDeleteTask = async () => {
|
||||
if (!selectedTaskId) return;
|
||||
|
||||
|
||||
// Set flag to indicate we're deleting the task
|
||||
isDeleting.current = true;
|
||||
|
||||
|
||||
const res = await tasksApiService.deleteTask(selectedTaskId);
|
||||
if (res.done) {
|
||||
// Explicitly clear the task parameter from URL
|
||||
clearTaskFromUrl();
|
||||
|
||||
|
||||
dispatch(setShowTaskDrawer(false));
|
||||
dispatch(setSelectedTaskId(null));
|
||||
dispatch(deleteTask({ taskId: selectedTaskId }));
|
||||
dispatch(deleteBoardTask({ sectionId: '', taskId: selectedTaskId }));
|
||||
|
||||
|
||||
// Reset the flag after a short delay
|
||||
setTimeout(() => {
|
||||
isDeleting.current = false;
|
||||
}, 100);
|
||||
if (taskFormViewModel?.task?.parent_task_id) {
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), taskFormViewModel?.task?.parent_task_id);
|
||||
socket?.emit(
|
||||
SocketEvents.GET_TASK_PROGRESS.toString(),
|
||||
taskFormViewModel?.task?.parent_task_id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
isDeleting.current = false;
|
||||
@@ -86,11 +89,15 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
|
||||
},
|
||||
];
|
||||
|
||||
const handleReceivedTaskNameChange = (data: { id: string; parent_task: string; name: string }) => {
|
||||
const handleReceivedTaskNameChange = (data: {
|
||||
id: string;
|
||||
parent_task: string;
|
||||
name: string;
|
||||
}) => {
|
||||
if (data.id === selectedTaskId) {
|
||||
const taskData = { ...data, manual_progress: false } as IProjectTask;
|
||||
dispatch(updateTaskName({ task: taskData }));
|
||||
|
||||
|
||||
// Also update enhanced kanban if on board tab
|
||||
if (tab === 'board') {
|
||||
dispatch(updateEnhancedKanbanTaskName({ task: taskData }));
|
||||
@@ -134,8 +141,8 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
|
||||
onBlur={handleInputBlur}
|
||||
placeholder={t('taskHeader.taskNamePlaceholder')}
|
||||
className="task-name-input"
|
||||
style={{
|
||||
width: '100%',
|
||||
style={{
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
showCount={true}
|
||||
@@ -143,16 +150,16 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
onClick={() => setIsEditing(true)}
|
||||
style={{
|
||||
margin: 0,
|
||||
<p
|
||||
onClick={() => setIsEditing(true)}
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: '4px 11px',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer',
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
width: '100%'
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{taskName || t('taskHeader.taskNamePlaceholder')}
|
||||
@@ -162,7 +169,7 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
|
||||
|
||||
<TaskDrawerStatusDropdown
|
||||
statuses={taskFormViewModel?.statuses ?? []}
|
||||
task={taskFormViewModel?.task ?? {} as ITaskViewModel}
|
||||
task={taskFormViewModel?.task ?? ({} as ITaskViewModel)}
|
||||
teamId={currentSession?.team_id ?? ''}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user