From 999c14c3d3c2498167b8d354c6969746efa28c8b Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Wed, 11 Nov 2026 07:35:37 +0800 Subject: [PATCH] Delete the active block from its zoomed view --- shared/src/logseq/core/core_effect.ml | 2 + shared/src/logseq/model.ml | 65 +++++++++++++--------- shared/src/logseq/model.mli | 13 +++-- shared/src/logseq/native_bridge.ml | 3 + shared/src/logseq/view_base.ml | 4 +- shared/src/logseq/view_base.mli | 2 +- shared/src/logseq/view_screens.ml | 24 ++++---- shared/src/logseq/view_settings.ml | 25 +++++---- shared/test/logseq/app_test.ml | 51 +++++++++++++++-- shared/test/logseq/localization_test.ml | 2 +- shared/test/logseq/mobile_feedback_test.ml | 2 +- shared/test/logseq/node_property_test.ml | 27 +++++++-- 12 files changed, 156 insertions(+), 64 deletions(-) diff --git a/shared/src/logseq/core/core_effect.ml b/shared/src/logseq/core/core_effect.ml index d5b05721..948d4c87 100644 --- a/shared/src/logseq/core/core_effect.ml +++ b/shared/src/logseq/core/core_effect.ml @@ -64,6 +64,8 @@ let request ~now ~fresh_id payload = | "set-page-favorite" -> let* uuid = text () in let* value = integer fields "value" in json "setPageFavorite" (["pageUuid", `String uuid; "favorite", `Bool (value = 1)] @ operation ()) | "delete-page" -> let* uuid = text () in json "deletePage" (("pageUuid", `String uuid) :: operation ()) + | "delete-block" -> let* uuid = text () in let* cursor = integer fields "value" in + json "deleteBlock" (["uuid", `String uuid; "expectedServerT", `Int cursor] @ operation ()) | "review-flashcard" -> let* uuid = string fields "uuid" in let* rating = text () in json "reviewFlashcard" (["uuid", `String uuid; "rating", `String rating] @ operation ()) | "tap-outliner-block" -> text_event "tapBlock" "uuid" diff --git a/shared/src/logseq/model.ml b/shared/src/logseq/model.ml index 5e1e5d1c..618470d4 100644 --- a/shared/src/logseq/model.ml +++ b/shared/src/logseq/model.ml @@ -32,6 +32,8 @@ type sidebar_page = ; title : string } +type node_deletion = PageToDelete of sidebar_page | BlockToDelete of sidebar_page + type ui_session = { graph_id : string option ; destination : primary_destination @@ -298,6 +300,7 @@ type logseq_effect = | PresentPageShareEffect of int * string * string list | SetPageFavoriteEffect of int * string * bool | DeletePageEffect of int * string + | DeleteBlockEffect of int * string * int | SyncNowEffect of int | SyncPendingEffect of int | SetOutlinerTaskStatusEffect of int * string * task_status @@ -399,7 +402,7 @@ type logseq_model = ; new_graph_password_required : bool ; new_graph_encrypted : bool ; pending_graph_deletion : graph option - ; pending_page_deletion : sidebar_page option + ; pending_node_deletion : node_deletion option ; pending_sign_out : bool ; connection_menu_open : bool ; settings_open : bool @@ -579,9 +582,9 @@ type logseq_action = | CloseConnectionMenu | ToggleActivePageFavorite | ShareActivePage - | RequestDeleteActivePage - | CancelDeleteActivePage - | ConfirmDeleteActivePage + | RequestDeleteActiveNode + | CancelDeleteActiveNode + | ConfirmDeleteActiveNode | OpenSyncDetails | CloseSyncDetails | SyncNow @@ -762,7 +765,7 @@ let initial () = new_graph_password_required = false; new_graph_encrypted = true; pending_graph_deletion = None; - pending_page_deletion = None; + pending_node_deletion = None; pending_sign_out = false; connection_menu_open = false; settings_open = false; @@ -878,7 +881,7 @@ let effect_id eff = | PresentAssetEffect (id, _, _, _) -> id | PresentPageShareEffect (id, _, _) -> id | SetPageFavoriteEffect (id, _, _) -> id - | DeletePageEffect (id, _) -> id + | DeletePageEffect (id, _) | DeleteBlockEffect (id, _, _) -> id | SyncNowEffect id -> id | SyncPendingEffect id -> id | SetOutlinerTaskStatusEffect (id, _, _) -> id @@ -1448,7 +1451,17 @@ let node_permission (current : logseq_model) uuid permission = List.assoc_opt permission fields <> Some (`Bool false) | _ -> true -let page_can_delete current uuid = node_permission current uuid "canDelete" +let active_delete_target (current : logseq_model) = + let routes = if current.search_open then + app_node_routes_from current current.search_navigation_path 0 + else app_node_routes current in + match last_opt routes with + | Some route when route.node_uuid <> route.node_page_uuid -> + if current.applied_server_t <> None && node_permission current route.node_uuid "canDelete" then + Some (BlockToDelete {uuid = route.node_uuid; title = route.node_title}) + else None + | _ -> Option.bind (active_page current) (fun page -> + if node_permission current page.uuid "canDelete" then Some (PageToDelete page) else None) let node_can_set_tags current uuid = node_permission current uuid "canAdd" let page_is_favorite_ (current : logseq_model) uuid = @@ -1675,6 +1688,11 @@ let search_query_ready query = String.trim query <> "" let resolve_successful_effect (current : logseq_model) eff message = match eff with + | DeleteBlockEffect (_, uuid, _) -> + let path = if current.search_open then current.search_navigation_path else current.app_navigation_path in + if Option.map navigation_route_uuid (last_opt path) <> Some uuid then current + else if current.search_open then back_search_navigation current 1 + else back_app_navigation current 1 | MaintainSearchIndexEffect (_, graph_id) -> let ready = try let open Yojson.Basic.Util in @@ -2848,23 +2866,19 @@ let rec update (current : logseq_model) action = , page_share_text page current.outliner_rows , page_share_asset_paths current.outliner_rows )) | None -> current) - | RequestDeleteActivePage -> - (match active_page current with - | Some page when page_can_delete current page.uuid -> - { - current with - connection_menu_open = false; - pending_page_deletion = Some page; - } - | _ -> current) - | CancelDeleteActivePage -> - { current with pending_page_deletion = None } - | ConfirmDeleteActivePage -> - (match current.pending_page_deletion with - | Some page when not (page_can_delete current page.uuid) -> - { current with pending_page_deletion = None } - | Some page -> - let updated = { current with pending_page_deletion = None } in + | RequestDeleteActiveNode -> + (match active_delete_target current with + | Some target -> {current with connection_menu_open = false; pending_node_deletion = Some target} + | None -> current) + | CancelDeleteActiveNode -> + { current with pending_node_deletion = None } + | ConfirmDeleteActiveNode -> + let target = current.pending_node_deletion in + let updated = {current with pending_node_deletion = None} in + (match target, active_delete_target current, current.applied_server_t with + | Some (BlockToDelete block), Some (BlockToDelete active), Some cursor when block.uuid = active.uuid -> + enqueue_effect updated (DeleteBlockEffect (updated.next_effect_id, block.uuid, cursor)) + | Some (PageToDelete page), Some (PageToDelete active), _ when page.uuid = active.uuid -> let delete_id = updated.next_effect_id in let deleting = enqueue_effect updated (DeletePageEffect (delete_id, page.uuid)) @@ -2886,7 +2900,7 @@ let rec update (current : logseq_model) action = enqueue_effect closed (CloseAppNodeEffect (close_id, navigation_route_uuid route)) end - | None -> current) + | _ -> updated) | OpenJournalCalendar -> enqueue_effect current (PresentJournalCalendarEffect current.next_effect_id) | OpenSettings -> { @@ -3327,6 +3341,7 @@ and apply_core_snapshot (current : logseq_model) else SyncedState else OfflineState); applied_server_t = projection.applied_server_t; + pending_node_deletion = (if selected_graph_changed then None else current.pending_node_deletion); has_pending_semantic_operations = projection.has_pending_semantic_operations; has_pending_sync_request = projection.has_pending_sync_request; diff --git a/shared/src/logseq/model.mli b/shared/src/logseq/model.mli index 49bac7da..5adf89df 100644 --- a/shared/src/logseq/model.mli +++ b/shared/src/logseq/model.mli @@ -29,6 +29,8 @@ type sidebar_page = ; title : string } +type node_deletion = PageToDelete of sidebar_page | BlockToDelete of sidebar_page + type ui_session = { graph_id : string option ; destination : primary_destination @@ -295,6 +297,7 @@ type logseq_effect = | PresentPageShareEffect of int * string * string list | SetPageFavoriteEffect of int * string * bool | DeletePageEffect of int * string + | DeleteBlockEffect of int * string * int | SyncNowEffect of int | SyncPendingEffect of int | SetOutlinerTaskStatusEffect of int * string * task_status @@ -396,7 +399,7 @@ type logseq_model = ; new_graph_password_required : bool ; new_graph_encrypted : bool ; pending_graph_deletion : graph option - ; pending_page_deletion : sidebar_page option + ; pending_node_deletion : node_deletion option ; pending_sign_out : bool ; connection_menu_open : bool ; settings_open : bool @@ -576,9 +579,9 @@ type logseq_action = | CloseConnectionMenu | ToggleActivePageFavorite | ShareActivePage - | RequestDeleteActivePage - | CancelDeleteActivePage - | ConfirmDeleteActivePage + | RequestDeleteActiveNode + | CancelDeleteActiveNode + | ConfirmDeleteActiveNode | OpenSyncDetails | CloseSyncDetails | SyncNow @@ -706,7 +709,7 @@ val indexed_outliner_autocomplete_candidates : val valid_authentication_state_ : string -> bool val current_settings : logseq_model -> settings_projection val active_page : logseq_model -> sidebar_page option -val page_can_delete : logseq_model -> string -> bool +val active_delete_target : logseq_model -> node_deletion option val node_can_set_tags : logseq_model -> string -> bool val page_is_favorite_ : logseq_model -> string -> bool val page_share_text : sidebar_page -> outline_row list -> string diff --git a/shared/src/logseq/native_bridge.ml b/shared/src/logseq/native_bridge.ml index 5f2b8102..a760959c 100644 --- a/shared/src/logseq/native_bridge.ml +++ b/shared/src/logseq/native_bridge.ml @@ -299,6 +299,9 @@ let encode_effect (eff : Model.logseq_effect) = | DeletePageEffect (id, uuid) -> "{\"id\":" ^ string_of_int id ^ ",\"kind\":\"delete-page\",\"text\":" ^ Lui_wire.quoted uuid ^ "}" + | DeleteBlockEffect (id, uuid, cursor) -> + Yojson.Basic.to_string (`Assoc ["id", `Int id; "kind", `String "delete-block"; + "text", `String uuid; "value", `Int cursor]) | PdfAnnotationEffect (id, payload) -> Yojson.Basic.to_string (`Assoc ["id", `Int id; "kind", `String "pdf-annotation"; "text", `String payload]) | ResultViewEffect (id, _, payload) -> diff --git a/shared/src/logseq/view_base.ml b/shared/src/logseq/view_base.ml index d81309c2..3c74d538 100644 --- a/shared/src/logseq/view_base.ml +++ b/shared/src/logseq/view_base.ml @@ -243,8 +243,8 @@ let active_page_favorite_label (current : Model.logseq_model) = else "Favorite" | None -> "Favorite" -let page_deletion_pending_ (current : Model.logseq_model) = - current.pending_page_deletion <> None +let node_deletion_pending_ (current : Model.logseq_model) = + current.pending_node_deletion <> None let sign_out_pending_ (current : Model.logseq_model) = current.pending_sign_out diff --git a/shared/src/logseq/view_base.mli b/shared/src/logseq/view_base.mli index d56f825a..8b16d880 100644 --- a/shared/src/logseq/view_base.mli +++ b/shared/src/logseq/view_base.mli @@ -45,7 +45,7 @@ val sync_cursor_label : Model.logseq_model -> string val active_page_actions_visible_ : Model.logseq_model -> bool val connection_settings_visible_ : Model.logseq_model -> bool val active_page_favorite_label : Model.logseq_model -> string -val page_deletion_pending_ : Model.logseq_model -> bool +val node_deletion_pending_ : Model.logseq_model -> bool val sign_out_pending_ : Model.logseq_model -> bool val sidebar_page_identifier : Model.sidebar_page -> string val sidebar_page_title : Model.sidebar_page -> string diff --git a/shared/src/logseq/view_screens.ml b/shared/src/logseq/view_screens.ml index 5b3cbe8f..4a17497c 100644 --- a/shared/src/logseq/view_screens.ml +++ b/shared/src/logseq/view_screens.ml @@ -568,15 +568,17 @@ let active_overflow_menu model_source send : t = (menu_item ~text:(View_base.tr (Signal.sample model_source) "Share") ?icon:(item_icon "share") ~on_press:(press send Model.ShareActivePage) []); - if_ ~test:(Signal.map (fun current -> - match Model.active_page current with - | Some page -> Model.page_can_delete current page.uuid - | None -> false) model_source) - (menu_item ~text:(View_base.tr (Signal.sample model_source) "Delete") ~variant:`destructive - ~accessibility_identifier:"button.page.delete" - ?icon:(item_icon "toolbar-delete") - ~on_press:(press send Model.RequestDeleteActivePage) - []); + keyed ~source:(own (Signal.map (fun model -> Option.to_list (Model.active_delete_target model)) model_source)) + ~key:(function Model.BlockToDelete _ -> `Block | Model.PageToDelete _ -> `Page) ~cmp:compare + ~mount:(fun source -> match Signal.sample source with + | Model.BlockToDelete _ -> + menu_item ~text:(View_base.tr (Signal.sample model_source) "Delete block") ~variant:`destructive + ~accessibility_identifier:"button.block.delete" ?icon:(item_icon "toolbar-delete") + ~on_press:(press send Model.RequestDeleteActiveNode) [] + | Model.PageToDelete _ -> + menu_item ~text:(View_base.tr (Signal.sample model_source) "Delete") ~variant:`destructive + ~accessibility_identifier:"button.page.delete" ?icon:(item_icon "toolbar-delete") + ~on_press:(press send Model.RequestDeleteActiveNode) []); if_ ~test:settings_source (menu_item ~text:(View_base.tr (Signal.sample model_source) "Settings") ?icon:(item_icon "settings") ~on_press:(press send Model.OpenSettings) @@ -984,7 +986,7 @@ let application_main_content (context : Lui_ui.ui_context) model_source model_source) (View_graphs.graph_password_sheet context model_source send); if_ - ~test:(Signal.map View_base.page_deletion_pending_ model_source) + ~test:(Signal.map View_base.node_deletion_pending_ model_source) (View_settings.page_delete_dialog model_source context send); if_ ~test:(Signal.map View_base.sign_out_pending_ model_source) @@ -1029,7 +1031,7 @@ let application_main_content (context : Lui_ui.ui_context) model_source model_source) (View_graphs.graph_password_sheet context model_source send); if_ - ~test:(Signal.map View_base.page_deletion_pending_ model_source) + ~test:(Signal.map View_base.node_deletion_pending_ model_source) (View_settings.page_delete_dialog model_source context send); if_ ~test:(Signal.map View_base.sign_out_pending_ model_source) diff --git a/shared/src/logseq/view_settings.ml b/shared/src/logseq/view_settings.ml index 05fac36d..c331c549 100644 --- a/shared/src/logseq/view_settings.ml +++ b/shared/src/logseq/view_settings.ml @@ -1172,14 +1172,19 @@ let settings_sheet (context : Lui_ui.ui_context) model_source send : t = else settings_main_sheet context model_source send let page_delete_dialog model_source (context : Lui_ui.ui_context) send : t = + let label page block = Signal.map (fun (model : Model.logseq_model) -> + View_base.tr model (match model.pending_node_deletion with + | Some (Model.BlockToDelete _) -> block | _ -> page)) model_source in + let title = label "Delete page?" "Delete block?" in + let description = label "The page will be moved to Recycle." "The block and its children will be deleted." in if Lui_ui.host context = KotlinHost then - dialog ~text:(View_base.tr (Signal.sample model_source) "Delete page?") ~height:280 + dialog ~text_signal:title ~height:280 ~accessibility_identifier:"dialog.page-delete" - ~on_dismiss:(press send Model.CancelDeleteActivePage) + ~on_dismiss:(press send Model.CancelDeleteActiveNode) [ column ~gap:16 ~cross:`stretch [ - text ~value:(View_base.tr (Signal.sample model_source) "The page will be moved to Recycle.") ~grow:1.0 []; + text ~value_signal:description ~grow:1.0 []; spacer ~grow:1.0 []; row ~main:`end_ [ @@ -1190,12 +1195,12 @@ let page_delete_dialog model_source (context : Lui_ui.ui_context) send : t = button ~variant:`ghost ~style_class:"cancellation-action" ~accessibility_identifier: "button.page-delete.cancel" - ~on_press:(press send Model.CancelDeleteActivePage) + ~on_press:(press send Model.CancelDeleteActiveNode) ~text:(View_base.tr (Signal.sample model_source) "Cancel") []; button ~variant:`destructive ~accessibility_identifier: "button.page-delete.confirm" - ~on_press:(press send Model.ConfirmDeleteActivePage) + ~on_press:(press send Model.ConfirmDeleteActiveNode) ~text:(View_base.tr (Signal.sample model_source) "Delete") []; ]; ]; @@ -1204,18 +1209,18 @@ let page_delete_dialog model_source (context : Lui_ui.ui_context) send : t = else (* Text-and-buttons-only children keep the dialog on the native alert path (rich children fall back to a custom sheet). *) - dialog ~text:(View_base.tr (Signal.sample model_source) "Delete page?") ~style_class:"alert" + dialog ~text_signal:title ~style_class:"alert" ~accessibility_identifier:"dialog.page-delete" - ~on_dismiss:(press send Model.CancelDeleteActivePage) + ~on_dismiss:(press send Model.CancelDeleteActiveNode) [ - text ~value:(View_base.tr (Signal.sample model_source) "The page will be moved to Recycle.") []; + text ~value_signal:description []; button ~style_class:"cancellation-action" ~accessibility_identifier:"button.page-delete.cancel" - ~on_press:(press send Model.CancelDeleteActivePage) + ~on_press:(press send Model.CancelDeleteActiveNode) ~text:(View_base.tr (Signal.sample model_source) "Cancel") []; button ~variant:`destructive ~accessibility_identifier:"button.page-delete.confirm" - ~on_press:(press send Model.ConfirmDeleteActivePage) + ~on_press:(press send Model.ConfirmDeleteActiveNode) ~text:(View_base.tr (Signal.sample model_source) "Delete") []; ] diff --git a/shared/test/logseq/app_test.ml b/shared/test/logseq/app_test.ml index 0da467c4..41f206ab 100644 --- a/shared/test/logseq/app_test.ml +++ b/shared/test/logseq/app_test.ml @@ -9063,8 +9063,8 @@ let active_page_actions_use_typed_core_and_platform_effects () = in let favorited = Model.update current Model.ToggleActivePageFavorite in let shared = Model.update favorited Model.ShareActivePage in - let requested = Model.update shared Model.RequestDeleteActivePage in - let deleted = Model.update requested Model.ConfirmDeleteActivePage in + let requested = Model.update shared Model.RequestDeleteActiveNode in + let deleted = Model.update requested Model.ConfirmDeleteActiveNode in check_eq ~msg:"favorite changes cross the semantic core boundary" favorited.pending_effects [ Model.SetPageFavoriteEffect (1, "page-a", true) ]; @@ -9079,7 +9079,7 @@ let active_page_actions_use_typed_core_and_platform_effects () = (2, "Project\n- Photo.jpg", [ "Assets/Photo.jpg" ]); ]; check_eq ~msg:"page deletion requires explicit confirmation" - requested.pending_page_deletion (Some page); + requested.pending_node_deletion (Some (Model.PageToDelete page)); check_eq ~msg:"confirmed deletion recycles the page and leaves its selected route" deleted.pending_effects [ @@ -9100,7 +9100,7 @@ let page_delete_dialog_exposes_stable_material_actions () = send application (apply_core_snapshot None sidebar [] true "" [] [] None None [] [] [] false []); - send application Model.RequestDeleteActivePage; + send application Model.RequestDeleteActiveNode; flush application; let renderer = Lui_app.runtime application in let root = main_root renderer application in @@ -9113,6 +9113,48 @@ let page_delete_dialog_exposes_stable_material_actions () = (descendant_with_identifier renderer root "button.page-delete.confirm" <> -1) +let zoomed_journal_block_has_a_block_delete_action () = + let app = App.create (kotlin_backend ()) in + let uuid = "journal-child" and journal = "journal-page" in + let snapshot = { (empty_core_projection ()) with + selected_graph_id = Some "work"; applied_server_t = Some 42; + node_routes = [node_projection uuid journal "Journal entry" [] []] } in + start app; + send app (Model.RequestAppNode uuid); + send app (Model.ApplyCoreSnapshot snapshot); + flush app; + let runtime = Lui_app.runtime app in + let find = descendant_with_identifier runtime (Lui_app.root_node app) in + let delete = find "button.block.delete" in + check ~msg:"the zoomed block has a delete action" (delete <> -1); + check_eq ~msg:"the journal page is never the target of a zoomed block action" + (find "button.page.delete") (-1); + dispatch app (Lui_protocol.Press delete); flush app; + let dialog = find "dialog.page-delete" in + check_eq (property_string runtime dialog Lui_protocol.TextValue) "Delete block?"; + dispatch app (Lui_protocol.Press (find "button.page-delete.confirm")); flush app; + let delete_id = List.find_map (function + | Model.DeleteBlockEffect (id, target, cursor) -> + check_eq ~msg:"delete is scoped to the zoomed block" target uuid; + check_eq cursor 42; + Some id + | _ -> None) (App.model app).pending_effects |> Option.get in + check_eq (App.model app).app_navigation_path [Model.NodeRoute uuid]; + send app (Model.DequeueEffect delete_id); + send app (Model.ResolveEffect (delete_id, false, "Stale cursor")); flush app; + check_eq ~msg:"a rejected deletion leaves the block open" + (App.model app).app_navigation_path [Model.NodeRoute uuid]; + dispatch app (Lui_protocol.Press (find "button.block.delete")); flush app; + dispatch app (Lui_protocol.Press (find "button.page-delete.confirm")); flush app; + let retry_id = List.find_map (function + | Model.DeleteBlockEffect (id, _, _) -> Some id | _ -> None) + (App.model app).pending_effects |> Option.get in + send app (Model.DequeueEffect retry_id); + send app (Model.ResolveEffect (retry_id, true, "")); flush app; + check_eq ~msg:"a successful block deletion returns to the journal" + (App.model app).app_navigation_path [] + + let connection_menu_matches_active_page_actions () = let application = App.create (ios_backend ()) in let page = { Model.uuid = "page-a"; title = "Project" } in @@ -11012,6 +11054,7 @@ let cases = case "outliner-row-press-publishes-a-typed-core-effect" outliner_row_press_publishes_a_typed_core_effect; case "kotlin-rich-rows-own-their-primary-tap" kotlin_rich_rows_own_their_primary_tap; case "active-page-actions-use-typed-core-and-platform-effects" active_page_actions_use_typed_core_and_platform_effects; + case "zoomed journal blocks expose block deletion" zoomed_journal_block_has_a_block_delete_action; case "page-delete-dialog-exposes-stable-material-actions" page_delete_dialog_exposes_stable_material_actions; case "connection-menu-matches-active-page-actions" connection_menu_matches_active_page_actions; case "page-and-block-overflow-keeps-one-native-icon" page_and_block_overflow_keeps_one_native_icon; diff --git a/shared/test/logseq/localization_test.ml b/shared/test/logseq/localization_test.ml index 08e86c78..ef1f0e65 100644 --- a/shared/test/logseq/localization_test.ml +++ b/shared/test/logseq/localization_test.ml @@ -124,7 +124,7 @@ let confirmation_cancel_actions_are_semantic_and_localized () = (List.filter (function Model.DeleteLocalGraphEffect _ | Model.DeletePageEffect _ -> true | _ -> false) (App.model app).pending_effects) []) ["button.graph-delete.cancel", Model.RequestDeleteGraph "local"; - "button.page-delete.cancel", Model.RequestDeleteActivePage; + "button.page-delete.cancel", Model.RequestDeleteActiveNode; "button.sign-out.cancel", Model.RequestSignOut]) ["en"; "zh-CN"; "ja"]) [ios_backend; kotlin_backend] diff --git a/shared/test/logseq/mobile_feedback_test.ml b/shared/test/logseq/mobile_feedback_test.ml index 4fd8626a..f509a375 100644 --- a/shared/test/logseq/mobile_feedback_test.ml +++ b/shared/test/logseq/mobile_feedback_test.ml @@ -99,7 +99,7 @@ let search_destinations_keep_the_page_menu () = let favorite = Model.update current Model.ToggleActivePageFavorite in check ~msg:"Favorite targets the visible search page" (List.exists (function Model.SetPageFavoriteEffect (_, uuid, _) -> uuid = Q.page | _ -> false) favorite.pending_effects); - let deleted = Model.update (Model.update current Model.RequestDeleteActivePage) Model.ConfirmDeleteActivePage in + let deleted = Model.update (Model.update current Model.RequestDeleteActiveNode) Model.ConfirmDeleteActiveNode in check_eq ~msg:"deleting a search page preserves the underlying app navigation" deleted.app_navigation_path current.app_navigation_path; check_eq ~msg:"deleting a search page returns to the previous search destination" deleted.search_navigation_path []; diff --git a/shared/test/logseq/node_property_test.ml b/shared/test/logseq/node_property_test.ml index cf68a4d4..964d1d10 100644 --- a/shared/test/logseq/node_property_test.ml +++ b/shared/test/logseq/node_property_test.ml @@ -461,12 +461,12 @@ let builtin_pages_hide_delete_and_reject_deletion_requests () = let host = Lui_app.runtime app in let item = App_test.descendant_with_identifier host (Lui_app.root_node app) "button.page.delete" in check_eq ~msg:(label ^ " menu delete visibility") (item <> -1) allowed; - let requested = Model.update (App.model app) Model.RequestDeleteActivePage in + let requested = Model.update (App.model app) Model.RequestDeleteActiveNode in check_eq ~msg:(label ^ " cannot open a protected delete confirmation") - (requested.pending_page_deletion <> None) allowed; + (requested.pending_node_deletion <> None) allowed; if not allowed then ( - let stale = { requested with Model.pending_page_deletion = Some {Model.uuid = Q.page; title = "Workspace"} } in - let confirmed = Model.update stale Model.ConfirmDeleteActivePage in + let stale = { requested with Model.pending_node_deletion = Some (Model.PageToDelete {Model.uuid = Q.page; title = "Workspace"}) } in + let confirmed = Model.update stale Model.ConfirmDeleteActiveNode in check ~msg:"a stale confirmation cannot enqueue a protected deletion" (not (List.exists (function Model.DeletePageEffect _ -> true | _ -> false) confirmed.pending_effects))))) [App_test.ios_backend; App_test.kotlin_backend]) @@ -907,7 +907,26 @@ let text_property_references_create_journals_and_keep_partial_dates_literal () = (Graph_runtime.pending_operations restored) [])) ["2026-10-26", 20261026; "Oct 26th, 2026", 20261026; "0044-03-15", 440315] +let zoomed_journal_delete_effect_preserves_the_journal () = + R.with_runtime (fun session runtime conn -> + ignore (Ds.transact_conn conn [ + put Q.page "block/journal-day" (Ds.Int64 20200101L); + put R.gamma "block/parent" (Ds.Ref_to (Q.ref_uuid Q.alpha)); + ]); + Graph_runtime.rebase runtime 42 [] []; + let opened = Rpc_test.dispatch_json session "openNode" (J.to_string (`Assoc ["uuid", `String Q.alpha])) in + check_eq (member "ok" opened) (`Bool true); + let rejected = Core_effect_test.execute session "delete-block" ["text", `String Q.alpha; "value", `Int 41] in + check_eq (member "ok" rejected) (`Bool false); + let deleted = Core_effect_test.execute session "delete-block" ["text", `String Q.alpha; "value", `Int 42] in + check_eq ~msg:(J.to_string deleted) (member "ok" deleted) (`Bool true); + let db = Graph_runtime.db runtime in + let exists uuid = Ds.entid db "block/uuid" (Ds.Uuid uuid) <> None in + check ~msg:"the block and its descendants are removed" (not (exists Q.alpha || exists R.gamma)); + check ~msg:"the journal and other entries remain" (exists Q.page && exists Q.beta)) + let cases = [ + case "deleting a zoomed journal block preserves its journal and siblings" zoomed_journal_delete_effect_preserves_the_journal; case "date editors open with empty NLP input and preserve existing values" date_editor_opens_empty_and_preserves_the_selected_date; case "Date properties accept natural language" date_properties_accept_natural_language; case "text property date references create Journals while partial dates remain Pages" text_property_references_create_journals_and_keep_partial_dates_literal;