feat: mover una actividad (con sus tareas) a otro proyecto
Menu de tres puntos en el listado de Actividades -> elegir proyecto destino -> confirmar
("Se movera la actividad... (junto con sus tareas)... Aceptar/Cancelar) antes de ejecutar.
Tareas, comentarios y adjuntos se mueven solos (referencian la actividad, no el proyecto). Las
dependencias existentes se eliminan al mover -- solo tienen sentido dentro del mismo proyecto
(addDependency ya lo exigia). Nuevo PUT /api/activities/:id/move, exige permiso de escritura en
ambos proyectos (origen y destino).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "project-linkedcorp-frontend",
|
||||
"private": true,
|
||||
"version": "1.16.4",
|
||||
"version": "1.17.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -76,6 +76,7 @@ export const messages = {
|
||||
common_edit: 'Editar',
|
||||
common_save: 'Guardar',
|
||||
common_cancel: 'Cancelar',
|
||||
common_accept: 'Aceptar',
|
||||
common_delete: 'Eliminar',
|
||||
common_close: 'Cerrar',
|
||||
common_search: 'Buscar...',
|
||||
@@ -199,6 +200,10 @@ export const messages = {
|
||||
activity_milestone: 'Es hito',
|
||||
activity_estimated_hours: 'Horas estimadas',
|
||||
activity_dependencies: 'Dependencias',
|
||||
activity_move: 'Mover a otro proyecto',
|
||||
activity_move_title: 'Mover actividad',
|
||||
activity_move_target: 'Proyecto destino',
|
||||
activity_move_confirm_msg: 'Se moverá la actividad "{activity}" (junto con sus tareas) al proyecto "{project}". ¿Deseas continuar?',
|
||||
activity_add_dependency: 'Agregar dependencia',
|
||||
activity_depends_on: 'Depende de',
|
||||
activity_no_dependency_options: 'No hay otras actividades en este proyecto para agregar como dependencia.',
|
||||
@@ -318,6 +323,7 @@ export const messages = {
|
||||
common_edit: 'Edit',
|
||||
common_save: 'Save',
|
||||
common_cancel: 'Cancel',
|
||||
common_accept: 'Accept',
|
||||
common_delete: 'Delete',
|
||||
common_close: 'Close',
|
||||
common_search: 'Search...',
|
||||
@@ -441,6 +447,10 @@ export const messages = {
|
||||
activity_milestone: 'Is milestone',
|
||||
activity_estimated_hours: 'Estimated hours',
|
||||
activity_dependencies: 'Dependencies',
|
||||
activity_move: 'Move to another project',
|
||||
activity_move_title: 'Move activity',
|
||||
activity_move_target: 'Target project',
|
||||
activity_move_confirm_msg: 'Activity "{activity}" (along with its tasks) will move to project "{project}". Continue?',
|
||||
activity_add_dependency: 'Add dependency',
|
||||
activity_depends_on: 'Depends on',
|
||||
activity_no_dependency_options: 'There are no other activities in this project to add as a dependency.',
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { Plus, Save, Trash2 } from 'lucide-react';
|
||||
import { Plus, Save, Trash2, ArrowRightLeft } from 'lucide-react';
|
||||
import { useUi } from '../../context/ui-context.jsx';
|
||||
import { useAuth } from '../../context/auth-context.jsx';
|
||||
import { useAsync, useAction } from '../../hooks/useAsync.js';
|
||||
import { listActivitiesByProject, createActivity, getActivity, deleteActivity, reorderActivities } from '../../services/activities.js';
|
||||
import { listActivitiesByProject, createActivity, getActivity, deleteActivity, reorderActivities, moveActivity } from '../../services/activities.js';
|
||||
import { listProjects } from '../../services/projects.js';
|
||||
import { listCatalog } from '../../services/catalogs.js';
|
||||
import { listUsers } from '../../services/users.js';
|
||||
import { ListView } from '../../components/ListView.jsx';
|
||||
@@ -26,15 +27,21 @@ export default function ProjectActivitiesTab({ projectId }) {
|
||||
const { data: priorities } = useAsync(() => listCatalog('priorities'), []);
|
||||
const { data: statuses } = useAsync(() => listCatalog('activity-statuses'), []);
|
||||
const { data: users } = useAsync(listUsers, []);
|
||||
const { data: allProjects } = useAsync(() => listProjects(), []);
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [form, setForm] = useState(EMPTY_FORM);
|
||||
const [selected, setSelected] = useState(null);
|
||||
const [moving, setMoving] = useState(null);
|
||||
const [targetProjectId, setTargetProjectId] = useState('');
|
||||
|
||||
const priorityOptions = (priorities || []).map((p) => ({ value: p.id, label: p.name }));
|
||||
const statusOptions = (statuses || []).map((s) => ({ value: s.id, label: s.name }));
|
||||
const userOptions = (users || []).map((u) => ({ value: u.id, label: u.name }));
|
||||
const projectOptions = (allProjects || [])
|
||||
.filter((p) => p.id !== Number(projectId))
|
||||
.map((p) => ({ value: p.id, label: `${p.code} — ${p.name}` }));
|
||||
|
||||
async function save() {
|
||||
if (!form.name.trim() || !form.priorityId || !form.statusId) return;
|
||||
@@ -62,6 +69,34 @@ export default function ProjectActivitiesTab({ projectId }) {
|
||||
await reload();
|
||||
}
|
||||
|
||||
function startMove(row) {
|
||||
setMoving(row);
|
||||
setTargetProjectId('');
|
||||
}
|
||||
|
||||
function confirmMove() {
|
||||
const target = (allProjects || []).find((p) => p.id === Number(targetProjectId));
|
||||
if (!target) return;
|
||||
const message = t('activity_move_confirm_msg').replace('{activity}', moving.name).replace('{project}', target.name);
|
||||
openDialog({
|
||||
tone: 'warning',
|
||||
title: t('activity_move_title'),
|
||||
message,
|
||||
actions: [
|
||||
{ label: t('common_cancel'), variant: 'standard' },
|
||||
{
|
||||
label: t('common_accept'), variant: 'accent',
|
||||
onClick: () => runAction(async () => {
|
||||
await moveActivity(moving.id, Number(targetProjectId));
|
||||
setMoving(null);
|
||||
await reload();
|
||||
pushToast({ tone: 'success', message: t('common_saved') });
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function confirmDelete(row) {
|
||||
openDialog({
|
||||
tone: 'warning',
|
||||
@@ -115,10 +150,25 @@ export default function ProjectActivitiesTab({ projectId }) {
|
||||
onRowClick={openActivity}
|
||||
onReorder={currentUser?.roleCode !== 'viewer' ? handleReorder : undefined}
|
||||
rowActions={currentUser?.roleCode !== 'viewer' ? (a) => [
|
||||
{ label: t('activity_move'), icon: ArrowRightLeft, onClick: () => startMove(a) },
|
||||
{ label: t('common_delete'), icon: Trash2, danger: true, onClick: () => confirmDelete(a) },
|
||||
] : undefined}
|
||||
/>
|
||||
|
||||
<DetailPopup open={!!moving} title={t('activity_move_title')} onClose={() => setMoving(null)} width={480} height={260}>
|
||||
{moving && (
|
||||
<>
|
||||
<Field label={t('activity_move_target')}>
|
||||
<ComboBox options={projectOptions} value={targetProjectId} onChange={setTargetProjectId} />
|
||||
</Field>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||||
<Button variant="accent" icon={ArrowRightLeft} onClick={confirmMove} disabled={!targetProjectId}>{t('activity_move')}</Button>
|
||||
<Button variant="standard" onClick={() => setMoving(null)}>{t('common_cancel')}</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DetailPopup>
|
||||
|
||||
<DetailPopup open={creating} title={t('activity_new')} onClose={() => setCreating(false)} width={640} height={560}>
|
||||
<Field label={t('common_name')}>
|
||||
<input className="input" value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} autoFocus />
|
||||
|
||||
@@ -5,6 +5,7 @@ export const createActivity = (projectId, data) => api.post(`/projects/${project
|
||||
export const getActivity = (id) => api.get(`/activities/${id}`);
|
||||
export const updateActivity = (id, data) => api.put(`/activities/${id}`, data);
|
||||
export const deleteActivity = (id) => api.delete(`/activities/${id}`);
|
||||
export const moveActivity = (id, projectId) => api.put(`/activities/${id}/move`, { projectId });
|
||||
export const reorderActivities = (projectId, ids) => api.put(`/projects/${projectId}/activities/reorder`, { ids });
|
||||
export const addDependency = (id, dependsOnActivityId) => api.post(`/activities/${id}/dependencies`, { dependsOnActivityId });
|
||||
export const removeDependency = (id, dependsOnId) => api.delete(`/activities/${id}/dependencies/${dependsOnId}`);
|
||||
|
||||
Reference in New Issue
Block a user