diff --git a/Src/Witsml/ServiceReference/OptionsIn.cs b/Src/Witsml/ServiceReference/OptionsIn.cs index 0e05926ad..fde082912 100644 --- a/Src/Witsml/ServiceReference/OptionsIn.cs +++ b/Src/Witsml/ServiceReference/OptionsIn.cs @@ -10,6 +10,7 @@ public record OptionsIn( int? MaxReturnNodes = null, int? RequestLatestValues = null, bool? RequestObjectSelectionCapability = null, + bool? CascadedDelete = null, string OptionsInString = null) { public string OptionsInString { get; init; } = ValidateOptionsInString(OptionsInString); @@ -34,6 +35,10 @@ public string GetKeywords() { keywords.Add($"requestObjectSelectionCapability=true"); } + if (CascadedDelete == true) + { + keywords.Add($"cascadedDelete=true"); + } if (!string.IsNullOrEmpty(OptionsInString)) { keywords.Add(OptionsInString); diff --git a/Src/Witsml/WitsmlClient.cs b/Src/Witsml/WitsmlClient.cs index b02852bff..94b355894 100644 --- a/Src/Witsml/WitsmlClient.cs +++ b/Src/Witsml/WitsmlClient.cs @@ -26,6 +26,7 @@ public interface IWitsmlClient Task UpdateInStoreAsync(T query) where T : IWitsmlQueryType; Task UpdateInStoreAsync(string query, OptionsIn optionsIn = null); Task DeleteFromStoreAsync(T query) where T : IWitsmlQueryType; + Task DeleteFromStoreAsync(T query, OptionsIn optionsIn) where T : IWitsmlQueryType; Task DeleteFromStoreAsync(string query, OptionsIn optionsIn = null); Task TestConnectionAsync(); Task GetCap(); @@ -367,7 +368,17 @@ public async Task UpdateInStoreAsync(string query, OptionsIn optionsIn = throw new Exception($"Error while adding to store: {response.Result} - {errorResponse.Result}. {response.SuppMsgOut}"); } - public async Task DeleteFromStoreAsync(T query) where T : IWitsmlQueryType + public Task DeleteFromStoreAsync(T query) where T : IWitsmlQueryType + { + return DeleteFromStoreAsyncImplementation(query); + } + + public Task DeleteFromStoreAsync(T query, OptionsIn optionsIn) where T : IWitsmlQueryType + { + return DeleteFromStoreAsyncImplementation(query, optionsIn); + } + + private async Task DeleteFromStoreAsyncImplementation(T query, OptionsIn optionsIn = null) where T : IWitsmlQueryType { try { @@ -375,7 +386,7 @@ public async Task DeleteFromStoreAsync(T query) where T : IWitsm { WMLtypeIn = query.TypeName, QueryIn = XmlHelper.Serialize(query), - OptionsIn = string.Empty, + OptionsIn = optionsIn == null ? string.Empty : optionsIn.GetKeywords(), CapabilitiesIn = _clientCapabilities }; diff --git a/Src/WitsmlExplorer.Api/Jobs/DeleteJobs.cs b/Src/WitsmlExplorer.Api/Jobs/DeleteJobs.cs index e3eaf7ec0..9bba2a383 100644 --- a/Src/WitsmlExplorer.Api/Jobs/DeleteJobs.cs +++ b/Src/WitsmlExplorer.Api/Jobs/DeleteJobs.cs @@ -4,6 +4,12 @@ namespace WitsmlExplorer.Api.Jobs { public record DeleteComponentsJob : IDeleteJob { } public record DeleteObjectsJob : IDeleteJob { } - public record DeleteWellboreJob : IDeleteJob { } - public record DeleteWellJob : IDeleteJob { } + public record DeleteWellboreJob : IDeleteJob + { + public bool CascadedDelete { get; init; } + } + public record DeleteWellJob : IDeleteJob + { + public bool CascadedDelete { get; init; } + } } diff --git a/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellWorker.cs b/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellWorker.cs index 35231ab25..efa495271 100644 --- a/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellWorker.cs @@ -23,10 +23,11 @@ public DeleteWellWorker(ILogger logger, IWitsmlClientProvider wit public override async Task<(WorkerResult, RefreshAction)> Execute(DeleteWellJob job, CancellationToken? cancellationToken = null) { + bool cascadedDelete = job.CascadedDelete; string wellUid = job.ToDelete.WellUid; WitsmlWells witsmlWell = WellQueries.DeleteWitsmlWell(wellUid); - QueryResult result = await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(witsmlWell); + QueryResult result = cascadedDelete ? await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(witsmlWell, new OptionsIn(CascadedDelete: true)) : await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(witsmlWell); if (result.IsSuccessful) { Logger.LogInformation("Deleted well. WellUid: {WellUid}", wellUid); diff --git a/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellboreWorker.cs b/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellboreWorker.cs index 18348a6d8..665b5d726 100644 --- a/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellboreWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/Delete/DeleteWellboreWorker.cs @@ -23,11 +23,12 @@ public DeleteWellboreWorker(ILogger logger, IWitsmlClientProv public override async Task<(WorkerResult, RefreshAction)> Execute(DeleteWellboreJob job, CancellationToken? cancellationToken = null) { + bool cascadedDelete = job.CascadedDelete; string wellUid = job.ToDelete.WellUid; string wellboreUid = job.ToDelete.WellboreUid; WitsmlWellbores witsmlWellbore = WellboreQueries.DeleteWitsmlWellbore(wellUid, wellboreUid); - QueryResult result = await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(witsmlWellbore); + QueryResult result = cascadedDelete ? await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(witsmlWellbore, new OptionsIn(CascadedDelete: true)) : await GetTargetWitsmlClientOrThrow().DeleteFromStoreAsync(witsmlWellbore); if (result.IsSuccessful) { Logger.LogInformation("Deleted wellbore. WellUid: {WellUid}, WellboreUid: {WellboreUid}", diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx index 724a97f6b..f6f723f55 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/WellContextMenu.tsx @@ -9,7 +9,9 @@ import { WellRow } from "components/ContentViews/WellsListView"; import ContextMenu from "components/ContextMenus/ContextMenu"; import { StyledIcon } from "components/ContextMenus/ContextMenuUtils"; import NestedMenuItem from "components/ContextMenus/NestedMenuItem"; -import ConfirmModal from "components/Modals/ConfirmModal"; +import ConfirmDeletionModal, { + ConfirmDeletionModalProps +} from "components/Modals/ConfirmDeletionModal"; import DeleteEmptyMnemonicsModal, { DeleteEmptyMnemonicsModalProps } from "components/Modals/DeleteEmptyMnemonicsModal"; @@ -104,37 +106,31 @@ const WellContextMenu = (props: WellContextMenuProps): React.ReactElement => { ); }; - const deleteWell = async () => { + const deleteWell = async (cascadedDelete: boolean) => { dispatchOperation({ type: OperationType.HideContextMenu }); dispatchOperation({ type: OperationType.HideModal }); const job: DeleteWellJob = { toDelete: { wellUid: well.uid, wellName: well.name - } + }, + cascadedDelete }; await JobService.orderJob(JobType.DeleteWell, job); }; const onClickDelete = async () => { - const confirmation = ( - - This will permanently delete {well.name} with uid:{" "} - {well.uid} - - } - onConfirm={deleteWell} - confirmColor={"danger"} - confirmText={"Delete well"} - switchButtonPlaces={true} - /> - ); + const userCredentialsModalProps: ConfirmDeletionModalProps = { + componentType: "well", + objectName: well.name, + objectUid: well.uid, + onSubmit(cascadedDelete) { + deleteWell(cascadedDelete); + } + }; dispatchOperation({ type: OperationType.DisplayModal, - payload: confirmation + payload: }); }; @@ -219,7 +215,11 @@ const WellContextMenu = (props: WellContextMenuProps): React.ReactElement => { New Wellbore , - + { + const deleteWellbore = async (cascadedDelete: boolean) => { dispatchOperation({ type: OperationType.HideContextMenu }); dispatchOperation({ type: OperationType.HideModal }); const job: DeleteWellboreJob = { @@ -116,30 +118,24 @@ const WellboreContextMenu = ( wellboreUid: wellbore.uid, wellName: wellbore.wellName, wellboreName: wellbore.name - } + }, + cascadedDelete }; await JobService.orderJob(JobType.DeleteWellbore, job); }; const onClickDelete = async () => { - const confirmation = ( - - This will permanently delete {wellbore.name} with - uid: {wellbore.uid} - - } - onConfirm={deleteWellbore} - confirmColor={"danger"} - confirmText={"Delete wellbore"} - switchButtonPlaces={true} - /> - ); + const userCredentialsModalProps: ConfirmDeletionModalProps = { + componentType: "wellbore", + objectName: wellbore.name, + objectUid: wellbore.uid, + onSubmit(cascadedDelete) { + deleteWellbore(cascadedDelete); + } + }; dispatchOperation({ type: OperationType.DisplayModal, - payload: confirmation + payload: }); }; @@ -237,7 +233,11 @@ const WellboreContextMenu = ( )} , - + void; +} + +const ConfirmDeletionModal = ( + props: ConfirmDeletionModalProps +): React.ReactElement => { + const { + operationState: { colors }, + dispatchOperation + } = useOperationState(); + + const [cascadedDelete, setCascadedDelete] = useState(false); + + const onConfirmClick = async () => { + props.onSubmit(cascadedDelete); + dispatchOperation({ type: OperationType.HideModal }); + }; + + return ( + + + + This will permanently delete {props.componentType}{" "} + {props.objectName} with uid:{" "} + {props.objectUid} + + + ) => { + setCascadedDelete(e.target.checked); + }} + colors={colors} + /> + + {cascadedDelete && ( + + )} + + + } + onConfirm={onConfirmClick} + confirmColor={"danger"} + confirmText={"Delete " + props.componentType} + switchButtonPlaces={true} + /> + ); +}; + +export default ConfirmDeletionModal; diff --git a/Src/WitsmlExplorer.Frontend/models/jobs/deleteJobs.ts b/Src/WitsmlExplorer.Frontend/models/jobs/deleteJobs.ts index 01679aefa..ce78f7d17 100644 --- a/Src/WitsmlExplorer.Frontend/models/jobs/deleteJobs.ts +++ b/Src/WitsmlExplorer.Frontend/models/jobs/deleteJobs.ts @@ -12,6 +12,7 @@ export interface DeleteComponentsJob { export interface DeleteWellboreJob { toDelete: WellboreReference; + cascadedDelete: boolean; } export interface DeleteWellJob { @@ -19,4 +20,5 @@ export interface DeleteWellJob { wellUid: string; wellName: string; }; + cascadedDelete: boolean; } diff --git a/Tests/Witsml.Tests/ServiceReference/OptionsInTests.cs b/Tests/Witsml.Tests/ServiceReference/OptionsInTests.cs index 2d6bea24c..44d53857b 100644 --- a/Tests/Witsml.Tests/ServiceReference/OptionsInTests.cs +++ b/Tests/Witsml.Tests/ServiceReference/OptionsInTests.cs @@ -62,7 +62,7 @@ public void GetKeywords_OptionsInString_MultipleKeywords_ReturnsCorrectValue() [Fact] public void GetKeywords_OptionsInStringAndOtherOptions_ReturnsCorrectValue() { - OptionsIn optionsIn = new(ReturnElements.DataOnly, 50, 100, true, "foo=bar;baz=qux"); + OptionsIn optionsIn = new(ReturnElements.DataOnly, 50, 100, true, false, "foo=bar;baz=qux"); Assert.Equal("returnElements=data-only;maxReturnNodes=50;requestLatestValues=100;requestObjectSelectionCapability=true;foo=bar;baz=qux", optionsIn.GetKeywords()); } diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellWorkerTests.cs index 33fb96f21..0ed840eb1 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellWorkerTests.cs @@ -9,6 +9,7 @@ using Witsml; using Witsml.Data; +using Witsml.ServiceReference; using WitsmlExplorer.Api.Jobs; using WitsmlExplorer.Api.Models; @@ -37,14 +38,15 @@ public DeleteWellWorkerTests() _worker = new DeleteWellWorker(logger, witsmlClientProvider.Object); } - private static DeleteWellJob CreateJob() + private static DeleteWellJob CreateJob(bool cascadedDelete) { return new() { ToDelete = new() { WellUid = WellUid - } + }, + CascadedDelete = cascadedDelete }; } @@ -54,7 +56,18 @@ public async Task Execute_DeleteWell_RefreshAction() _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny())) .ReturnsAsync(new QueryResult(true)); - (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob()); + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(false)); + Assert.True(result.IsSuccess); + Assert.True(((RefreshWell)refreshAction).WellUid == WellUid); + } + + [Fact] + public async Task Execute_CascadedDeleteWell_RefreshAction() + { + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new QueryResult(true)); + + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(true)); Assert.True(result.IsSuccess); Assert.True(((RefreshWell)refreshAction).WellUid == WellUid); } @@ -67,10 +80,26 @@ public async Task Execute_DeleteWell_ReturnResult() .Callback((wells) => query = wells) .ReturnsAsync(new QueryResult(true)); - (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob()); + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(false)); + Assert.True(result.IsSuccess); + Assert.Single(query.Wells); + Assert.Equal(WellUid, query.Wells.First().Uid); + _witsmlClient.Verify(client => client.DeleteFromStoreAsync(It.IsAny(), It.Is(options => options.CascadedDelete == true)), Times.Never); + } + + [Fact] + public async Task Execute_CascadedDeleteWell_ReturnResult() + { + WitsmlWells query = null; + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny(), It.IsAny())) + .Callback((wells, _) => query = wells) + .ReturnsAsync(new QueryResult(true)); + + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(true)); Assert.True(result.IsSuccess); Assert.Single(query.Wells); Assert.Equal(WellUid, query.Wells.First().Uid); + _witsmlClient.Verify(client => client.DeleteFromStoreAsync(It.IsAny(), It.Is(options => options.CascadedDelete == true)), Times.Once); } } } diff --git a/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellboreWorkerTests.cs b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellboreWorkerTests.cs index ad446bef7..ca0c588ec 100644 --- a/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellboreWorkerTests.cs +++ b/Tests/WitsmlExplorer.Api.Tests/Workers/DeleteWellboreWorkerTests.cs @@ -9,6 +9,7 @@ using Witsml; using Witsml.Data; +using Witsml.ServiceReference; using WitsmlExplorer.Api.Jobs; using WitsmlExplorer.Api.Models; @@ -38,7 +39,7 @@ public DeleteWellboreWorkerTests() _worker = new DeleteWellboreWorker(logger, witsmlClientProvider.Object); } - private static DeleteWellboreJob CreateJob() + private static DeleteWellboreJob CreateJob(bool cascadedDelete) { return new() { @@ -46,7 +47,8 @@ private static DeleteWellboreJob CreateJob() { WellboreUid = WellboreUid, WellUid = WellUid - } + }, + CascadedDelete = cascadedDelete }; } @@ -56,7 +58,19 @@ public async Task Execute_DeleteWellbore_RefreshAction() _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny())) .ReturnsAsync(new QueryResult(true)); - (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob()); + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(false)); + Assert.True(result.IsSuccess); + Assert.True(((RefreshWellbore)refreshAction).WellboreUid == WellboreUid); + Assert.True(((RefreshWellbore)refreshAction).WellUid == WellUid); + } + + [Fact] + public async Task Execute_CascadedDeleteWellbore_RefreshAction() + { + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new QueryResult(true)); + + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(true)); Assert.True(result.IsSuccess); Assert.True(((RefreshWellbore)refreshAction).WellboreUid == WellboreUid); Assert.True(((RefreshWellbore)refreshAction).WellUid == WellUid); @@ -70,10 +84,26 @@ public async Task Execute_DeleteWellbore_ReturnResult() .Callback((wellBores) => query = wellBores) .ReturnsAsync(new QueryResult(true)); - (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob()); + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(false)); + Assert.True(result.IsSuccess); + Assert.Single(query.Wellbores); + Assert.Equal(WellboreUid, query.Wellbores.First().Uid); + _witsmlClient.Verify(client => client.DeleteFromStoreAsync(It.IsAny(), It.Is(options => options.CascadedDelete == true)), Times.Never); + } + + [Fact] + public async Task Execute_CascadedDeleteWellbore_ReturnResult() + { + WitsmlWellbores query = null; + _witsmlClient.Setup(client => client.DeleteFromStoreAsync(It.IsAny(), It.IsAny())) + .Callback((wellBores, _) => query = wellBores) + .ReturnsAsync(new QueryResult(true)); + + (WorkerResult result, RefreshAction refreshAction) = await _worker.Execute(CreateJob(true)); Assert.True(result.IsSuccess); Assert.Single(query.Wellbores); Assert.Equal(WellboreUid, query.Wellbores.First().Uid); + _witsmlClient.Verify(client => client.DeleteFromStoreAsync(It.IsAny(), It.Is(options => options.CascadedDelete == true)), Times.Once); } } }