feat: Add unsaved changes tracking to PipelineFormComponent

This commit is contained in:
Junyan Qin
2026-02-23 14:36:04 +08:00
parent 42caae1bcf
commit 063dc6fe97
5 changed files with 26 additions and 4 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState, useMemo } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { GetPipelineResponseData, Pipeline } from '@/app/infra/entities/api';
import {
@@ -118,6 +118,14 @@ export default function PipelineFormComponent({
},
});
// Track unsaved changes by comparing current form values against a saved snapshot
const savedSnapshotRef = useRef<string>('');
const watchedValues = form.watch();
const hasUnsavedChanges = useMemo(() => {
if (!isEditMode || !savedSnapshotRef.current) return false;
return JSON.stringify(watchedValues) !== savedSnapshotRef.current;
}, [isEditMode, watchedValues]);
useEffect(() => {
// get config schema from metadata
httpClient.getGeneralPipelineMetadata().then((resp) => {
@@ -139,7 +147,7 @@ export default function PipelineFormComponent({
.getPipeline(pipelineId || '')
.then((resp: GetPipelineResponseData) => {
setIsDefaultPipeline(resp.pipeline.is_default ?? false);
form.reset({
const loadedValues = {
basic: {
name: resp.pipeline.name,
description: resp.pipeline.description,
@@ -149,7 +157,9 @@ export default function PipelineFormComponent({
trigger: resp.pipeline.config.trigger,
safety: resp.pipeline.config.safety,
output: resp.pipeline.config.output,
});
};
form.reset(loadedValues);
savedSnapshotRef.current = JSON.stringify(loadedValues);
});
}
}, []);
@@ -216,6 +226,7 @@ export default function PipelineFormComponent({
httpClient
.updatePipeline(pipelineId || '', pipeline)
.then(() => {
savedSnapshotRef.current = JSON.stringify(form.getValues());
onFinish();
toast.success(t('pipelines.saveSuccess'));
})
@@ -509,7 +520,14 @@ export default function PipelineFormComponent({
</form>
{/* 按钮栏移到 Tabs 外部,始终固定底部 */}
{showButtons && (
<div className="flex justify-end gap-2 pt-4 border-t mb-0 bg-white dark:bg-black sticky bottom-0 z-10">
<div className="flex justify-end items-center gap-2 pt-4 border-t mb-0 bg-white dark:bg-black sticky bottom-0 z-10">
{isEditMode && hasUnsavedChanges && (
<div className="text-amber-600 dark:text-amber-400 text-sm flex items-center gap-1.5 mr-auto">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-amber-500" />
{t('pipelines.unsavedChanges')}
</div>
)}
{isEditMode && !isDefaultPipeline && (
<Button
type="button"

View File

@@ -589,6 +589,7 @@ const enUS = {
copyConfirmTitle: 'Confirm Copy',
copyConfirmation:
'Are you sure you want to copy this pipeline? This will create a new pipeline with all configurations.',
unsavedChanges: 'You have unsaved changes',
extensions: {
title: 'Extensions',
loadError: 'Failed to load plugins',

View File

@@ -591,6 +591,7 @@ const jaJP = {
copyConfirmTitle: 'コピーの確認',
copyConfirmation:
'このパイプラインをコピーしますか?すべての設定を含む新しいパイプラインが作成されます。',
unsavedChanges: '未保存の変更があります',
extensions: {
title: 'プラグイン統合',
loadError: 'プラグインリストの読み込みに失敗しました',

View File

@@ -565,6 +565,7 @@ const zhHans = {
copyConfirmTitle: '确认复制',
copyConfirmation:
'确定要复制这个流水线吗?复制将创建一个包含完整配置的新流水线。',
unsavedChanges: '有未保存的更改',
extensions: {
title: '扩展集成',
loadError: '加载插件列表失败',

View File

@@ -558,6 +558,7 @@ const zhHant = {
copyConfirmTitle: '確認複製',
copyConfirmation:
'確定要複製這個流程線嗎?複製將創建一個包含完整配置的新流程線。',
unsavedChanges: '有未儲存的變更',
extensions: {
title: '擴展集成',
loadError: '載入插件清單失敗',