init
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Drawer,
|
||||
Flex,
|
||||
Form,
|
||||
Input,
|
||||
List,
|
||||
Select,
|
||||
Skeleton,
|
||||
Typography,
|
||||
} from 'antd/es';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { ITaskTemplatesGetResponse } from '@/types/settings/task-templates.types';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { taskTemplatesApiService } from '@/api/task-templates/task-templates.api.service';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { fetchBoardTaskGroups } from '@/features/board/board-slice';
|
||||
import { setImportTaskTemplateDrawerOpen } from '@/features/project/project.slice';
|
||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||
import { fetchTaskGroups } from '@/features/tasks/tasks.slice';
|
||||
|
||||
const ImportTaskTemplate = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation('project-view/import-task-templates');
|
||||
const { tab } = useTabSearchParam();
|
||||
|
||||
const { importTaskTemplateDrawerOpen, projectId } = useAppSelector(state => state.projectReducer);
|
||||
const [templates, setTemplates] = useState<ITaskTemplatesGetResponse[]>([]);
|
||||
const [tasks, setTasks] = useState<IProjectTask[]>([]);
|
||||
const [loadingTemplates, setLoadingTemplates] = useState(false);
|
||||
const [loadingTasks, setLoadingTasks] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
form.resetFields();
|
||||
setTasks([]);
|
||||
dispatch(setImportTaskTemplateDrawerOpen(false));
|
||||
};
|
||||
|
||||
const fetchTemplates = async () => {
|
||||
try {
|
||||
setLoadingTemplates(true);
|
||||
const res = await taskTemplatesApiService.getTemplates();
|
||||
if (res.done) {
|
||||
setTemplates(res.body);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching templates', error);
|
||||
} finally {
|
||||
setLoadingTemplates(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTasks = async (templateId: string) => {
|
||||
try {
|
||||
setLoadingTasks(true);
|
||||
const res = await taskTemplatesApiService.getTemplate(templateId);
|
||||
if (res.done) {
|
||||
setTasks(res.body?.tasks || []);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error fetching tasks', error);
|
||||
} finally {
|
||||
setLoadingTasks(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTemplateSelect = (value: string) => {
|
||||
if (!value) return;
|
||||
fetchTasks(value);
|
||||
};
|
||||
|
||||
const handleAfterOpenChange = (open: boolean) => {
|
||||
if (open) {
|
||||
fetchTemplates();
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!projectId || tasks.length === 0) return;
|
||||
|
||||
try {
|
||||
setImporting(true);
|
||||
const res = await taskTemplatesApiService.importTemplate(projectId, tasks);
|
||||
if (res.done) {
|
||||
if (tab === 'board') dispatch(fetchBoardTaskGroups(projectId));
|
||||
if (tab === 'tasks-list') dispatch(fetchTaskGroups(projectId));
|
||||
dispatch(setImportTaskTemplateDrawerOpen(false));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error importing task template', error);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTask = (index: number) => {
|
||||
const newTasks = [...tasks];
|
||||
newTasks.splice(index, 1);
|
||||
setTasks(newTasks);
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={t('importTaskTemplate')}
|
||||
open={importTaskTemplateDrawerOpen}
|
||||
onClose={handleClose}
|
||||
width={650}
|
||||
afterOpenChange={handleAfterOpenChange}
|
||||
destroyOnClose
|
||||
footer={
|
||||
<Flex justify="end" gap={10}>
|
||||
<Button onClick={handleClose}>{t('cancel')}</Button>
|
||||
<Button type="primary" onClick={handleImport} loading={importing} disabled={tasks.length === 0}>
|
||||
{t('import')}
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="horizontal">
|
||||
<Form.Item name="templateName" label={t('templateName')}>
|
||||
<Select
|
||||
options={templates.map(t => ({ label: t.name, value: t.id }))}
|
||||
loading={loadingTemplates}
|
||||
onSelect={handleTemplateSelect}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Divider />
|
||||
<Typography.Text strong>
|
||||
{t('selectedTasks')} ({tasks.length})
|
||||
</Typography.Text>
|
||||
<List
|
||||
loading={loadingTasks}
|
||||
dataSource={tasks}
|
||||
bordered
|
||||
renderItem={(task, index) => (
|
||||
<List.Item
|
||||
key={task.id}
|
||||
actions={[
|
||||
<Button type="link" onClick={() => handleRemoveTask(index)}>
|
||||
{t('remove')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Typography.Text>{task.name}</Typography.Text>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportTaskTemplate;
|
||||
@@ -0,0 +1,179 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Drawer, Form, Input, List, Typography } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { taskTemplatesApiService } from '@/api/task-templates/task-templates.api.service';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { ITaskTemplateGetResponse } from '@/types/settings/task-templates.types';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { setSelectedTasks } from '@/features/project/project.slice';
|
||||
|
||||
interface TaskTemplateDrawerProps {
|
||||
showDrawer: boolean;
|
||||
selectedTemplateId: string | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TaskTemplateDrawer = ({
|
||||
showDrawer = false,
|
||||
selectedTemplateId,
|
||||
onClose,
|
||||
}: TaskTemplateDrawerProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation('task-template-drawer');
|
||||
const [form] = Form.useForm();
|
||||
const [templateData, setTemplateData] = useState<ITaskTemplateGetResponse>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [creatingTemplate, setCreatingTemplate] = useState(false);
|
||||
const [updatingTemplate, setUpdatingTemplate] = useState(false);
|
||||
|
||||
const { selectedTasks } = useAppSelector(state => state.bulkActionReducer);
|
||||
|
||||
const onCloseDrawer = () => {
|
||||
form.resetFields();
|
||||
setTemplateData({});
|
||||
onClose();
|
||||
};
|
||||
|
||||
const fetchTemplateData = async () => {
|
||||
if (!selectedTemplateId) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await taskTemplatesApiService.getTemplate(selectedTemplateId);
|
||||
if (res.done) {
|
||||
setTemplateData(res.body);
|
||||
form.setFieldsValue({
|
||||
name: res.body.name,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch template data:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const afterOpenChange = (open: boolean) => {
|
||||
if (selectedTemplateId) {
|
||||
fetchTemplateData();
|
||||
return;
|
||||
}
|
||||
setTemplateData({ tasks: selectedTasks });
|
||||
};
|
||||
|
||||
const handleRemoveTask = (index: number) => {
|
||||
const updatedTasks = [...(templateData.tasks || [])];
|
||||
updatedTasks.splice(index, 1);
|
||||
setTemplateData({
|
||||
...templateData,
|
||||
tasks: updatedTasks,
|
||||
});
|
||||
};
|
||||
|
||||
const createTemplate = async () => {
|
||||
const values = form.getFieldsValue();
|
||||
if (!values.name || !templateData.tasks) return;
|
||||
try {
|
||||
setCreatingTemplate(true);
|
||||
const res = await taskTemplatesApiService.createTemplate({
|
||||
name: values.name || '',
|
||||
tasks: templateData.tasks || [],
|
||||
});
|
||||
if (res.done) {
|
||||
onCloseDrawer();
|
||||
dispatch(setSelectedTasks([]));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to create template:', error);
|
||||
} finally {
|
||||
setCreatingTemplate(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateTemplate = async () => {
|
||||
if (!selectedTemplateId || !templateData.name || !templateData.tasks) return;
|
||||
const values = form.getFieldsValue();
|
||||
try {
|
||||
setUpdatingTemplate(true);
|
||||
const res = await taskTemplatesApiService.updateTemplate(selectedTemplateId, {
|
||||
name: values.name || '',
|
||||
tasks: templateData.tasks || [],
|
||||
});
|
||||
if (res.done) {
|
||||
onCloseDrawer();
|
||||
dispatch(setSelectedTasks([]));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to update template:', error);
|
||||
} finally {
|
||||
setUpdatingTemplate(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveTemplate = () => {
|
||||
if (!selectedTemplateId) {
|
||||
createTemplate();
|
||||
} else {
|
||||
updateTemplate();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width={650}
|
||||
title={selectedTemplateId ? t('editTaskTemplate') : t('createTaskTemplate')}
|
||||
open={showDrawer}
|
||||
onClose={onCloseDrawer}
|
||||
afterOpenChange={afterOpenChange}
|
||||
destroyOnClose={true}
|
||||
footer={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', justifyContent: 'right' }}>
|
||||
<Button onClick={onCloseDrawer}>{t('cancelButton')}</Button>
|
||||
<Button type="primary" onClick={handleSaveTemplate} loading={creatingTemplate || updatingTemplate}>
|
||||
{t('saveButton')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form form={form} initialValues={{ name: templateData?.name }}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('templateNameText')}
|
||||
rules={[{ required: true, message: t('templateNameRequired') }]}
|
||||
>
|
||||
<Input type="text" />
|
||||
</Form.Item>
|
||||
<Typography.Text style={{ fontWeight: 700 }}>
|
||||
{t('selectedTasks')} ({templateData?.tasks?.length || 0})
|
||||
</Typography.Text>
|
||||
<div style={{ marginTop: '1.5rem' }}>
|
||||
<List
|
||||
loading={isLoading}
|
||||
bordered
|
||||
dataSource={templateData?.tasks}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<span>{item.name}</span>
|
||||
<Button type="link" onClick={() => handleRemoveTask(index)}>
|
||||
{t('removeTask')}
|
||||
</Button>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskTemplateDrawer;
|
||||
Reference in New Issue
Block a user