feat(JiraSync): enhance synchronization logic to preserve original Jira actions and detect changes
- Updated the Jira synchronization process to include original Jira actions for better detail retention. - Implemented a new function to detect real changes and preserved fields during task synchronization. - Enhanced the UI to display actions with preserved fields separately for improved clarity. - Added comprehensive tests for the new change detection logic to ensure accuracy and reliability.
This commit is contained in:
@@ -118,6 +118,17 @@ export async function POST(request: Request) {
|
|||||||
const syncResult = await jiraService.syncTasks(session.user.id);
|
const syncResult = await jiraService.syncTasks(session.user.id);
|
||||||
|
|
||||||
// Convertir SyncResult en JiraSyncResult pour le client
|
// Convertir SyncResult en JiraSyncResult pour le client
|
||||||
|
// Utiliser les actions Jira originales si disponibles pour préserver les détails (changes, etc.)
|
||||||
|
const actions =
|
||||||
|
syncResult.jiraActions ||
|
||||||
|
syncResult.actions.map((action) => ({
|
||||||
|
type: action.type as 'created' | 'updated' | 'skipped' | 'deleted',
|
||||||
|
taskKey: action.itemId.toString(),
|
||||||
|
taskTitle: action.title,
|
||||||
|
reason: action.message,
|
||||||
|
changes: action.message ? [action.message] : undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
const jiraSyncResult = {
|
const jiraSyncResult = {
|
||||||
success: syncResult.success,
|
success: syncResult.success,
|
||||||
tasksFound: syncResult.totalItems,
|
tasksFound: syncResult.totalItems,
|
||||||
@@ -127,13 +138,7 @@ export async function POST(request: Request) {
|
|||||||
tasksDeleted: syncResult.stats.deleted,
|
tasksDeleted: syncResult.stats.deleted,
|
||||||
errors: syncResult.errors,
|
errors: syncResult.errors,
|
||||||
unknownStatuses: syncResult.unknownStatuses || [], // Nouveaux statuts inconnus
|
unknownStatuses: syncResult.unknownStatuses || [], // Nouveaux statuts inconnus
|
||||||
actions: syncResult.actions.map((action) => ({
|
actions,
|
||||||
type: action.type as 'created' | 'updated' | 'skipped' | 'deleted',
|
|
||||||
taskKey: action.itemId.toString(),
|
|
||||||
taskTitle: action.title,
|
|
||||||
reason: action.message,
|
|
||||||
changes: action.message ? [action.message] : undefined,
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (syncResult.success) {
|
if (syncResult.success) {
|
||||||
|
|||||||
@@ -395,9 +395,157 @@ function SyncActionsList({ actions }: { actions: JiraSyncAction[] }) {
|
|||||||
{} as Record<string, JiraSyncAction[]>
|
{} as Record<string, JiraSyncAction[]>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Ordre d'affichage : créées, mises à jour, supprimées, ignorées (toujours en dernier)
|
||||||
|
const displayOrder: JiraSyncAction['type'][] = [
|
||||||
|
'created',
|
||||||
|
'updated',
|
||||||
|
'deleted',
|
||||||
|
'skipped',
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{Object.entries(groupedActions).map(([type, typeActions]) => (
|
{displayOrder
|
||||||
|
.filter((type) => groupedActions[type]?.length > 0)
|
||||||
|
.map((type) => {
|
||||||
|
const typeActions = groupedActions[type];
|
||||||
|
|
||||||
|
// Pour les actions skipped, séparer celles avec champs préservés de celles sans changement
|
||||||
|
if (type === 'skipped') {
|
||||||
|
const withPreservedFields = typeActions.filter(
|
||||||
|
(action) => action.changes && action.changes.length > 0
|
||||||
|
);
|
||||||
|
const withoutChanges = typeActions.filter(
|
||||||
|
(action) => !action.changes || action.changes.length === 0
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={type} className="space-y-4">
|
||||||
|
<h4
|
||||||
|
className={`font-bold text-sm flex items-center gap-2 ${getActionColor(type as JiraSyncAction['type'])}`}
|
||||||
|
>
|
||||||
|
{getActionIcon(type as JiraSyncAction['type'])}
|
||||||
|
{getActionLabel(type as JiraSyncAction['type'])} (
|
||||||
|
{typeActions.length})
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* Tâches avec champs préservés */}
|
||||||
|
{withPreservedFields.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-xs font-semibold text-[var(--muted-foreground)] uppercase tracking-wide flex items-center gap-2">
|
||||||
|
<div className="h-px flex-1 bg-[var(--border)]"></div>
|
||||||
|
<span className="text-orange-400">
|
||||||
|
Avec champs préservés ({withPreservedFields.length})
|
||||||
|
</span>
|
||||||
|
<div className="h-px flex-1 bg-[var(--border)]"></div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{withPreservedFields.map((action, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="p-2 bg-[var(--muted)]/10 rounded border border-orange-400/30"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="font-mono text-sm font-bold text-[var(--foreground)] shrink-0">
|
||||||
|
{action.taskKey}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-[var(--muted-foreground)] truncate">
|
||||||
|
{action.taskTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
{getActionLabel(action.type)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{action.reason && (
|
||||||
|
<div className="mt-1 text-xs text-[var(--muted-foreground)] italic">
|
||||||
|
<Emoji emoji="💡" /> {action.reason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{action.changes && action.changes.length > 0 && (
|
||||||
|
<div className="mt-1 space-y-0.5">
|
||||||
|
<div className="text-xs font-medium text-[var(--muted-foreground)]">
|
||||||
|
Champs préservés:
|
||||||
|
</div>
|
||||||
|
{action.changes.map((change, changeIndex) => (
|
||||||
|
<div
|
||||||
|
key={changeIndex}
|
||||||
|
className="text-xs font-mono text-[var(--foreground)] pl-2 border-l-2 border-orange-400/30"
|
||||||
|
>
|
||||||
|
{change}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tâches sans changement */}
|
||||||
|
{withoutChanges.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{withPreservedFields.length > 0 && (
|
||||||
|
<div className="text-xs font-semibold text-[var(--muted-foreground)] uppercase tracking-wide flex items-center gap-2">
|
||||||
|
<div className="h-px flex-1 bg-[var(--border)]"></div>
|
||||||
|
<span className="text-[var(--muted-foreground)]">
|
||||||
|
Aucun changement ({withoutChanges.length})
|
||||||
|
</span>
|
||||||
|
<div className="h-px flex-1 bg-[var(--border)]"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{withoutChanges.map((action, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="p-2 bg-[var(--muted)]/5 rounded border border-[var(--border)] opacity-75"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="font-mono text-sm font-bold text-[var(--muted-foreground)] shrink-0">
|
||||||
|
{action.taskKey}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-[var(--muted-foreground)] truncate">
|
||||||
|
{action.taskTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="shrink-0 opacity-75"
|
||||||
|
>
|
||||||
|
{getActionLabel(action.type)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{action.reason && (
|
||||||
|
<div className="mt-1 text-xs text-[var(--muted-foreground)] italic">
|
||||||
|
<Emoji emoji="💡" /> {action.reason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour les autres types, affichage normal
|
||||||
|
return (
|
||||||
<div key={type} className="space-y-3">
|
<div key={type} className="space-y-3">
|
||||||
<h4
|
<h4
|
||||||
className={`font-bold text-sm flex items-center gap-2 ${getActionColor(type as JiraSyncAction['type'])}`}
|
className={`font-bold text-sm flex items-center gap-2 ${getActionColor(type as JiraSyncAction['type'])}`}
|
||||||
@@ -454,7 +602,8 @@ function SyncActionsList({ actions }: { actions: JiraSyncAction[] }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,9 +383,157 @@ function SyncActionsList({ actions }: { actions: TfsSyncAction[] }) {
|
|||||||
{} as Record<string, TfsSyncAction[]>
|
{} as Record<string, TfsSyncAction[]>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Ordre d'affichage : créées, mises à jour, supprimées, ignorées (toujours en dernier)
|
||||||
|
const displayOrder: TfsSyncAction['type'][] = [
|
||||||
|
'created',
|
||||||
|
'updated',
|
||||||
|
'deleted',
|
||||||
|
'skipped',
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{Object.entries(groupedActions).map(([type, typeActions]) => (
|
{displayOrder
|
||||||
|
.filter((type) => groupedActions[type]?.length > 0)
|
||||||
|
.map((type) => {
|
||||||
|
const typeActions = groupedActions[type] as TfsSyncAction[];
|
||||||
|
|
||||||
|
// Pour les actions skipped, séparer celles avec champs préservés de celles sans changement
|
||||||
|
if (type === 'skipped') {
|
||||||
|
const withPreservedFields = typeActions.filter(
|
||||||
|
(action) => action.changes && action.changes.length > 0
|
||||||
|
);
|
||||||
|
const withoutChanges = typeActions.filter(
|
||||||
|
(action) => !action.changes || action.changes.length === 0
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={type} className="space-y-4">
|
||||||
|
<h4
|
||||||
|
className={`font-bold text-sm flex items-center gap-2 ${getActionColor(type as TfsSyncAction['type'])}`}
|
||||||
|
>
|
||||||
|
{getActionIcon(type as TfsSyncAction['type'])}
|
||||||
|
{getActionLabel(type as TfsSyncAction['type'])} (
|
||||||
|
{typeActions.length})
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* PRs avec champs préservés */}
|
||||||
|
{withPreservedFields.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-xs font-semibold text-[var(--muted-foreground)] uppercase tracking-wide flex items-center gap-2">
|
||||||
|
<div className="h-px flex-1 bg-[var(--border)]"></div>
|
||||||
|
<span className="text-orange-400">
|
||||||
|
Avec champs préservés ({withPreservedFields.length})
|
||||||
|
</span>
|
||||||
|
<div className="h-px flex-1 bg-[var(--border)]"></div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{withPreservedFields.map((action, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="p-2 bg-[var(--muted)]/10 rounded border border-orange-400/30"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="font-mono text-sm font-bold text-[var(--foreground)] shrink-0">
|
||||||
|
PR #{action.pullRequestId}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-[var(--muted-foreground)] truncate">
|
||||||
|
{action.prTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
{getActionLabel(action.type)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{action.reason && (
|
||||||
|
<div className="mt-1 text-xs text-[var(--muted-foreground)] italic">
|
||||||
|
💡 {action.reason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{action.changes && action.changes.length > 0 && (
|
||||||
|
<div className="mt-1 space-y-0.5">
|
||||||
|
<div className="text-xs font-medium text-[var(--muted-foreground)]">
|
||||||
|
Champs préservés:
|
||||||
|
</div>
|
||||||
|
{action.changes.map((change, changeIndex) => (
|
||||||
|
<div
|
||||||
|
key={changeIndex}
|
||||||
|
className="text-xs font-mono text-[var(--foreground)] pl-2 border-l-2 border-orange-400/30"
|
||||||
|
>
|
||||||
|
{change}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PRs sans changement */}
|
||||||
|
{withoutChanges.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{withPreservedFields.length > 0 && (
|
||||||
|
<div className="text-xs font-semibold text-[var(--muted-foreground)] uppercase tracking-wide flex items-center gap-2">
|
||||||
|
<div className="h-px flex-1 bg-[var(--border)]"></div>
|
||||||
|
<span className="text-[var(--muted-foreground)]">
|
||||||
|
Aucun changement ({withoutChanges.length})
|
||||||
|
</span>
|
||||||
|
<div className="h-px flex-1 bg-[var(--border)]"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{withoutChanges.map((action, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="p-2 bg-[var(--muted)]/5 rounded border border-[var(--border)] opacity-75"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="font-mono text-sm font-bold text-[var(--muted-foreground)] shrink-0">
|
||||||
|
PR #{action.pullRequestId}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-[var(--muted-foreground)] truncate">
|
||||||
|
{action.prTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="shrink-0 opacity-75"
|
||||||
|
>
|
||||||
|
{getActionLabel(action.type)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{action.reason && (
|
||||||
|
<div className="mt-1 text-xs text-[var(--muted-foreground)] italic">
|
||||||
|
💡 {action.reason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour les autres types, affichage normal
|
||||||
|
return (
|
||||||
<div key={type} className="space-y-3">
|
<div key={type} className="space-y-3">
|
||||||
<h4
|
<h4
|
||||||
className={`font-bold text-sm flex items-center gap-2 ${getActionColor(type as TfsSyncAction['type'])}`}
|
className={`font-bold text-sm flex items-center gap-2 ${getActionColor(type as TfsSyncAction['type'])}`}
|
||||||
@@ -442,7 +590,8 @@ function SyncActionsList({ actions }: { actions: TfsSyncAction[] }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,21 +383,168 @@ function SyncActionsList({ actions }: { actions: TfsSyncAction[] }) {
|
|||||||
{} as Record<string, TfsSyncAction[]>
|
{} as Record<string, TfsSyncAction[]>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Ordre d'affichage : créées, mises à jour, supprimées, ignorées (toujours en dernier)
|
||||||
|
const displayOrder: TfsSyncAction['type'][] = [
|
||||||
|
'created',
|
||||||
|
'updated',
|
||||||
|
'deleted',
|
||||||
|
'skipped',
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{Object.entries(groupedActions).map(([type, typeActions]) => (
|
{displayOrder
|
||||||
|
.filter((type) => groupedActions[type]?.length > 0)
|
||||||
|
.map((type) => {
|
||||||
|
const typeActions = groupedActions[type] as TfsSyncAction[];
|
||||||
|
|
||||||
|
// Pour les actions skipped, séparer celles avec champs préservés de celles sans changement
|
||||||
|
if (type === 'skipped') {
|
||||||
|
const withPreservedFields = typeActions.filter(
|
||||||
|
(action) => action.changes && action.changes.length > 0
|
||||||
|
);
|
||||||
|
const withoutChanges = typeActions.filter(
|
||||||
|
(action) => !action.changes || action.changes.length === 0
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={type} className="space-y-4">
|
||||||
|
<h4
|
||||||
|
className={`font-bold text-sm flex items-center gap-2 ${getActionColor(type as TfsSyncAction['type'])}`}
|
||||||
|
>
|
||||||
|
{getActionIcon(type as TfsSyncAction['type'])}
|
||||||
|
{getActionLabel(type as TfsSyncAction['type'])} (
|
||||||
|
{typeActions.length})
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* PRs avec champs préservés */}
|
||||||
|
{withPreservedFields.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-xs font-semibold text-[var(--muted-foreground)] uppercase tracking-wide flex items-center gap-2">
|
||||||
|
<div className="h-px flex-1 bg-[var(--border)]"></div>
|
||||||
|
<span className="text-orange-400">
|
||||||
|
Avec champs préservés ({withPreservedFields.length})
|
||||||
|
</span>
|
||||||
|
<div className="h-px flex-1 bg-[var(--border)]"></div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{withPreservedFields.map((action, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="p-2 bg-[var(--muted)]/10 rounded border border-orange-400/30"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="font-mono text-sm font-bold text-[var(--foreground)] shrink-0">
|
||||||
|
PR #{action.pullRequestId}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-[var(--muted-foreground)] truncate">
|
||||||
|
{action.prTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
{getActionLabel(action.type)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{action.reason && (
|
||||||
|
<div className="mt-1 text-xs text-[var(--muted-foreground)] italic">
|
||||||
|
💡 {action.reason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{action.changes && action.changes.length > 0 && (
|
||||||
|
<div className="mt-1 space-y-0.5">
|
||||||
|
<div className="text-xs font-medium text-[var(--muted-foreground)]">
|
||||||
|
Champs préservés:
|
||||||
|
</div>
|
||||||
|
{action.changes.map((change, changeIndex) => (
|
||||||
|
<div
|
||||||
|
key={changeIndex}
|
||||||
|
className="text-xs font-mono text-[var(--foreground)] pl-2 border-l-2 border-orange-400/30"
|
||||||
|
>
|
||||||
|
{change}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PRs sans changement */}
|
||||||
|
{withoutChanges.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{withPreservedFields.length > 0 && (
|
||||||
|
<div className="text-xs font-semibold text-[var(--muted-foreground)] uppercase tracking-wide flex items-center gap-2">
|
||||||
|
<div className="h-px flex-1 bg-[var(--border)]"></div>
|
||||||
|
<span className="text-[var(--muted-foreground)]">
|
||||||
|
Aucun changement ({withoutChanges.length})
|
||||||
|
</span>
|
||||||
|
<div className="h-px flex-1 bg-[var(--border)]"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{withoutChanges.map((action, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="p-2 bg-[var(--muted)]/5 rounded border border-[var(--border)] opacity-75"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="font-mono text-sm font-bold text-[var(--muted-foreground)] shrink-0">
|
||||||
|
PR #{action.pullRequestId}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-[var(--muted-foreground)] truncate">
|
||||||
|
{action.prTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="shrink-0 opacity-75"
|
||||||
|
>
|
||||||
|
{getActionLabel(action.type)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{action.reason && (
|
||||||
|
<div className="mt-1 text-xs text-[var(--muted-foreground)] italic">
|
||||||
|
💡 {action.reason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour les autres types, affichage normal
|
||||||
|
return (
|
||||||
<div key={type} className="space-y-3">
|
<div key={type} className="space-y-3">
|
||||||
<h4
|
<h4
|
||||||
className={`font-bold text-sm flex items-center gap-2 ${getActionColor(type as TfsSyncAction['type'])}`}
|
className={`font-bold text-sm flex items-center gap-2 ${getActionColor(type as TfsSyncAction['type'])}`}
|
||||||
>
|
>
|
||||||
{getActionIcon(type as TfsSyncAction['type'])}
|
{getActionIcon(type as TfsSyncAction['type'])}
|
||||||
{getActionLabel(type as TfsSyncAction['type'])} (
|
{getActionLabel(type as TfsSyncAction['type'])} (
|
||||||
{(typeActions as TfsSyncAction[]).length})
|
{typeActions.length})
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{(typeActions as TfsSyncAction[]).map(
|
{typeActions.map((action, index) => (
|
||||||
(action: TfsSyncAction, index: number) => (
|
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="p-2 bg-[var(--muted)]/10 rounded border border-[var(--border)]"
|
className="p-2 bg-[var(--muted)]/10 rounded border border-[var(--border)]"
|
||||||
@@ -429,24 +576,22 @@ function SyncActionsList({ actions }: { actions: TfsSyncAction[] }) {
|
|||||||
<div className="text-xs font-medium text-[var(--muted-foreground)]">
|
<div className="text-xs font-medium text-[var(--muted-foreground)]">
|
||||||
Modifications:
|
Modifications:
|
||||||
</div>
|
</div>
|
||||||
{action.changes.map(
|
{action.changes.map((change, changeIndex) => (
|
||||||
(change: string, changeIndex: number) => (
|
|
||||||
<div
|
<div
|
||||||
key={changeIndex}
|
key={changeIndex}
|
||||||
className="text-xs font-mono text-[var(--foreground)] pl-2 border-l-2 border-purple-400/30"
|
className="text-xs font-mono text-[var(--foreground)] pl-2 border-l-2 border-purple-400/30"
|
||||||
>
|
>
|
||||||
{change}
|
{change}
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { buildSyncUpdateData } from '@/services/task-management/readonly-fields';
|
import { buildSyncUpdateData } from '@/services/task-management/readonly-fields';
|
||||||
|
import { detectSyncChanges } from '@/services/integrations/jira/jira';
|
||||||
|
|
||||||
describe('Synchronisation Jira - Logique de préservation des champs', () => {
|
describe('Synchronisation Jira - Logique de préservation des champs', () => {
|
||||||
describe('buildSyncUpdateData pour Jira', () => {
|
describe('buildSyncUpdateData pour Jira', () => {
|
||||||
@@ -168,4 +169,263 @@ describe('Synchronisation Jira - Logique de préservation des champs', () => {
|
|||||||
expect(result.priority).toBe('medium'); // Préservé car modifié localement
|
expect(result.priority).toBe('medium'); // Préservé car modifié localement
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('detectSyncChanges - Détection des changements et préservations', () => {
|
||||||
|
it('devrait détecter une priorité préservée localement', () => {
|
||||||
|
const existingTask = {
|
||||||
|
title: 'Tâche test',
|
||||||
|
description: 'Description locale',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'high', // Modifié localement
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'TEST',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskData = {
|
||||||
|
title: 'Tâche test',
|
||||||
|
description: 'Description locale',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium', // Valeur Jira différente
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'TEST',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalData = buildSyncUpdateData('jira', existingTask, taskData);
|
||||||
|
|
||||||
|
const { realChanges, preservedFields } = detectSyncChanges(
|
||||||
|
existingTask,
|
||||||
|
taskData,
|
||||||
|
finalData
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(realChanges).toHaveLength(0);
|
||||||
|
expect(preservedFields).toHaveLength(1);
|
||||||
|
expect(preservedFields[0]).toBe(
|
||||||
|
'Priorité: préservée localement (high) vs Jira (medium)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('devrait détecter un titre préservé localement', () => {
|
||||||
|
const existingTask = {
|
||||||
|
title: 'Titre modifié localement',
|
||||||
|
description: 'Description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium',
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'TEST',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskData = {
|
||||||
|
title: 'Titre original Jira',
|
||||||
|
description: 'Description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium',
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'TEST',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalData = buildSyncUpdateData('jira', existingTask, taskData);
|
||||||
|
|
||||||
|
const { realChanges, preservedFields } = detectSyncChanges(
|
||||||
|
existingTask,
|
||||||
|
taskData,
|
||||||
|
finalData
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(realChanges).toHaveLength(0);
|
||||||
|
expect(preservedFields).toHaveLength(1);
|
||||||
|
expect(preservedFields[0]).toContain('Titre: préservé localement');
|
||||||
|
expect(preservedFields[0]).toContain('vs Jira');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('devrait détecter plusieurs champs préservés', () => {
|
||||||
|
const existingTask = {
|
||||||
|
title: 'Titre modifié',
|
||||||
|
description: 'Description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'high', // Modifié localement
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'TEST',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskData = {
|
||||||
|
title: 'Titre original', // Différent
|
||||||
|
description: 'Description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium', // Différent
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'TEST',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalData = buildSyncUpdateData('jira', existingTask, taskData);
|
||||||
|
|
||||||
|
const { realChanges, preservedFields } = detectSyncChanges(
|
||||||
|
existingTask,
|
||||||
|
taskData,
|
||||||
|
finalData
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(realChanges).toHaveLength(0);
|
||||||
|
expect(preservedFields.length).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(preservedFields.some((f) => f.includes('Titre'))).toBe(true);
|
||||||
|
expect(preservedFields.some((f) => f.includes('Priorité'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('devrait détecter un changement réel de statut', () => {
|
||||||
|
const existingTask = {
|
||||||
|
title: 'Tâche',
|
||||||
|
description: 'Description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium',
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'TEST',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskData = {
|
||||||
|
title: 'Tâche',
|
||||||
|
description: 'Description',
|
||||||
|
status: 'in_progress', // Changé dans Jira
|
||||||
|
priority: 'medium',
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'TEST',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalData = buildSyncUpdateData('jira', existingTask, taskData);
|
||||||
|
|
||||||
|
const { realChanges, preservedFields } = detectSyncChanges(
|
||||||
|
existingTask,
|
||||||
|
taskData,
|
||||||
|
finalData
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(realChanges.length).toBeGreaterThan(0);
|
||||||
|
expect(realChanges.some((c) => c.includes('Statut'))).toBe(true);
|
||||||
|
expect(preservedFields).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('devrait détecter changement réel + préservation simultanés', () => {
|
||||||
|
const existingTask = {
|
||||||
|
title: 'Titre modifié localement',
|
||||||
|
description: 'Description locale',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'high', // Modifié localement
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'TEST',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskData = {
|
||||||
|
title: 'Titre original', // Différent
|
||||||
|
description: 'Description Jira', // Différent
|
||||||
|
status: 'in_progress', // Changé dans Jira
|
||||||
|
priority: 'medium', // Différent
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'TEST',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalData = buildSyncUpdateData('jira', existingTask, taskData);
|
||||||
|
|
||||||
|
const { realChanges, preservedFields } = detectSyncChanges(
|
||||||
|
existingTask,
|
||||||
|
taskData,
|
||||||
|
finalData
|
||||||
|
);
|
||||||
|
|
||||||
|
// Description et statut devraient être dans realChanges (écrasés)
|
||||||
|
expect(realChanges.length).toBeGreaterThan(0);
|
||||||
|
// Titre et priorité devraient être préservés
|
||||||
|
expect(preservedFields.length).toBeGreaterThan(0);
|
||||||
|
expect(preservedFields.some((f) => f.includes('Titre'))).toBe(true);
|
||||||
|
expect(preservedFields.some((f) => f.includes('Priorité'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('devrait détecter aucun changement quand tout est identique', () => {
|
||||||
|
const existingTask = {
|
||||||
|
title: 'Tâche',
|
||||||
|
description: 'Description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium',
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'TEST',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskData = {
|
||||||
|
title: 'Tâche',
|
||||||
|
description: 'Description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium',
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'TEST',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalData = buildSyncUpdateData('jira', existingTask, taskData);
|
||||||
|
|
||||||
|
const { realChanges, preservedFields } = detectSyncChanges(
|
||||||
|
existingTask,
|
||||||
|
taskData,
|
||||||
|
finalData
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(realChanges).toHaveLength(0);
|
||||||
|
expect(preservedFields).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('devrait détecter un changement de projet Jira', () => {
|
||||||
|
const existingTask = {
|
||||||
|
title: 'Tâche',
|
||||||
|
description: 'Description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium',
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'OLD',
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskData = {
|
||||||
|
title: 'Tâche',
|
||||||
|
description: 'Description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium',
|
||||||
|
dueDate: null,
|
||||||
|
jiraProject: 'NEW', // Changé
|
||||||
|
jiraType: 'Task',
|
||||||
|
assignee: 'User',
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalData = buildSyncUpdateData('jira', existingTask, taskData);
|
||||||
|
|
||||||
|
const { realChanges, preservedFields } = detectSyncChanges(
|
||||||
|
existingTask,
|
||||||
|
taskData,
|
||||||
|
finalData
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(realChanges.length).toBeGreaterThan(0);
|
||||||
|
expect(realChanges.some((c) => c.includes('Projet'))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export interface SyncResult {
|
|||||||
skipped: number;
|
skipped: number;
|
||||||
deleted: number;
|
deleted: number;
|
||||||
};
|
};
|
||||||
|
// Actions originales Jira avec tous les détails (changes, etc.)
|
||||||
|
jiraActions?: JiraSyncAction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JiraSyncResult {
|
export interface JiraSyncResult {
|
||||||
@@ -60,6 +62,126 @@ export interface JiraSyncResult {
|
|||||||
actions: JiraSyncAction[]; // Détail des actions effectuées
|
actions: JiraSyncAction[]; // Détail des actions effectuées
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface pour les données de tâche existante
|
||||||
|
*/
|
||||||
|
interface ExistingTaskData {
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
dueDate: Date | null;
|
||||||
|
jiraProject: string | null;
|
||||||
|
jiraType: string | null;
|
||||||
|
assignee: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détecte les changements réels et les préservations lors de la synchronisation
|
||||||
|
* @param existingTask - Données existantes de la tâche
|
||||||
|
* @param taskData - Données venant de Jira (avec champs Jira spécifiques)
|
||||||
|
* @param finalData - Données finales après application de la logique de préservation (sans champs Jira)
|
||||||
|
* @returns Objet contenant les changements réels et les champs préservés
|
||||||
|
*/
|
||||||
|
export function detectSyncChanges(
|
||||||
|
existingTask: ExistingTaskData,
|
||||||
|
taskData: {
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
dueDate: Date | null;
|
||||||
|
jiraProject: string;
|
||||||
|
jiraType: string;
|
||||||
|
assignee: string | null;
|
||||||
|
},
|
||||||
|
finalData: {
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
dueDate: Date | null;
|
||||||
|
}
|
||||||
|
): { realChanges: string[]; preservedFields: string[] } {
|
||||||
|
const realChanges: string[] = [];
|
||||||
|
const preservedFields: string[] = [];
|
||||||
|
|
||||||
|
// Détecter les changements réels et les préservations
|
||||||
|
// Pour chaque champ, comparer existingTask avec taskData pour voir les différences
|
||||||
|
if (existingTask.title !== taskData.title) {
|
||||||
|
if (existingTask.title !== finalData.title) {
|
||||||
|
// Changement réel appliqué
|
||||||
|
realChanges.push(`Titre: ${existingTask.title} → ${finalData.title}`);
|
||||||
|
} else {
|
||||||
|
// Valeur préservée (finalData === existingTask mais différent de taskData)
|
||||||
|
preservedFields.push(
|
||||||
|
`Titre: préservé localement ("${existingTask.title}") vs Jira ("${taskData.title}")`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingTask.description !== taskData.description) {
|
||||||
|
if (existingTask.description !== finalData.description) {
|
||||||
|
realChanges.push(`Description modifiée`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingTask.status !== taskData.status) {
|
||||||
|
if (existingTask.status !== finalData.status) {
|
||||||
|
realChanges.push(`Statut: ${existingTask.status} → ${finalData.status}`);
|
||||||
|
} else {
|
||||||
|
// Valeur préservée (finalData === existingTask mais différent de taskData)
|
||||||
|
preservedFields.push(
|
||||||
|
`Statut: préservé localement (${existingTask.status}) vs Jira (${taskData.status})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingTask.priority !== taskData.priority) {
|
||||||
|
if (existingTask.priority !== finalData.priority) {
|
||||||
|
realChanges.push(
|
||||||
|
`Priorité: ${existingTask.priority} → ${finalData.priority}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Valeur préservée (finalData === existingTask mais différent de taskData)
|
||||||
|
preservedFields.push(
|
||||||
|
`Priorité: préservée localement (${existingTask.priority}) vs Jira (${taskData.priority})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(existingTask.dueDate?.getTime() || null) !==
|
||||||
|
(finalData.dueDate?.getTime() || null)
|
||||||
|
) {
|
||||||
|
const oldDate = existingTask.dueDate
|
||||||
|
? formatDateForDisplay(existingTask.dueDate)
|
||||||
|
: 'Aucune';
|
||||||
|
const newDate = finalData.dueDate
|
||||||
|
? formatDateForDisplay(finalData.dueDate)
|
||||||
|
: 'Aucune';
|
||||||
|
realChanges.push(`Échéance: ${oldDate} → ${newDate}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingTask.jiraProject !== taskData.jiraProject) {
|
||||||
|
realChanges.push(
|
||||||
|
`Projet: ${existingTask.jiraProject} → ${taskData.jiraProject}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingTask.jiraType !== taskData.jiraType) {
|
||||||
|
realChanges.push(`Type: ${existingTask.jiraType} → ${taskData.jiraType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingTask.assignee !== taskData.assignee) {
|
||||||
|
realChanges.push(
|
||||||
|
`Assigné: ${existingTask.assignee} → ${taskData.assignee}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { realChanges, preservedFields };
|
||||||
|
}
|
||||||
|
|
||||||
export class JiraService {
|
export class JiraService {
|
||||||
readonly config: JiraConfig;
|
readonly config: JiraConfig;
|
||||||
|
|
||||||
@@ -437,6 +559,8 @@ export class JiraService {
|
|||||||
|
|
||||||
// Déterminer le succès et enregistrer le log
|
// Déterminer le succès et enregistrer le log
|
||||||
result.success = result.errors.length === 0;
|
result.success = result.errors.length === 0;
|
||||||
|
// Ajouter les actions Jira originales pour préserver les détails (changes, etc.)
|
||||||
|
result.jiraActions = jiraActions;
|
||||||
await this.logSync(result);
|
await this.logSync(result);
|
||||||
|
|
||||||
console.log('✅ Synchronisation Jira terminée:', result);
|
console.log('✅ Synchronisation Jira terminée:', result);
|
||||||
@@ -531,65 +655,15 @@ export class JiraService {
|
|||||||
// Utiliser la logique centralisée pour déterminer quels champs préserver
|
// Utiliser la logique centralisée pour déterminer quels champs préserver
|
||||||
const finalData = buildSyncUpdateData('jira', existingTask, taskData);
|
const finalData = buildSyncUpdateData('jira', existingTask, taskData);
|
||||||
|
|
||||||
// Détecter les changements et créer la liste des modifications
|
// Détecter les changements réels et préservations
|
||||||
const changes: string[] = [];
|
const { realChanges, preservedFields } = detectSyncChanges(
|
||||||
|
existingTask,
|
||||||
// Détecter les changements en comparant les valeurs finales avec les existantes
|
taskData,
|
||||||
if (existingTask.title !== finalData.title) {
|
finalData
|
||||||
changes.push(`Titre: ${existingTask.title} → ${finalData.title}`);
|
|
||||||
} else if (existingTask.title !== taskData.title) {
|
|
||||||
// Valeur préservée (finalData === existingTask mais différent de taskData)
|
|
||||||
changes.push(`Titre: préservé localement ("${existingTask.title}")`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingTask.description !== finalData.description) {
|
|
||||||
changes.push(`Description modifiée`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingTask.status !== finalData.status) {
|
|
||||||
changes.push(`Statut: ${existingTask.status} → ${finalData.status}`);
|
|
||||||
} else if (existingTask.status !== taskData.status) {
|
|
||||||
// Valeur préservée (finalData === existingTask mais différent de taskData)
|
|
||||||
changes.push(`Statut: préservé localement (${existingTask.status})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingTask.priority !== finalData.priority) {
|
|
||||||
changes.push(
|
|
||||||
`Priorité: ${existingTask.priority} → ${finalData.priority}`
|
|
||||||
);
|
);
|
||||||
} else if (existingTask.priority !== taskData.priority) {
|
|
||||||
// Valeur préservée (finalData === existingTask mais différent de taskData)
|
|
||||||
changes.push(
|
|
||||||
`Priorité: préservée localement (${existingTask.priority})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
(existingTask.dueDate?.getTime() || null) !==
|
|
||||||
(finalData.dueDate?.getTime() || null)
|
|
||||||
) {
|
|
||||||
const oldDate = existingTask.dueDate
|
|
||||||
? formatDateForDisplay(existingTask.dueDate)
|
|
||||||
: 'Aucune';
|
|
||||||
const newDate = finalData.dueDate
|
|
||||||
? formatDateForDisplay(finalData.dueDate)
|
|
||||||
: 'Aucune';
|
|
||||||
changes.push(`Échéance: ${oldDate} → ${newDate}`);
|
|
||||||
}
|
|
||||||
if (existingTask.jiraProject !== taskData.jiraProject) {
|
|
||||||
changes.push(
|
|
||||||
`Projet: ${existingTask.jiraProject} → ${taskData.jiraProject}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (existingTask.jiraType !== taskData.jiraType) {
|
|
||||||
changes.push(`Type: ${existingTask.jiraType} → ${taskData.jiraType}`);
|
|
||||||
}
|
|
||||||
if (existingTask.assignee !== taskData.assignee) {
|
|
||||||
changes.push(
|
|
||||||
`Assigné: ${existingTask.assignee} → ${taskData.assignee}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changes.length === 0) {
|
// Si tous les champs sont préservés et aucun changement réel, skip
|
||||||
|
if (realChanges.length === 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`⏭️ Aucun changement pour ${jiraTask.key}, skip mise à jour`
|
`⏭️ Aucun changement pour ${jiraTask.key}, skip mise à jour`
|
||||||
);
|
);
|
||||||
@@ -601,7 +675,8 @@ export class JiraService {
|
|||||||
type: 'skipped',
|
type: 'skipped',
|
||||||
taskKey: jiraTask.key,
|
taskKey: jiraTask.key,
|
||||||
taskTitle: jiraTask.summary,
|
taskTitle: jiraTask.summary,
|
||||||
reason: 'Aucun changement détecté',
|
reason: 'Tous les champs préservés localement',
|
||||||
|
changes: preservedFields.length > 0 ? preservedFields : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,14 +717,17 @@ export class JiraService {
|
|||||||
// S'assurer que le tag Jira est assigné (pour les anciennes tâches)
|
// S'assurer que le tag Jira est assigné (pour les anciennes tâches)
|
||||||
await this.assignJiraTag(existingTask.id, userId);
|
await this.assignJiraTag(existingTask.id, userId);
|
||||||
|
|
||||||
|
// Construire la liste complète des changements (réels + préservations pour info)
|
||||||
|
const allChanges = [...realChanges, ...preservedFields];
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`🔄 Tâche mise à jour (titre/priorité préservés): ${jiraTask.key} (${changes.length} changement${changes.length > 1 ? 's' : ''})`
|
`🔄 Tâche mise à jour: ${jiraTask.key} (${realChanges.length} changement${realChanges.length > 1 ? 's' : ''} réel${realChanges.length > 1 ? 's' : ''}${preservedFields.length > 0 ? `, ${preservedFields.length} préservé${preservedFields.length > 1 ? 's' : ''}` : ''})`
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
type: 'updated',
|
type: 'updated',
|
||||||
taskKey: jiraTask.key,
|
taskKey: jiraTask.key,
|
||||||
taskTitle: jiraTask.summary,
|
taskTitle: jiraTask.summary,
|
||||||
changes,
|
changes: allChanges,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user