mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat: Add unsaved changes tracking to PipelineFormComponent
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { GetPipelineResponseData, Pipeline } from '@/app/infra/entities/api';
|
import { GetPipelineResponseData, Pipeline } from '@/app/infra/entities/api';
|
||||||
import {
|
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(() => {
|
useEffect(() => {
|
||||||
// get config schema from metadata
|
// get config schema from metadata
|
||||||
httpClient.getGeneralPipelineMetadata().then((resp) => {
|
httpClient.getGeneralPipelineMetadata().then((resp) => {
|
||||||
@@ -139,7 +147,7 @@ export default function PipelineFormComponent({
|
|||||||
.getPipeline(pipelineId || '')
|
.getPipeline(pipelineId || '')
|
||||||
.then((resp: GetPipelineResponseData) => {
|
.then((resp: GetPipelineResponseData) => {
|
||||||
setIsDefaultPipeline(resp.pipeline.is_default ?? false);
|
setIsDefaultPipeline(resp.pipeline.is_default ?? false);
|
||||||
form.reset({
|
const loadedValues = {
|
||||||
basic: {
|
basic: {
|
||||||
name: resp.pipeline.name,
|
name: resp.pipeline.name,
|
||||||
description: resp.pipeline.description,
|
description: resp.pipeline.description,
|
||||||
@@ -149,7 +157,9 @@ export default function PipelineFormComponent({
|
|||||||
trigger: resp.pipeline.config.trigger,
|
trigger: resp.pipeline.config.trigger,
|
||||||
safety: resp.pipeline.config.safety,
|
safety: resp.pipeline.config.safety,
|
||||||
output: resp.pipeline.config.output,
|
output: resp.pipeline.config.output,
|
||||||
});
|
};
|
||||||
|
form.reset(loadedValues);
|
||||||
|
savedSnapshotRef.current = JSON.stringify(loadedValues);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -216,6 +226,7 @@ export default function PipelineFormComponent({
|
|||||||
httpClient
|
httpClient
|
||||||
.updatePipeline(pipelineId || '', pipeline)
|
.updatePipeline(pipelineId || '', pipeline)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
savedSnapshotRef.current = JSON.stringify(form.getValues());
|
||||||
onFinish();
|
onFinish();
|
||||||
toast.success(t('pipelines.saveSuccess'));
|
toast.success(t('pipelines.saveSuccess'));
|
||||||
})
|
})
|
||||||
@@ -509,7 +520,14 @@ export default function PipelineFormComponent({
|
|||||||
</form>
|
</form>
|
||||||
{/* 按钮栏移到 Tabs 外部,始终固定底部 */}
|
{/* 按钮栏移到 Tabs 外部,始终固定底部 */}
|
||||||
{showButtons && (
|
{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 && (
|
{isEditMode && !isDefaultPipeline && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -589,6 +589,7 @@ const enUS = {
|
|||||||
copyConfirmTitle: 'Confirm Copy',
|
copyConfirmTitle: 'Confirm Copy',
|
||||||
copyConfirmation:
|
copyConfirmation:
|
||||||
'Are you sure you want to copy this pipeline? This will create a new pipeline with all configurations.',
|
'Are you sure you want to copy this pipeline? This will create a new pipeline with all configurations.',
|
||||||
|
unsavedChanges: 'You have unsaved changes',
|
||||||
extensions: {
|
extensions: {
|
||||||
title: 'Extensions',
|
title: 'Extensions',
|
||||||
loadError: 'Failed to load plugins',
|
loadError: 'Failed to load plugins',
|
||||||
|
|||||||
@@ -591,6 +591,7 @@ const jaJP = {
|
|||||||
copyConfirmTitle: 'コピーの確認',
|
copyConfirmTitle: 'コピーの確認',
|
||||||
copyConfirmation:
|
copyConfirmation:
|
||||||
'このパイプラインをコピーしますか?すべての設定を含む新しいパイプラインが作成されます。',
|
'このパイプラインをコピーしますか?すべての設定を含む新しいパイプラインが作成されます。',
|
||||||
|
unsavedChanges: '未保存の変更があります',
|
||||||
extensions: {
|
extensions: {
|
||||||
title: 'プラグイン統合',
|
title: 'プラグイン統合',
|
||||||
loadError: 'プラグインリストの読み込みに失敗しました',
|
loadError: 'プラグインリストの読み込みに失敗しました',
|
||||||
|
|||||||
@@ -565,6 +565,7 @@ const zhHans = {
|
|||||||
copyConfirmTitle: '确认复制',
|
copyConfirmTitle: '确认复制',
|
||||||
copyConfirmation:
|
copyConfirmation:
|
||||||
'确定要复制这个流水线吗?复制将创建一个包含完整配置的新流水线。',
|
'确定要复制这个流水线吗?复制将创建一个包含完整配置的新流水线。',
|
||||||
|
unsavedChanges: '有未保存的更改',
|
||||||
extensions: {
|
extensions: {
|
||||||
title: '扩展集成',
|
title: '扩展集成',
|
||||||
loadError: '加载插件列表失败',
|
loadError: '加载插件列表失败',
|
||||||
|
|||||||
@@ -558,6 +558,7 @@ const zhHant = {
|
|||||||
copyConfirmTitle: '確認複製',
|
copyConfirmTitle: '確認複製',
|
||||||
copyConfirmation:
|
copyConfirmation:
|
||||||
'確定要複製這個流程線嗎?複製將創建一個包含完整配置的新流程線。',
|
'確定要複製這個流程線嗎?複製將創建一個包含完整配置的新流程線。',
|
||||||
|
unsavedChanges: '有未儲存的變更',
|
||||||
extensions: {
|
extensions: {
|
||||||
title: '擴展集成',
|
title: '擴展集成',
|
||||||
loadError: '載入插件清單失敗',
|
loadError: '載入插件清單失敗',
|
||||||
|
|||||||
Reference in New Issue
Block a user