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,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 />
|
||||
|
||||
Reference in New Issue
Block a user