From 24d6378b4a8f7614c6a2046a0b2c7d6014a4ae18 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 18 Dec 2024 17:11:11 +0700 Subject: [PATCH 01/16] feat: contracts documents screen --- src/app.rs | 2 +- src/backend_task/contract.rs | 18 +- src/context.rs | 15 +- src/ui/components/left_panel.rs | 3 - .../add_contracts_screen.rs | 263 ++++++++++++++++++ .../document_query_screen.rs | 74 +++++ src/ui/contracts_documents/mod.rs | 2 + src/ui/document_query_screen.rs | 191 ------------- src/ui/mod.rs | 20 +- 9 files changed, 389 insertions(+), 199 deletions(-) create mode 100644 src/ui/contracts_documents/add_contracts_screen.rs create mode 100644 src/ui/contracts_documents/document_query_screen.rs create mode 100644 src/ui/contracts_documents/mod.rs delete mode 100644 src/ui/document_query_screen.rs diff --git a/src/app.rs b/src/app.rs index 118519327..a46057cac 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,7 +9,7 @@ use crate::components::core_zmq_listener::{CoreZMQListener, ZMQMessage}; use crate::context::AppContext; use crate::database::Database; use crate::logging::initialize_logger; -use crate::ui::document_query_screen::DocumentQueryScreen; +use crate::ui::contracts_documents::document_query_screen::DocumentQueryScreen; use crate::ui::dpns_contested_names_screen::{ DPNSContestedNamesScreen, DPNSSubscreen, IndividualVoteCastingStatus, }; diff --git a/src/backend_task/contract.rs b/src/backend_task/contract.rs index a7aa3881a..9df319bc6 100644 --- a/src/backend_task/contract.rs +++ b/src/backend_task/contract.rs @@ -1,12 +1,13 @@ use crate::context::AppContext; use dash_sdk::dpp::system_data_contracts::dpns_contract; -use dash_sdk::platform::{DataContract, Fetch, Identifier}; +use dash_sdk::platform::{DataContract, Fetch, FetchMany, Identifier}; use dash_sdk::Sdk; #[derive(Debug, Clone, PartialEq)] pub(crate) enum ContractTask { FetchDPNSContract, FetchContract(Identifier, Option), + FetchContracts(Vec), } impl AppContext { @@ -22,6 +23,21 @@ impl AppContext { Err(e) => Err(e.to_string()), } } + ContractTask::FetchContracts(identifiers) => { + match DataContract::fetch_many(sdk, identifiers).await { + Ok(data_contracts) => { + for data_contract in data_contracts { + if let Some(contract) = &data_contract.1 { + self.db + .insert_contract_if_not_exists(contract, None, self) + .map_err(|e| e.to_string())?; + } + } + Ok(()) + } + Err(e) => Err(e.to_string()), + } + } ContractTask::FetchDPNSContract => { match DataContract::fetch(sdk, Into::::into(dpns_contract::ID_BYTES)) .await diff --git a/src/context.rs b/src/context.rs index 29733f7e6..8fee1db90 100644 --- a/src/context.rs +++ b/src/context.rs @@ -143,6 +143,7 @@ impl AppContext { .insert_local_qualified_identity(&identity.clone().into(), None, self) } + /// Inserts a local qualified identity into the database pub fn insert_local_qualified_identity( &self, qualified_identity: &QualifiedIdentity, @@ -155,6 +156,7 @@ impl AppContext { ) } + /// Updates a local qualified identity in the database pub fn update_local_qualified_identity( &self, qualified_identity: &QualifiedIdentity, @@ -163,6 +165,7 @@ impl AppContext { .update_local_qualified_identity(qualified_identity, self) } + /// Sets the alias for an identity pub fn set_alias(&self, identifier: &Identifier, new_alias: Option<&str>) -> Result<()> { self.db.set_alias(identifier, new_alias) } @@ -182,44 +185,54 @@ impl AppContext { ) } + /// Fetches all local qualified identities from the database pub fn load_local_qualified_identities(&self) -> Result> { let wallets = self.wallets.read().unwrap(); self.db.get_local_qualified_identities(self, &wallets) } + /// Fetches all voting identities from the database pub fn load_local_voting_identities(&self) -> Result> { self.db.get_local_voting_identities(self) } + /// Fetches all contested names from the database including past and active ones pub fn all_contested_names(&self) -> Result> { self.db.get_all_contested_names(self) } + /// Fetches all ongoing contested names from the database pub fn ongoing_contested_names(&self) -> Result> { self.db.get_ongoing_contested_names(self) } + /// Inserts scheduled votes into the database pub fn insert_scheduled_votes(&self, scheduled_votes: &Vec) -> Result<()> { self.db.insert_scheduled_votes(self, &scheduled_votes) } + /// Fetches all scheduled votes from the database pub fn get_scheduled_votes(&self) -> Result> { self.db.get_scheduled_votes(&self) } + /// Clears all scheduled votes from the database pub fn clear_all_scheduled_votes(&self) -> Result<()> { self.db.clear_all_scheduled_votes(self) } + /// Clears all executed scheduled votes from the database pub fn clear_executed_scheduled_votes(&self) -> Result<()> { self.db.clear_executed_scheduled_votes(self) } + /// Deletes a scheduled vote from the database pub fn delete_scheduled_vote(&self, identity_id: &[u8], contested_name: &String) -> Result<()> { self.db .delete_scheduled_vote(self, identity_id, &contested_name) } + /// Marks a scheduled vote as executed in the database pub fn mark_vote_executed(&self, identity_id: &[u8], contested_name: String) -> Result<()> { self.db .mark_vote_executed(self, identity_id, contested_name) @@ -270,7 +283,7 @@ impl AppContext { self.db.get_settings() } - /// Retrieves the DPNS contract along with other contracts from the database. + /// Retrieves all contracts from the database plus the DPNS contract from app context. pub fn get_contracts( &self, limit: Option, diff --git a/src/ui/components/left_panel.rs b/src/ui/components/left_panel.rs index 8887d845d..1b10bdfe3 100644 --- a/src/ui/components/left_panel.rs +++ b/src/ui/components/left_panel.rs @@ -82,9 +82,6 @@ pub fn add_left_panel( .show(ctx, |ui| { ui.vertical_centered(|ui| { for (label, screen_type, icon_path) in buttons.iter() { - if *screen_type == RootScreenType::RootScreenDocumentQuery { - continue; // Skip rendering the document button for now - } if *screen_type == RootScreenType::RootScreenWithdrawsStatus { continue; // Skip rendering the withdrawals button for now } diff --git a/src/ui/contracts_documents/add_contracts_screen.rs b/src/ui/contracts_documents/add_contracts_screen.rs new file mode 100644 index 000000000..4fe8d5376 --- /dev/null +++ b/src/ui/contracts_documents/add_contracts_screen.rs @@ -0,0 +1,263 @@ +use crate::app::{AppAction, DesiredAppAction}; +use crate::backend_task::contract::ContractTask; +use crate::backend_task::BackendTask; +use crate::context::AppContext; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::{MessageType, ScreenLike}; +use dash_sdk::dpp::identifier::Identifier; +use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::dpp::prelude::TimestampMillis; +use eframe::egui::{self, Color32, Context, RichText, Ui}; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +const MAX_CONTRACTS: usize = 10; + +enum AddContractsStatus { + NotStarted, + WaitingForResult(TimestampMillis), + Complete(Vec<(String, Result<(), String>)>), + ErrorMessage(String), +} + +pub struct AddContractsScreen { + pub app_context: Arc, + contract_ids: Vec, + add_contracts_status: AddContractsStatus, +} + +impl AddContractsScreen { + pub fn new(app_context: &Arc) -> Self { + Self { + app_context: app_context.clone(), + contract_ids: vec!["".to_string()], + add_contracts_status: AddContractsStatus::NotStarted, + } + } + + fn add_contract_field(&mut self) { + if self.contract_ids.len() < MAX_CONTRACTS { + self.contract_ids.push("".to_string()); + } + } + + fn parse_identifiers(&self) -> Result, String> { + let mut identifiers = Vec::new(); + for (i, input) in self.contract_ids.iter().enumerate() { + let trimmed = input.trim(); + if trimmed.is_empty() { + continue; // Empty fields are ignored + } + // Try hex first + let identifier = if let Ok(bytes) = hex::decode(trimmed) { + Identifier::from_bytes(&bytes) + .map_err(|e| format!("Invalid ID in field {}: {}", i + 1, e))? + } else { + // Try Base58 + Identifier::from_string(trimmed, Encoding::Base58) + .map_err(|e| format!("Invalid ID in field {}: {}", i + 1, e))? + }; + identifiers.push(identifier); + } + if identifiers.is_empty() { + return Err("No valid contract IDs entered.".to_string()); + } + Ok(identifiers) + } + + fn add_contracts_clicked(&mut self) -> AppAction { + match self.parse_identifiers() { + Ok(identifiers) => { + self.add_contracts_status = AddContractsStatus::WaitingForResult( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(), + ); + AppAction::BackendTask(BackendTask::ContractTask(ContractTask::FetchContracts( + identifiers, + ))) + } + Err(e) => { + self.add_contracts_status = AddContractsStatus::ErrorMessage(e); + AppAction::None + } + } + } + + fn show_input_fields(&mut self, ui: &mut Ui) { + ui.heading("Enter Contract Identifiers:"); + ui.add_space(5.0); + + for (i, contract_id) in self.contract_ids.iter_mut().enumerate() { + ui.horizontal(|ui| { + ui.label(format!("Contract {}:", i + 1)); + ui.text_edit_singleline(contract_id); + }); + ui.add_space(5.0); + } + + if self.contract_ids.len() < MAX_CONTRACTS { + if ui.button("Add Another Contract Field").clicked() { + self.add_contract_field(); + } + } + } + + fn show_success_screen(&mut self, ui: &mut Ui) -> AppAction { + ui.heading("Contracts Added"); + ui.add_space(10.0); + + if let AddContractsStatus::Complete(results) = &self.add_contracts_status { + for (original_input, result) in results { + match result { + Ok(_) => { + ui.colored_label( + Color32::DARK_GREEN, + format!("Contract {}: Successfully Added", original_input), + ); + } + Err(err) => { + ui.colored_label( + Color32::RED, + format!("Contract {}: Failed to Add - {}", original_input, err), + ); + } + } + ui.add_space(5.0); + } + } + + ui.add_space(20.0); + let button = + egui::Button::new(RichText::new("Go back to Contracts Screen").color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .frame(true) + .rounding(3.0); + if ui.add(button).clicked() { + // Return to previous screen + return AppAction::PopScreenAndRefresh; + } + + AppAction::None + } +} + +impl ScreenLike for AddContractsScreen { + fn display_message(&mut self, message: &str, message_type: MessageType) { + match message_type { + MessageType::Success => { + // Assume we get something like "AddContractsComplete" along with the contract results + // You would parse the backend result here and store in Complete state + // For demonstration, let's say the backend returns a success/fail result for each entered ID. + // We’ll simulate it with a placeholder. In real code, you'd store the actual results from the backend. + + // Example: + // self.add_contracts_status = AddContractsStatus::Complete(results_from_backend); + + // If you only got a single message, you might need to implement a channel or another mechanism + // to store the actual results. For now, let's assume results were handled elsewhere + // and that this message indicates completion. + + // If we have no mechanism, let's just set complete with a success message for each. + let results = self + .contract_ids + .iter() + .map(|id| { + if !id.trim().is_empty() { + (id.clone(), Ok(())) + } else { + (id.clone(), Err("Empty input".to_string())) + } + }) + .collect(); + self.add_contracts_status = AddContractsStatus::Complete(results); + } + MessageType::Error => { + self.add_contracts_status = AddContractsStatus::ErrorMessage(message.to_string()); + } + MessageType::Info => { + // Not used in this scenario + } + } + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + let add_contract_button = ( + "Add Contracts", + DesiredAppAction::AddScreenType(crate::ui::ScreenType::AddContracts), + ); + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Document Queries", AppAction::GoToMainScreen), + ("Add Contracts", AppAction::None), + ], + vec![add_contract_button], + ); + + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Add Contracts to Query"); + ui.add_space(10.0); + + match &self.add_contracts_status { + AddContractsStatus::NotStarted | AddContractsStatus::ErrorMessage(_) => { + if let AddContractsStatus::ErrorMessage(msg) = &self.add_contracts_status { + ui.colored_label(Color32::RED, format!("Error: {}", msg)); + ui.add_space(10.0); + } + + // Show input fields + self.show_input_fields(ui); + + ui.add_space(10.0); + // Add Contracts Button + let button = + egui::Button::new(RichText::new("Add Contracts").color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .frame(true) + .rounding(3.0); + if ui.add(button).clicked() { + action = self.add_contracts_clicked(); + } + } + AddContractsStatus::WaitingForResult(start_time) => { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + let elapsed_seconds = now - start_time; + + let display_time = if elapsed_seconds < 60 { + format!( + "{} second{}", + elapsed_seconds, + if elapsed_seconds == 1 { "" } else { "s" } + ) + } else { + let minutes = elapsed_seconds / 60; + let seconds = elapsed_seconds % 60; + format!( + "{} minute{} and {} second{}", + minutes, + if minutes == 1 { "" } else { "s" }, + seconds, + if seconds == 1 { "" } else { "s" } + ) + }; + + ui.label(format!( + "Adding contracts... Time taken so far: {}", + display_time + )); + } + AddContractsStatus::Complete(_) => { + action = self.show_success_screen(ui); + } + } + }); + + action + } +} diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs new file mode 100644 index 000000000..60c6d6ae7 --- /dev/null +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -0,0 +1,74 @@ +use crate::app::{AppAction, DesiredAppAction}; +use crate::context::AppContext; +use crate::ui::components::contract_chooser_panel::add_contract_chooser_panel; +use crate::ui::components::left_panel::add_left_panel; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::{MessageType, RootScreenType, ScreenLike, ScreenType}; +use chrono::{DateTime, Utc}; +use egui::Context; +use std::sync::Arc; + +pub struct DocumentQueryScreen { + pub app_context: Arc, + error_message: Option<(String, MessageType, DateTime)>, + contract_search_term: String, +} + +impl DocumentQueryScreen { + pub fn new(app_context: &Arc) -> Self { + Self { + app_context: app_context.clone(), + error_message: None, + contract_search_term: String::new(), + } + } + + fn dismiss_error(&mut self) { + self.error_message = None; + } + + fn check_error_expiration(&mut self) { + if let Some((_, _, timestamp)) = &self.error_message { + let now = Utc::now(); + let elapsed = now.signed_duration_since(*timestamp); + + // Automatically dismiss the error message after 5 seconds + if elapsed.num_seconds() > 5 { + self.dismiss_error(); + } + } + } +} + +impl ScreenLike for DocumentQueryScreen { + fn refresh(&mut self) {} + + fn display_message(&mut self, message: &str, message_type: MessageType) { + self.error_message = Some((message.to_string(), message_type, Utc::now())); + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + self.check_error_expiration(); + let add_contract_button = ( + "Add Contracts", + DesiredAppAction::AddScreenType(ScreenType::AddContracts), + ); + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![("Document Queries", AppAction::None)], + vec![add_contract_button], + ); + + action |= add_left_panel( + ctx, + &self.app_context, + RootScreenType::RootScreenDocumentQuery, + ); + + action |= + add_contract_chooser_panel(ctx, &mut self.contract_search_term, &self.app_context); + + action + } +} diff --git a/src/ui/contracts_documents/mod.rs b/src/ui/contracts_documents/mod.rs new file mode 100644 index 000000000..7a803090b --- /dev/null +++ b/src/ui/contracts_documents/mod.rs @@ -0,0 +1,2 @@ +pub mod add_contracts_screen; +pub mod document_query_screen; diff --git a/src/ui/document_query_screen.rs b/src/ui/document_query_screen.rs deleted file mode 100644 index 6354018e7..000000000 --- a/src/ui/document_query_screen.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::app::AppAction; -use crate::backend_task::contested_names::ContestedResourceTask; -use crate::backend_task::BackendTask; -use crate::context::AppContext; -use crate::model::contested_name::ContestedName; -use crate::ui::components::contract_chooser_panel::add_contract_chooser_panel; -use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::top_panel::add_top_panel; -use crate::ui::{MessageType, RootScreenType, ScreenLike}; -use chrono::{DateTime, Utc}; -use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; -use egui::{Context, Ui}; -use std::sync::{Arc, Mutex}; - -#[derive(Clone, Copy, PartialEq, Eq)] -enum SortColumn { - ContestedName, - LockedVotes, - AbstainVotes, - EndingTime, - LastUpdated, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum SortOrder { - Ascending, - Descending, -} - -pub struct DocumentQueryScreen { - contested_names: Arc>>, - pub app_context: Arc, - error_message: Option<(String, MessageType, DateTime)>, - sort_column: SortColumn, - sort_order: SortOrder, - show_vote_popup: Option<(String, ContestedResourceTask)>, - contract_search_term: String, -} - -impl DocumentQueryScreen { - pub fn new(app_context: &Arc) -> Self { - let contested_names = Arc::new(Mutex::new( - app_context.all_contested_names().unwrap_or_default(), - )); - Self { - contested_names, - app_context: app_context.clone(), - error_message: None, - sort_column: SortColumn::ContestedName, - sort_order: SortOrder::Ascending, - show_vote_popup: None, - contract_search_term: String::new(), - } - } - - fn show_contested_name_details( - &mut self, - ui: &mut Ui, - contested_name: &ContestedName, - is_locked_votes_bold: bool, - max_contestant_votes: u32, - ) { - if let Some(contestants) = &contested_name.contestants { - for contestant in contestants { - let button_text = format!("{} - {} votes", contestant.name, contestant.votes); - - // Determine if this contestant's votes should be bold - let text = if contestant.votes == max_contestant_votes && !is_locked_votes_bold { - egui::RichText::new(button_text) - .strong() - .color(egui::Color32::from_rgb(0, 100, 0)) - } else { - egui::RichText::new(button_text) - }; - - if ui.button(text).clicked() { - self.show_vote_popup = Some(( - format!( - "Confirm Voting for Contestant {} for name \"{}\"", - contestant.id, contestant.name - ), - ContestedResourceTask::VoteOnDPNSName( - contested_name.normalized_contested_name.clone(), - ResourceVoteChoice::Abstain, - vec![], - ), - )); - } - } - } - } - - fn sort_contested_names(&self, contested_names: &mut Vec) { - contested_names.sort_by(|a, b| { - let order = match self.sort_column { - SortColumn::ContestedName => a - .normalized_contested_name - .cmp(&b.normalized_contested_name), - SortColumn::LockedVotes => a.locked_votes.cmp(&b.locked_votes), - SortColumn::AbstainVotes => a.abstain_votes.cmp(&b.abstain_votes), - SortColumn::EndingTime => a.end_time.cmp(&b.end_time), - SortColumn::LastUpdated => a.last_updated.cmp(&b.last_updated), - }; - - if self.sort_order == SortOrder::Descending { - order.reverse() - } else { - order - } - }); - } - - fn dismiss_error(&mut self) { - self.error_message = None; - } - - fn check_error_expiration(&mut self) { - if let Some((_, _, timestamp)) = &self.error_message { - let now = Utc::now(); - let elapsed = now.signed_duration_since(*timestamp); - - // Automatically dismiss the error message after 5 seconds - if elapsed.num_seconds() > 5 { - self.dismiss_error(); - } - } - } - - fn toggle_sort(&mut self, column: SortColumn) { - if self.sort_column == column { - self.sort_order = match self.sort_order { - SortOrder::Ascending => SortOrder::Descending, - SortOrder::Descending => SortOrder::Ascending, - }; - } else { - self.sort_column = column; - self.sort_order = SortOrder::Ascending; - } - } - - fn show_vote_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut app_action = AppAction::None; - if let Some((message, action)) = self.show_vote_popup.clone() { - ui.label(message); - - ui.horizontal(|ui| { - if ui.button("Vote Immediate").clicked() { - app_action = AppAction::BackendTask(BackendTask::ContestedResourceTask(action)); - self.show_vote_popup = None; - } else if ui.button("Vote Deferred").clicked() { - app_action = AppAction::BackendTask(BackendTask::ContestedResourceTask(action)); - self.show_vote_popup = None; - } else if ui.button("Cancel").clicked() { - self.show_vote_popup = None; - } - }); - } - app_action - } -} -impl ScreenLike for DocumentQueryScreen { - fn refresh(&mut self) { - let mut contested_names = self.contested_names.lock().unwrap(); - *contested_names = self.app_context.all_contested_names().unwrap_or_default(); - } - - fn display_message(&mut self, message: &str, message_type: MessageType) { - self.error_message = Some((message.to_string(), message_type, Utc::now())); - } - - fn ui(&mut self, ctx: &Context) -> AppAction { - self.check_error_expiration(); - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![("Document Queries", AppAction::None)], - vec![], - ); - - action |= add_left_panel( - ctx, - &self.app_context, - RootScreenType::RootScreenDocumentQuery, - ); - - action |= - add_contract_chooser_panel(ctx, &mut self.contract_search_term, &self.app_context); - - action - } -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 4b77e0aa1..29b03b152 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,7 +5,7 @@ use crate::model::qualified_identity::encrypted_key_storage::{ PrivateKeyData, WalletDerivationPath, }; use crate::model::qualified_identity::QualifiedIdentity; -use crate::ui::document_query_screen::DocumentQueryScreen; +use crate::ui::contracts_documents::document_query_screen::DocumentQueryScreen; use crate::ui::dpns_contested_names_screen::DPNSContestedNamesScreen; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; @@ -18,6 +18,7 @@ use crate::ui::transfers::TransferScreen; use crate::ui::wallet::import_wallet_screen::ImportWalletScreen; use crate::ui::wallet::wallets_screen::WalletsBalancesScreen; use crate::ui::withdrawal_statuses_screen::WithdrawsStatusScreen; +use contracts_documents::add_contracts_screen::AddContractsScreen; use dash_sdk::dpp::identity::Identity; use dash_sdk::dpp::prelude::IdentityPublicKey; use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; @@ -35,7 +36,7 @@ use tool_screens::transition_visualizer_screen::TransitionVisualizerScreen; use wallet::add_new_wallet_screen::AddNewWalletScreen; pub mod components; -pub mod document_query_screen; +pub mod contracts_documents; pub mod dpns_contested_names_screen; pub mod dpns_vote_scheduling_screen; pub(crate) mod identities; @@ -147,6 +148,7 @@ pub enum ScreenType { TopUpIdentity(QualifiedIdentity), ScheduleVoteScreen(String, u64, Vec, ResourceVoteChoice), ScheduledVotes, + AddContracts, } impl ScreenType { @@ -231,6 +233,9 @@ impl ScreenType { ScreenType::ScheduledVotes => Screen::DPNSContestedNamesScreen( DPNSContestedNamesScreen::new(app_context, DPNSSubscreen::ScheduledVotes), ), + ScreenType::AddContracts => { + Screen::AddContractsScreen(AddContractsScreen::new(app_context)) + } } } } @@ -256,6 +261,7 @@ pub enum Screen { NetworkChooserScreen(NetworkChooserScreen), WalletsBalancesScreen(WalletsBalancesScreen), ScheduleVoteScreen(ScheduleVoteScreen), + AddContractsScreen(AddContractsScreen), } impl Screen { @@ -281,6 +287,7 @@ impl Screen { Screen::ImportWalletScreen(screen) => screen.app_context = app_context, Screen::ProofLogScreen(screen) => screen.app_context = app_context, Screen::ScheduleVoteScreen(screen) => screen.app_context = app_context, + Screen::AddContractsScreen(screen) => screen.app_context = app_context, } } } @@ -371,6 +378,7 @@ impl Screen { screen.identities.clone(), screen.vote_choice.clone(), ), + Screen::AddContractsScreen(_) => ScreenType::AddContracts, } } } @@ -398,6 +406,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.refresh(), Screen::ProofLogScreen(screen) => screen.refresh(), Screen::ScheduleVoteScreen(screen) => screen.refresh(), + Screen::AddContractsScreen(screen) => screen.refresh(), } } @@ -423,6 +432,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.refresh_on_arrival(), Screen::ProofLogScreen(screen) => screen.refresh_on_arrival(), Screen::ScheduleVoteScreen(screen) => screen.refresh_on_arrival(), + Screen::AddContractsScreen(screen) => screen.refresh_on_arrival(), } } @@ -448,6 +458,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.ui(ctx), Screen::ProofLogScreen(screen) => screen.ui(ctx), Screen::ScheduleVoteScreen(screen) => screen.ui(ctx), + Screen::AddContractsScreen(screen) => screen.ui(ctx), } } @@ -479,6 +490,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.display_message(message, message_type), Screen::ProofLogScreen(screen) => screen.display_message(message, message_type), Screen::ScheduleVoteScreen(screen) => screen.display_message(message, message_type), + Screen::AddContractsScreen(screen) => screen.display_message(message, message_type), } } @@ -544,6 +556,9 @@ impl ScreenLike for Screen { Screen::ScheduleVoteScreen(screen) => { screen.display_task_result(backend_task_success_result) } + Screen::AddContractsScreen(screen) => { + screen.display_task_result(backend_task_success_result) + } } } @@ -569,6 +584,7 @@ impl ScreenLike for Screen { Screen::WalletsBalancesScreen(screen) => screen.pop_on_success(), Screen::ProofLogScreen(screen) => screen.pop_on_success(), Screen::ScheduleVoteScreen(screen) => screen.pop_on_success(), + Screen::AddContractsScreen(screen) => screen.pop_on_success(), } } } From 1ee32ec49a922d8b05c6bb8e51af58ad47ffe0aa Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 19 Dec 2024 19:15:34 +0800 Subject: [PATCH 02/16] adding contracts roughly works --- src/app.rs | 6 ++ src/backend_task/contract.rs | 24 ++++-- src/backend_task/mod.rs | 10 ++- .../add_contracts_screen.rs | 79 +++++++++---------- 4 files changed, 68 insertions(+), 51 deletions(-) diff --git a/src/app.rs b/src/app.rs index a46057cac..e8a2ffaae 100644 --- a/src/app.rs +++ b/src/app.rs @@ -460,6 +460,12 @@ impl App for AppState { BackendTaskSuccessResult::ToppedUpIdentity(_) => { self.visible_screen_mut().display_task_result(message); } + BackendTaskSuccessResult::FetchedContract(_) => { + self.visible_screen_mut().display_task_result(message); + } + BackendTaskSuccessResult::FetchedContracts(_) => { + self.visible_screen_mut().display_task_result(message); + } }, TaskResult::Error(message) => { self.visible_screen_mut() diff --git a/src/backend_task/contract.rs b/src/backend_task/contract.rs index 9df319bc6..a810fee7c 100644 --- a/src/backend_task/contract.rs +++ b/src/backend_task/contract.rs @@ -3,6 +3,8 @@ use dash_sdk::dpp::system_data_contracts::dpns_contract; use dash_sdk::platform::{DataContract, Fetch, FetchMany, Identifier}; use dash_sdk::Sdk; +use super::BackendTaskSuccessResult; + #[derive(Debug, Clone, PartialEq)] pub(crate) enum ContractTask { FetchDPNSContract, @@ -11,31 +13,40 @@ pub(crate) enum ContractTask { } impl AppContext { - pub async fn run_contract_task(&self, task: ContractTask, sdk: &Sdk) -> Result<(), String> { + pub async fn run_contract_task( + &self, + task: ContractTask, + sdk: &Sdk, + ) -> Result { match task { ContractTask::FetchContract(identifier, name) => { match DataContract::fetch(sdk, identifier).await { Ok(Some(data_contract)) => self .db .insert_contract_if_not_exists(&data_contract, name.as_deref(), self) + .map(|_| BackendTaskSuccessResult::FetchedContract(data_contract)) .map_err(|e| e.to_string()), - Ok(None) => Ok(()), - Err(e) => Err(e.to_string()), + Ok(None) => Err("Contract not found".to_string()), + Err(e) => Err(format!("Error fetching contract: {}", e.to_string())), } } ContractTask::FetchContracts(identifiers) => { match DataContract::fetch_many(sdk, identifiers).await { Ok(data_contracts) => { + let mut results = vec![]; for data_contract in data_contracts { if let Some(contract) = &data_contract.1 { self.db .insert_contract_if_not_exists(contract, None, self) .map_err(|e| e.to_string())?; + results.push(Some(contract.clone())); + } else { + results.push(None); } } - Ok(()) + Ok(BackendTaskSuccessResult::FetchedContracts(results)) } - Err(e) => Err(e.to_string()), + Err(e) => Err(format!("Error fetching contracts: {}", e.to_string())), } } ContractTask::FetchDPNSContract => { @@ -45,9 +56,10 @@ impl AppContext { Ok(Some(data_contract)) => self .db .insert_contract_if_not_exists(&data_contract, Some("dpns"), self) + .map(|_| BackendTaskSuccessResult::FetchedContract(data_contract)) .map_err(|e| e.to_string()), Ok(None) => Err("No DPNS contract found".to_string()), - Err(e) => Err(e.to_string()), + Err(e) => Err(format!("Error fetching DPNS contract: {}", e.to_string())), } } } diff --git a/src/backend_task/mod.rs b/src/backend_task/mod.rs index 5183c62f7..e8a378878 100644 --- a/src/backend_task/mod.rs +++ b/src/backend_task/mod.rs @@ -8,6 +8,7 @@ use crate::backend_task::withdrawal_statuses::{WithdrawStatusPartialData, Withdr use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use contested_names::ScheduledDPNSVote; +use dash_sdk::dpp::prelude::DataContract; use dash_sdk::dpp::voting::votes::Vote; use dash_sdk::query_types::Documents; use std::sync::Arc; @@ -42,6 +43,8 @@ pub(crate) enum BackendTaskSuccessResult { SuccessfulVotes(Vec), CastScheduledVote(ScheduledDPNSVote), WithdrawalStatus(WithdrawStatusPartialData), + FetchedContract(DataContract), + FetchedContracts(Vec>), } impl BackendTaskSuccessResult {} @@ -65,10 +68,9 @@ impl AppContext { ) -> Result { let sdk = self.sdk.clone(); match task { - BackendTask::ContractTask(contract_task) => self - .run_contract_task(contract_task, &sdk) - .await - .map(|_| BackendTaskSuccessResult::None), + BackendTask::ContractTask(contract_task) => { + self.run_contract_task(contract_task, &sdk).await + } BackendTask::ContestedResourceTask(contested_resource_task) => { self.run_contested_resource_task(contested_resource_task, &sdk, sender) .await diff --git a/src/ui/contracts_documents/add_contracts_screen.rs b/src/ui/contracts_documents/add_contracts_screen.rs index 4fe8d5376..6eff35c55 100644 --- a/src/ui/contracts_documents/add_contracts_screen.rs +++ b/src/ui/contracts_documents/add_contracts_screen.rs @@ -3,9 +3,11 @@ use crate::backend_task::contract::ContractTask; use crate::backend_task::BackendTask; use crate::context::AppContext; use crate::ui::components::top_panel::add_top_panel; -use crate::ui::{MessageType, ScreenLike}; +use crate::ui::{BackendTaskSuccessResult, MessageType, ScreenLike}; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::identifier::Identifier; use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::dpp::prelude::DataContract; use dash_sdk::dpp::prelude::TimestampMillis; use eframe::egui::{self, Color32, Context, RichText, Ui}; use std::sync::Arc; @@ -16,7 +18,7 @@ const MAX_CONTRACTS: usize = 10; enum AddContractsStatus { NotStarted, WaitingForResult(TimestampMillis), - Complete(Vec<(String, Result<(), String>)>), + Complete(Vec), // Vec of tuples: original input contract id and option if it was fetched from platform ErrorMessage(String), } @@ -109,21 +111,11 @@ impl AddContractsScreen { ui.add_space(10.0); if let AddContractsStatus::Complete(results) = &self.add_contracts_status { - for (original_input, result) in results { - match result { - Ok(_) => { - ui.colored_label( - Color32::DARK_GREEN, - format!("Contract {}: Successfully Added", original_input), - ); - } - Err(err) => { - ui.colored_label( - Color32::RED, - format!("Contract {}: Failed to Add - {}", original_input, err), - ); - } - } + for id_string in results { + ui.colored_label( + Color32::DARK_GREEN, + format!("Contract {}: Successfully Added", id_string), + ); ui.add_space(5.0); } } @@ -147,37 +139,42 @@ impl ScreenLike for AddContractsScreen { fn display_message(&mut self, message: &str, message_type: MessageType) { match message_type { MessageType::Success => { - // Assume we get something like "AddContractsComplete" along with the contract results - // You would parse the backend result here and store in Complete state - // For demonstration, let's say the backend returns a success/fail result for each entered ID. - // We’ll simulate it with a placeholder. In real code, you'd store the actual results from the backend. - - // Example: - // self.add_contracts_status = AddContractsStatus::Complete(results_from_backend); - - // If you only got a single message, you might need to implement a channel or another mechanism - // to store the actual results. For now, let's assume results were handled elsewhere - // and that this message indicates completion. + // Not used + } + MessageType::Error => { + self.add_contracts_status = AddContractsStatus::ErrorMessage(message.to_string()); + } + MessageType::Info => { + // Not used + } + } + } - // If we have no mechanism, let's just set complete with a success message for each. - let results = self + fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { + match backend_task_success_result { + BackendTaskSuccessResult::FetchedContracts(contract_options) => { + let options = self .contract_ids - .iter() - .map(|id| { - if !id.trim().is_empty() { - (id.clone(), Ok(())) + .clone() + .into_iter() + .filter_map(|input_id| { + if contract_options.iter().any(|option| { + if let Some(contract) = option { + contract.id().to_string(Encoding::Base58) == input_id.trim() + } else { + false + } + }) { + Some(input_id) } else { - (id.clone(), Err("Empty input".to_string())) + None } }) .collect(); - self.add_contracts_status = AddContractsStatus::Complete(results); + self.add_contracts_status = AddContractsStatus::Complete(options); } - MessageType::Error => { - self.add_contracts_status = AddContractsStatus::ErrorMessage(message.to_string()); - } - MessageType::Info => { - // Not used in this scenario + _ => { + // Nothing } } } From 19c8e00a77e86413dcee6cc70ced122c46b01a4d Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 19 Dec 2024 19:22:52 +0800 Subject: [PATCH 03/16] display both found and unfound contracts in success screen --- .../add_contracts_screen.rs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/ui/contracts_documents/add_contracts_screen.rs b/src/ui/contracts_documents/add_contracts_screen.rs index 6eff35c55..f8a4ca4da 100644 --- a/src/ui/contracts_documents/add_contracts_screen.rs +++ b/src/ui/contracts_documents/add_contracts_screen.rs @@ -110,12 +110,20 @@ impl AddContractsScreen { ui.heading("Contracts Added"); ui.add_space(10.0); - if let AddContractsStatus::Complete(results) = &self.add_contracts_status { - for id_string in results { - ui.colored_label( - Color32::DARK_GREEN, - format!("Contract {}: Successfully Added", id_string), - ); + if let AddContractsStatus::Complete(options) = &self.add_contracts_status { + for id_string in self.contract_ids.clone() { + let trimmed_id_string = id_string.trim(); + if options.contains(&trimmed_id_string.to_string()) { + ui.colored_label( + Color32::DARK_GREEN, + format!("Contract {}: Successfully Added", trimmed_id_string), + ); + } else { + ui.colored_label( + Color32::RED, + format!("Contract {}: Not Found", trimmed_id_string), + ); + } ui.add_space(5.0); } } @@ -155,8 +163,7 @@ impl ScreenLike for AddContractsScreen { BackendTaskSuccessResult::FetchedContracts(contract_options) => { let options = self .contract_ids - .clone() - .into_iter() + .iter() .filter_map(|input_id| { if contract_options.iter().any(|option| { if let Some(contract) = option { @@ -170,6 +177,7 @@ impl ScreenLike for AddContractsScreen { None } }) + .cloned() .collect(); self.add_contracts_status = AddContractsStatus::Complete(options); } From 18f232cd4b19aae15168c5c65cb6bf1a21c9af2e Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 20 Dec 2024 00:17:21 +0800 Subject: [PATCH 04/16] progress on the plane without wifi --- src/main.rs | 1 + .../add_contracts_screen.rs | 97 +++++++++---------- .../document_query_screen.rs | 87 ++++++++++++++++- src/utils/mod.rs | 1 + src/utils/parsers.rs | 50 ++++++++++ 5 files changed, 184 insertions(+), 52 deletions(-) create mode 100644 src/utils/mod.rs create mode 100644 src/utils/parsers.rs diff --git a/src/main.rs b/src/main.rs index 59435766f..11f05615c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ mod logging; mod model; mod sdk_wrapper; mod ui; +mod utils; fn main() -> eframe::Result<()> { check_cpu_compatibility(); diff --git a/src/ui/contracts_documents/add_contracts_screen.rs b/src/ui/contracts_documents/add_contracts_screen.rs index f8a4ca4da..a7c214f67 100644 --- a/src/ui/contracts_documents/add_contracts_screen.rs +++ b/src/ui/contracts_documents/add_contracts_screen.rs @@ -7,7 +7,6 @@ use crate::ui::{BackendTaskSuccessResult, MessageType, ScreenLike}; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::identifier::Identifier; use dash_sdk::dpp::platform_value::string_encoding::Encoding; -use dash_sdk::dpp::prelude::DataContract; use dash_sdk::dpp::prelude::TimestampMillis; use eframe::egui::{self, Color32, Context, RichText, Ui}; use std::sync::Arc; @@ -18,13 +17,13 @@ const MAX_CONTRACTS: usize = 10; enum AddContractsStatus { NotStarted, WaitingForResult(TimestampMillis), - Complete(Vec), // Vec of tuples: original input contract id and option if it was fetched from platform + Complete(Vec), ErrorMessage(String), } pub struct AddContractsScreen { pub app_context: Arc, - contract_ids: Vec, + contract_ids_input: Vec, add_contracts_status: AddContractsStatus, } @@ -32,20 +31,20 @@ impl AddContractsScreen { pub fn new(app_context: &Arc) -> Self { Self { app_context: app_context.clone(), - contract_ids: vec!["".to_string()], + contract_ids_input: vec!["".to_string()], add_contracts_status: AddContractsStatus::NotStarted, } } fn add_contract_field(&mut self) { - if self.contract_ids.len() < MAX_CONTRACTS { - self.contract_ids.push("".to_string()); + if self.contract_ids_input.len() < MAX_CONTRACTS { + self.contract_ids_input.push("".to_string()); } } fn parse_identifiers(&self) -> Result, String> { let mut identifiers = Vec::new(); - for (i, input) in self.contract_ids.iter().enumerate() { + for (i, input) in self.contract_ids_input.iter().enumerate() { let trimmed = input.trim(); if trimmed.is_empty() { continue; // Empty fields are ignored @@ -91,7 +90,7 @@ impl AddContractsScreen { ui.heading("Enter Contract Identifiers:"); ui.add_space(5.0); - for (i, contract_id) in self.contract_ids.iter_mut().enumerate() { + for (i, contract_id) in self.contract_ids_input.iter_mut().enumerate() { ui.horizontal(|ui| { ui.label(format!("Contract {}:", i + 1)); ui.text_edit_singleline(contract_id); @@ -99,7 +98,7 @@ impl AddContractsScreen { ui.add_space(5.0); } - if self.contract_ids.len() < MAX_CONTRACTS { + if self.contract_ids_input.len() < MAX_CONTRACTS { if ui.button("Add Another Contract Field").clicked() { self.add_contract_field(); } @@ -107,39 +106,40 @@ impl AddContractsScreen { } fn show_success_screen(&mut self, ui: &mut Ui) -> AppAction { - ui.heading("Contracts Added"); - ui.add_space(10.0); + let mut action = AppAction::None; - if let AddContractsStatus::Complete(options) = &self.add_contracts_status { - for id_string in self.contract_ids.clone() { - let trimmed_id_string = id_string.trim(); - if options.contains(&trimmed_id_string.to_string()) { - ui.colored_label( - Color32::DARK_GREEN, - format!("Contract {}: Successfully Added", trimmed_id_string), - ); - } else { - ui.colored_label( - Color32::RED, - format!("Contract {}: Not Found", trimmed_id_string), - ); + ui.vertical_centered(|ui| { + ui.add_space(50.0); + + ui.heading("🎉"); + ui.heading("Successfully added contracts"); + ui.add_space(10.0); + + if let AddContractsStatus::Complete(options) = &self.add_contracts_status { + for id_string in self.contract_ids_input.clone() { + let trimmed_id_string = id_string.trim(); + if options.contains(&trimmed_id_string.to_string()) { + ui.colored_label(Color32::DARK_GREEN, format!("{} ✅", trimmed_id_string)); + } else { + ui.colored_label(Color32::RED, format!("{} ❌", trimmed_id_string)); + } + ui.add_space(5.0); } - ui.add_space(5.0); } - } - ui.add_space(20.0); - let button = - egui::Button::new(RichText::new("Go back to Contracts Screen").color(Color32::WHITE)) - .fill(Color32::from_rgb(0, 128, 255)) - .frame(true) - .rounding(3.0); - if ui.add(button).clicked() { - // Return to previous screen - return AppAction::PopScreenAndRefresh; - } + ui.add_space(20.0); + let button = + egui::Button::new(RichText::new("Back to Contracts").color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .frame(true) + .rounding(3.0); + if ui.add(button).clicked() { + // Return to previous screen + action = AppAction::PopScreenAndRefresh; + } + }); - AppAction::None + action } } @@ -160,14 +160,15 @@ impl ScreenLike for AddContractsScreen { fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { match backend_task_success_result { - BackendTaskSuccessResult::FetchedContracts(contract_options) => { - let options = self - .contract_ids + BackendTaskSuccessResult::FetchedContracts(maybe_found_contracts) => { + let maybe_contracts = self + .contract_ids_input .iter() .filter_map(|input_id| { - if contract_options.iter().any(|option| { + if maybe_found_contracts.iter().any(|option| { if let Some(contract) = option { contract.id().to_string(Encoding::Base58) == input_id.trim() + || hex::encode(contract.id()) == input_id.trim() } else { false } @@ -179,7 +180,7 @@ impl ScreenLike for AddContractsScreen { }) .cloned() .collect(); - self.add_contracts_status = AddContractsStatus::Complete(options); + self.add_contracts_status = AddContractsStatus::Complete(maybe_contracts); } _ => { // Nothing @@ -188,22 +189,18 @@ impl ScreenLike for AddContractsScreen { } fn ui(&mut self, ctx: &Context) -> AppAction { - let add_contract_button = ( - "Add Contracts", - DesiredAppAction::AddScreenType(crate::ui::ScreenType::AddContracts), - ); let mut action = add_top_panel( ctx, &self.app_context, vec![ - ("Document Queries", AppAction::GoToMainScreen), + ("Contracts", AppAction::GoToMainScreen), ("Add Contracts", AppAction::None), ], - vec![add_contract_button], + vec![], ); egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("Add Contracts to Query"); + ui.heading("Add Contracts"); ui.add_space(10.0); match &self.add_contracts_status { @@ -253,7 +250,7 @@ impl ScreenLike for AddContractsScreen { }; ui.label(format!( - "Adding contracts... Time taken so far: {}", + "Fetching contracts... Time taken so far: {}", display_time )); } diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs index 60c6d6ae7..e11792162 100644 --- a/src/ui/contracts_documents/document_query_screen.rs +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -3,8 +3,9 @@ use crate::context::AppContext; use crate::ui::components::contract_chooser_panel::add_contract_chooser_panel; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::top_panel::add_top_panel; -use crate::ui::{MessageType, RootScreenType, ScreenLike, ScreenType}; +use crate::ui::{BackendTaskSuccessResult, MessageType, RootScreenType, ScreenLike, ScreenType}; use chrono::{DateTime, Utc}; +use dash_sdk::dpp::prelude::DocumentType; use egui::Context; use std::sync::Arc; @@ -12,14 +13,23 @@ pub struct DocumentQueryScreen { pub app_context: Arc, error_message: Option<(String, MessageType, DateTime)>, contract_search_term: String, + document_query: String, + selected_data_contract: DataContract, + selected_document_type: DocumentType, + matching_documents: Vec, } impl DocumentQueryScreen { pub fn new(app_context: &Arc) -> Self { + let selected_document_type = app_context.dpns_contract.document_type_for_name("domain"); Self { app_context: app_context.clone(), error_message: None, contract_search_term: String::new(), + document_query: format!("SELECT * FROM {}", selected_document_type), + selected_data_contract: app_context.dpns_contract, + selected_document_type, + matching_documents: vec![], } } @@ -38,6 +48,50 @@ impl DocumentQueryScreen { } } } + + fn show_input_field(&mut self, ui: &mut Ui) { + ui.label("Document SQL query:"); + ui.horizontal(|ui| { + ui.text_edit_singleline(self.document_query); + let button = egui::Button::new(RichText::new("Go").color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .frame(true) + .rounding(3.0); + if ui.add(button).clicked() { + // Set the status to waiting and capture the current time + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.register_dpns_name_status = DocumentQueryStatus::WaitingForResult(now); + action = AppAction::BackendTask(BackendTask::DocumentTask(FetchDocuments( + self.document_query, + ))); + } + }); + } + + fn show_output(&self, ui: &mut Ui) { + ui.separator(); + ui.label("Matching documents:"); + + ScrollArea::vertical().show(ui, |ui| { + ui.set_width(ui.available_width()); // Make the scroll area take the entire width + + if let Some(ref json) = self.matching_documents { + ui.add( + TextEdit::multiline(&mut json.clone()) + .desired_rows(10) + .desired_width(ui.available_width()) // Make the output take the entire width + .font(egui::TextStyle::Monospace), // Use a monospace font for JSON + ); + } else if let Some(ref error) = self.error_message { + ui.colored_label(egui::Color32::RED, error.0); + } else { + ui.label("No valid documents parsed yet."); + } + }); + } } impl ScreenLike for DocumentQueryScreen { @@ -47,6 +101,15 @@ impl ScreenLike for DocumentQueryScreen { self.error_message = Some((message.to_string(), message_type, Utc::now())); } + fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { + match backend_task_success_result { + BackendTaskSuccessResult::Documents(documents) => self.matching_documents = documents, + _ => { + // Nothing + } + } + } + fn ui(&mut self, ctx: &Context) -> AppAction { self.check_error_expiration(); let add_contract_button = ( @@ -56,7 +119,7 @@ impl ScreenLike for DocumentQueryScreen { let mut action = add_top_panel( ctx, &self.app_context, - vec![("Document Queries", AppAction::None)], + vec![("Contracts", AppAction::None)], vec![add_contract_button], ); @@ -69,6 +132,26 @@ impl ScreenLike for DocumentQueryScreen { action |= add_contract_chooser_panel(ctx, &mut self.contract_search_term, &self.app_context); + egui::CentralPanel::default().show(ctx, |ui| { + self.show_input_field(ui); + + let parser = DocumentQueryTextInputParser::new(self.selected_data_contract); + match parser.parse_input(&query) { + Ok(drive_document_query) => { + // BackendTask to query documents + } + Err(e) => { + self.error_message = Some(( + format!("Failed to parse query properly: {}", e), + MessageType::Error, + Utc::now(), + )); + } + } + + self.show_output(ui); + }); + action } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 000000000..be756a006 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod parsers; diff --git a/src/utils/parsers.rs b/src/utils/parsers.rs new file mode 100644 index 000000000..8fa75e8e9 --- /dev/null +++ b/src/utils/parsers.rs @@ -0,0 +1,50 @@ +//! Parsers for text input. + +use dash_sdk::dpp::prelude::DataContract; +use dash_sdk::platform::{DocumentQuery, DriveDocumentQuery}; +use std::{marker::PhantomData, str::FromStr}; + +pub(crate) trait TextInputParser { + type Output; + fn parse_input(&self, input: &str) -> Result; +} + +pub(crate) struct DefaultTextInputParser { + _t: PhantomData, +} + +impl DefaultTextInputParser { + pub(crate) fn new() -> Self { + DefaultTextInputParser { _t: PhantomData } + } +} + +impl TextInputParser for DefaultTextInputParser { + type Output = T; + + fn parse_input(&self, input: &str) -> Result { + input + .parse() + .map_err(|_| format!("Cannot parse as a {}", std::any::type_name::())) + } +} + +pub(crate) struct DocumentQueryTextInputParser { + data_contract: DataContract, +} + +impl DocumentQueryTextInputParser { + pub(crate) fn new(data_contract: DataContract) -> Self { + DocumentQueryTextInputParser { data_contract } + } +} + +impl TextInputParser for DocumentQueryTextInputParser { + type Output = DocumentQuery; + + fn parse_input(&self, input: &str) -> Result { + DriveDocumentQuery::from_sql_expr(input, &self.data_contract, None) + .map(Into::into) + .map_err(|e| e.to_string()) + } +} From c02e20d6e94a1b65c092f753688a188cfb5ddb31 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 20 Dec 2024 14:02:48 -0500 Subject: [PATCH 05/16] document queries working --- src/backend_task/document.rs | 3 +- src/backend_task/mod.rs | 2 +- .../add_contracts_screen.rs | 23 +-- .../document_query_screen.rs | 166 ++++++++++++------ 4 files changed, 124 insertions(+), 70 deletions(-) diff --git a/src/backend_task/document.rs b/src/backend_task/document.rs index e67ff2de6..90ae3bf15 100644 --- a/src/backend_task/document.rs +++ b/src/backend_task/document.rs @@ -3,7 +3,6 @@ use crate::context::AppContext; use dash_sdk::platform::{Document, DocumentQuery, FetchMany}; use dash_sdk::Sdk; -pub type DocumentTypeName = String; #[derive(Debug, Clone, PartialEq)] pub(crate) enum DocumentTask { FetchDocuments(DocumentQuery), @@ -19,7 +18,7 @@ impl AppContext { DocumentTask::FetchDocuments(drive_query) => Document::fetch_many(sdk, drive_query) .await .map(BackendTaskSuccessResult::Documents) - .map_err(|e| e.to_string()), + .map_err(|e| format!("Error fetching documents: {}", e.to_string())), } } } diff --git a/src/backend_task/mod.rs b/src/backend_task/mod.rs index e8a378878..b804fc62d 100644 --- a/src/backend_task/mod.rs +++ b/src/backend_task/mod.rs @@ -17,7 +17,7 @@ use tokio::sync::mpsc; pub mod contested_names; pub mod contract; pub mod core; -mod document; +pub mod document; pub mod identity; pub mod withdrawal_statuses; diff --git a/src/ui/contracts_documents/add_contracts_screen.rs b/src/ui/contracts_documents/add_contracts_screen.rs index a7c214f67..daa72d433 100644 --- a/src/ui/contracts_documents/add_contracts_screen.rs +++ b/src/ui/contracts_documents/add_contracts_screen.rs @@ -1,4 +1,4 @@ -use crate::app::{AppAction, DesiredAppAction}; +use crate::app::AppAction; use crate::backend_task::contract::ContractTask; use crate::backend_task::BackendTask; use crate::context::AppContext; @@ -161,22 +161,15 @@ impl ScreenLike for AddContractsScreen { fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { match backend_task_success_result { BackendTaskSuccessResult::FetchedContracts(maybe_found_contracts) => { - let maybe_contracts = self + let maybe_contracts: Vec<_> = self .contract_ids_input .iter() - .filter_map(|input_id| { - if maybe_found_contracts.iter().any(|option| { - if let Some(contract) = option { - contract.id().to_string(Encoding::Base58) == input_id.trim() - || hex::encode(contract.id()) == input_id.trim() - } else { - false - } - }) { - Some(input_id) - } else { - None - } + .filter(|input_id| { + maybe_found_contracts.iter().flatten().any(|contract| { + let trimmed = input_id.trim(); + contract.id().to_string(Encoding::Base58) == trimmed + || hex::encode(contract.id()) == trimmed + }) }) .cloned() .collect(); diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs index e11792162..5e9d08008 100644 --- a/src/ui/contracts_documents/document_query_screen.rs +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -1,13 +1,21 @@ use crate::app::{AppAction, DesiredAppAction}; +use crate::backend_task::document::DocumentTask::FetchDocuments; +use crate::backend_task::BackendTask; use crate::context::AppContext; use crate::ui::components::contract_chooser_panel::add_contract_chooser_panel; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::{BackendTaskSuccessResult, MessageType, RootScreenType, ScreenLike, ScreenType}; +use crate::utils::parsers::{DocumentQueryTextInputParser, TextInputParser}; use chrono::{DateTime, Utc}; -use dash_sdk::dpp::prelude::DocumentType; -use egui::Context; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dash_sdk::dpp::data_contract::document_type::DocumentType; +use dash_sdk::dpp::prelude::TimestampMillis; +use dash_sdk::platform::{DataContract, Document}; +use egui::{Color32, Context, RichText, ScrollArea, TextEdit, Ui}; use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; pub struct DocumentQueryScreen { pub app_context: Arc, @@ -16,20 +24,32 @@ pub struct DocumentQueryScreen { document_query: String, selected_data_contract: DataContract, selected_document_type: DocumentType, - matching_documents: Vec, + matching_documents: Vec, + document_query_status: DocumentQueryStatus, +} + +pub enum DocumentQueryStatus { + NotStarted, + WaitingForResult(TimestampMillis), + Complete, + ErrorMessage(String), } impl DocumentQueryScreen { pub fn new(app_context: &Arc) -> Self { - let selected_document_type = app_context.dpns_contract.document_type_for_name("domain"); + let selected_document_type = app_context + .dpns_contract + .document_type_cloned_for_name("domain") + .expect("Expected to find domain document type in DPNS contract"); Self { app_context: app_context.clone(), error_message: None, contract_search_term: String::new(), - document_query: format!("SELECT * FROM {}", selected_document_type), - selected_data_contract: app_context.dpns_contract, + document_query: format!("SELECT * FROM {}", selected_document_type.name()), + selected_data_contract: (*app_context.dpns_contract).clone(), selected_document_type, matching_documents: vec![], + document_query_status: DocumentQueryStatus::NotStarted, } } @@ -42,53 +62,100 @@ impl DocumentQueryScreen { let now = Utc::now(); let elapsed = now.signed_duration_since(*timestamp); - // Automatically dismiss the error message after 5 seconds - if elapsed.num_seconds() > 5 { + // Automatically dismiss the error message after 10 seconds + if elapsed.num_seconds() > 10 { self.dismiss_error(); } } } - fn show_input_field(&mut self, ui: &mut Ui) { - ui.label("Document SQL query:"); + fn show_input_field(&mut self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; ui.horizontal(|ui| { - ui.text_edit_singleline(self.document_query); + ui.label("Document SQL query: "); + ui.text_edit_singleline(&mut self.document_query); let button = egui::Button::new(RichText::new("Go").color(Color32::WHITE)) .fill(Color32::from_rgb(0, 128, 255)) .frame(true) .rounding(3.0); if ui.add(button).clicked() { - // Set the status to waiting and capture the current time - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.register_dpns_name_status = DocumentQueryStatus::WaitingForResult(now); - action = AppAction::BackendTask(BackendTask::DocumentTask(FetchDocuments( - self.document_query, - ))); + let parser = DocumentQueryTextInputParser::new(self.selected_data_contract.clone()); + match parser.parse_input(&self.document_query) { + Ok(parsed_query) => { + // Set the status to waiting and capture the current time + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.document_query_status = DocumentQueryStatus::WaitingForResult(now); + action = AppAction::BackendTask(BackendTask::DocumentTask(FetchDocuments( + parsed_query, + ))); + } + Err(e) => { + self.document_query_status = DocumentQueryStatus::ErrorMessage(format!( + "Failed to parse query properly: {}", + e + )); + self.error_message = Some(( + format!("Failed to parse query properly: {}", e), + MessageType::Error, + Utc::now(), + )); + } + } } }); + + action } - fn show_output(&self, ui: &mut Ui) { + fn show_output(&mut self, ui: &mut Ui) { ui.separator(); ui.label("Matching documents:"); + ui.add_space(10.0); ScrollArea::vertical().show(ui, |ui| { - ui.set_width(ui.available_width()); // Make the scroll area take the entire width - - if let Some(ref json) = self.matching_documents { - ui.add( - TextEdit::multiline(&mut json.clone()) - .desired_rows(10) - .desired_width(ui.available_width()) // Make the output take the entire width - .font(egui::TextStyle::Monospace), // Use a monospace font for JSON - ); - } else if let Some(ref error) = self.error_message { - ui.colored_label(egui::Color32::RED, error.0); - } else { - ui.label("No valid documents parsed yet."); + ui.set_width(ui.available_width()); + + match self.document_query_status { + DocumentQueryStatus::WaitingForResult(start_time) => { + let time_elapsed = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + - start_time; + ui.label(format!( + "Fetching documents... Time taken so far: {}", + time_elapsed + )); + } + DocumentQueryStatus::Complete => { + // Display fetched documents in JSON format (stored in self.matching_documents) + // First, convert documents to JSON strings + let mut json_string_documents = + self.matching_documents + .iter() + .fold(String::new(), |acc, doc| { + let doc_json = serde_json::to_string_pretty(doc).unwrap(); + format!("{}\n{}", acc, doc_json) + }); + + ui.add( + TextEdit::multiline(&mut json_string_documents) + .desired_rows(10) + .desired_width(ui.available_width()) + .font(egui::TextStyle::Monospace), + ); + } + DocumentQueryStatus::ErrorMessage(ref message) => { + self.error_message = + Some((message.to_string(), MessageType::Error, Utc::now())); + ui.colored_label(egui::Color32::DARK_RED, message); + } + _ => { + // Nothing + } } }); } @@ -98,12 +165,22 @@ impl ScreenLike for DocumentQueryScreen { fn refresh(&mut self) {} fn display_message(&mut self, message: &str, message_type: MessageType) { - self.error_message = Some((message.to_string(), message_type, Utc::now())); + // Only display the error message resulting from FetchDocuments backend task + if message.contains("Error fetching documents") { + self.document_query_status = DocumentQueryStatus::ErrorMessage(message.to_string()); + self.error_message = Some((message.to_string(), message_type, Utc::now())); + } } fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { match backend_task_success_result { - BackendTaskSuccessResult::Documents(documents) => self.matching_documents = documents, + BackendTaskSuccessResult::Documents(documents) => { + self.matching_documents = documents + .iter() + .filter_map(|(_, doc)| doc.clone()) + .collect(); + self.document_query_status = DocumentQueryStatus::Complete; + } _ => { // Nothing } @@ -133,22 +210,7 @@ impl ScreenLike for DocumentQueryScreen { add_contract_chooser_panel(ctx, &mut self.contract_search_term, &self.app_context); egui::CentralPanel::default().show(ctx, |ui| { - self.show_input_field(ui); - - let parser = DocumentQueryTextInputParser::new(self.selected_data_contract); - match parser.parse_input(&query) { - Ok(drive_document_query) => { - // BackendTask to query documents - } - Err(e) => { - self.error_message = Some(( - format!("Failed to parse query properly: {}", e), - MessageType::Error, - Utc::now(), - )); - } - } - + action |= self.show_input_field(ui); self.show_output(ui); }); From 40cbc1212f3351280be21fef27979d6870f237ff Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 20 Dec 2024 15:42:23 -0500 Subject: [PATCH 06/16] almost done --- src/ui/components/contract_chooser_panel.rs | 118 +++++++++++++++--- .../document_query_screen.rs | 72 +++++++---- 2 files changed, 147 insertions(+), 43 deletions(-) diff --git a/src/ui/components/contract_chooser_panel.rs b/src/ui/components/contract_chooser_panel.rs index 10eb1896c..56cb303ae 100644 --- a/src/ui/components/contract_chooser_panel.rs +++ b/src/ui/components/contract_chooser_panel.rs @@ -1,24 +1,31 @@ use crate::app::AppAction; use crate::context::AppContext; -use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use crate::model::qualified_contract::QualifiedContract; use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dash_sdk::dpp::data_contract::document_type::Index; +use dash_sdk::dpp::data_contract::{ + accessors::v0::DataContractV0Getters, document_type::DocumentType, +}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; -use egui::{Context, Frame, Margin, SidePanel}; +use egui::{Color32, Context, Frame, Margin, RichText, SidePanel}; use std::sync::Arc; + pub fn add_contract_chooser_panel( ctx: &Context, current_search_term: &mut String, app_context: &Arc, + selected_data_contract: &mut QualifiedContract, + selected_document_type: &mut DocumentType, + selected_index: &mut Option, + document_query: &mut String, ) -> AppAction { let action = AppAction::None; - // Fetch contracts from the app context let contracts = app_context.get_contracts(None, None).unwrap_or_else(|e| { eprintln!("Error fetching contracts: {}", e); vec![] }); - // Filter the contracts based on the search term let filtered_contracts: Vec<_> = contracts .iter() .filter(|contract| { @@ -40,42 +47,119 @@ pub fn add_contract_chooser_panel( .inner_margin(Margin::same(10.0)), ) .show(ctx, |ui| { - // Search bar at the top ui.horizontal(|ui| { ui.label("Search:"); ui.text_edit_singleline(current_search_term); }); - ui.separator(); // Separator below the search bar + ui.separator(); - // Display filtered contracts with nested document types and indexes ui.vertical(|ui| { for contract in filtered_contracts { + let is_selected_contract = *selected_data_contract == *contract; + let name_or_id = contract .alias .clone() .unwrap_or(contract.contract.id().to_string(Encoding::Base58)); - // Expandable contract section - ui.collapsing(name_or_id, |ui| { - // Loop over the document types in the contract + let contract_header_text = if is_selected_contract { + RichText::new(name_or_id).color(Color32::from_rgb(21, 101, 192)) + } else { + RichText::new(name_or_id) + }; + + ui.collapsing(contract_header_text, |ui| { for (doc_name, doc_type) in contract.contract.document_types() { - // Expandable section for each document type - ui.collapsing(doc_name, |ui| { - // Loop over the indexes in the document type - for index in doc_type.indexes().values() { - ui.label(format!("Index: {}", index.name)); - ui.indent("index_properties", |ui| { + let is_selected_doc_type = *selected_document_type == *doc_type; + + let doc_type_header_text = if is_selected_doc_type { + RichText::new(doc_name.clone()) + .color(Color32::from_rgb(21, 101, 192)) + } else { + RichText::new(doc_name.clone()) + }; + + let doc_resp = ui.collapsing(doc_type_header_text, |ui| { + // Display indexes as collapsible items + for (index_name, index) in doc_type.indexes() { + let is_selected_index = *selected_index == Some(index.clone()); + + let index_header_text = if is_selected_index { + RichText::new(format!("Index: {}", index_name)) + .color(Color32::from_rgb(21, 101, 192)) + } else { + RichText::new(format!("Index: {}", index_name)) + }; + + let index_resp = ui.collapsing(index_header_text, |ui| { + // Show index properties if expanded for prop in &index.properties { ui.label(format!("Property: {:?}", prop)); } }); + + // Handle toggling of index + if index_resp.header_response.clicked() { + if index_resp.body_response.is_some() { + // Index opened (expanded) + *selected_index = Some(index.clone()); + if let Ok(new_doc_type) = contract + .contract + .document_type_cloned_for_name(&doc_name) + { + *selected_document_type = new_doc_type; + *selected_data_contract = contract.clone(); + // Rebuild the query with the selected index + *document_query = format!( + "SELECT * FROM {} WHERE `{}` = 'INSERT {} HERE'", + selected_document_type.name(), + index.property_names().first().expect("Expected the index to have at least one property name"), + index.property_names().first().expect("Expected the index to have at least one property name") + ); + } + } else { + // Index closed (collapsed) + *selected_index = None; + // Rebuild the query without index constraint + *document_query = format!( + "SELECT * FROM {}", + selected_document_type.name() + ); + } + } } }); + + // Check doc type toggling + if doc_resp.header_response.clicked() && doc_resp.body_response.is_some() + { + if let Ok(new_doc_type) = + contract.contract.document_type_cloned_for_name(&doc_name) + { + *selected_document_type = new_doc_type; + *selected_data_contract = contract.clone(); + *selected_index = None; + *document_query = format!( + "SELECT * FROM {}", + selected_document_type.name() + ); + } + } else if doc_resp.header_response.clicked() + && doc_resp.body_response.is_none() + { + // Doc type collapsed again: still have doc type & contract + // required, so do not clear them. Just clear index if any. + *selected_index = None; + *document_query = format!( + "SELECT * FROM {}", + selected_document_type.name() + ); + } } }); - ui.add_space(5.0); // Spacing between contracts + ui.add_space(5.0); } }); }); diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs index 5e9d08008..dbeebec48 100644 --- a/src/ui/contracts_documents/document_query_screen.rs +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -2,6 +2,7 @@ use crate::app::{AppAction, DesiredAppAction}; use crate::backend_task::document::DocumentTask::FetchDocuments; use crate::backend_task::BackendTask; use crate::context::AppContext; +use crate::model::qualified_contract::QualifiedContract; use crate::ui::components::contract_chooser_panel::add_contract_chooser_panel; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::top_panel::add_top_panel; @@ -10,10 +11,10 @@ use crate::utils::parsers::{DocumentQueryTextInputParser, TextInputParser}; use chrono::{DateTime, Utc}; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; -use dash_sdk::dpp::data_contract::document_type::DocumentType; +use dash_sdk::dpp::data_contract::document_type::{DocumentType, Index}; use dash_sdk::dpp::prelude::TimestampMillis; -use dash_sdk::platform::{DataContract, Document}; -use egui::{Color32, Context, RichText, ScrollArea, TextEdit, Ui}; +use dash_sdk::platform::Document; +use egui::{Color32, Context, Frame, Margin, RichText, ScrollArea, TextEdit, Ui}; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -22,8 +23,9 @@ pub struct DocumentQueryScreen { error_message: Option<(String, MessageType, DateTime)>, contract_search_term: String, document_query: String, - selected_data_contract: DataContract, + selected_data_contract: QualifiedContract, selected_document_type: DocumentType, + selected_index: Option, matching_documents: Vec, document_query_status: DocumentQueryStatus, } @@ -37,17 +39,24 @@ pub enum DocumentQueryStatus { impl DocumentQueryScreen { pub fn new(app_context: &Arc) -> Self { - let selected_document_type = app_context - .dpns_contract + let dpns_contract = QualifiedContract { + contract: Arc::clone(&app_context.dpns_contract).as_ref().clone(), + alias: Some("dpns".to_string()), + }; + + let selected_document_type = dpns_contract + .contract .document_type_cloned_for_name("domain") .expect("Expected to find domain document type in DPNS contract"); + Self { app_context: app_context.clone(), error_message: None, contract_search_term: String::new(), document_query: format!("SELECT * FROM {}", selected_document_type.name()), - selected_data_contract: (*app_context.dpns_contract).clone(), + selected_data_contract: dpns_contract, selected_document_type, + selected_index: None, matching_documents: vec![], document_query_status: DocumentQueryStatus::NotStarted, } @@ -72,14 +81,14 @@ impl DocumentQueryScreen { fn show_input_field(&mut self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; ui.horizontal(|ui| { - ui.label("Document SQL query: "); ui.text_edit_singleline(&mut self.document_query); - let button = egui::Button::new(RichText::new("Go").color(Color32::WHITE)) + let button = egui::Button::new(RichText::new("Fetch Documents").color(Color32::WHITE)) .fill(Color32::from_rgb(0, 128, 255)) .frame(true) .rounding(3.0); if ui.add(button).clicked() { - let parser = DocumentQueryTextInputParser::new(self.selected_data_contract.clone()); + let parser = + DocumentQueryTextInputParser::new(self.selected_data_contract.contract.clone()); match parser.parse_input(&self.document_query) { Ok(parsed_query) => { // Set the status to waiting and capture the current time @@ -112,7 +121,6 @@ impl DocumentQueryScreen { fn show_output(&mut self, ui: &mut Ui) { ui.separator(); - ui.label("Matching documents:"); ui.add_space(10.0); ScrollArea::vertical().show(ui, |ui| { @@ -131,15 +139,13 @@ impl DocumentQueryScreen { )); } DocumentQueryStatus::Complete => { - // Display fetched documents in JSON format (stored in self.matching_documents) - // First, convert documents to JSON strings - let mut json_string_documents = - self.matching_documents - .iter() - .fold(String::new(), |acc, doc| { - let doc_json = serde_json::to_string_pretty(doc).unwrap(); - format!("{}\n{}", acc, doc_json) - }); + let docs: Vec = self + .matching_documents + .iter() + .map(|doc| serde_json::to_string_pretty(doc).unwrap()) + .collect(); + + let mut json_string_documents = docs.join("\n\n"); ui.add( TextEdit::multiline(&mut json_string_documents) @@ -148,6 +154,7 @@ impl DocumentQueryScreen { .font(egui::TextStyle::Monospace), ); } + DocumentQueryStatus::ErrorMessage(ref message) => { self.error_message = Some((message.to_string(), MessageType::Error, Utc::now())); @@ -206,13 +213,26 @@ impl ScreenLike for DocumentQueryScreen { RootScreenType::RootScreenDocumentQuery, ); - action |= - add_contract_chooser_panel(ctx, &mut self.contract_search_term, &self.app_context); + action |= add_contract_chooser_panel( + ctx, + &mut self.contract_search_term, + &self.app_context, + &mut self.selected_data_contract, + &mut self.selected_document_type, + &mut self.selected_index, + &mut self.document_query, + ); - egui::CentralPanel::default().show(ctx, |ui| { - action |= self.show_input_field(ui); - self.show_output(ui); - }); + egui::CentralPanel::default() + .frame( + Frame::none() + .fill(ctx.style().visuals.panel_fill) + .inner_margin(Margin::same(10.0)), + ) + .show(ctx, |ui| { + action |= self.show_input_field(ui); + self.show_output(ui); + }); action } From 541787038f8b9e2cff06e95e0c2ea8434fa741e7 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 20 Dec 2024 16:05:21 -0500 Subject: [PATCH 07/16] impl indexes with more than one property --- src/ui/components/contract_chooser_panel.rs | 79 +++++++++++-------- .../document_query_screen.rs | 16 ++-- 2 files changed, 57 insertions(+), 38 deletions(-) diff --git a/src/ui/components/contract_chooser_panel.rs b/src/ui/components/contract_chooser_panel.rs index 56cb303ae..779804859 100644 --- a/src/ui/components/contract_chooser_panel.rs +++ b/src/ui/components/contract_chooser_panel.rs @@ -100,39 +100,56 @@ pub fn add_contract_chooser_panel( }); // Handle toggling of index - if index_resp.header_response.clicked() { - if index_resp.body_response.is_some() { - // Index opened (expanded) - *selected_index = Some(index.clone()); - if let Ok(new_doc_type) = contract - .contract - .document_type_cloned_for_name(&doc_name) - { - *selected_document_type = new_doc_type; - *selected_data_contract = contract.clone(); - // Rebuild the query with the selected index - *document_query = format!( - "SELECT * FROM {} WHERE `{}` = 'INSERT {} HERE'", - selected_document_type.name(), - index.property_names().first().expect("Expected the index to have at least one property name"), - index.property_names().first().expect("Expected the index to have at least one property name") - ); - } - } else { - // Index closed (collapsed) - *selected_index = None; - // Rebuild the query without index constraint + // If the index is selected (expanded), build a WHERE clause for all properties: + if index_resp.header_response.clicked() + && index_resp.body_response.is_some() + { + *selected_index = Some(index.clone()); + if let Ok(new_doc_type) = contract + .contract + .document_type_cloned_for_name(&doc_name) + { + *selected_document_type = new_doc_type; + *selected_data_contract = contract.clone(); + + // Build the WHERE clause using all property names + let conditions: Vec = index + .property_names() + .iter() + .map(|property_name| { + format!("`{}` = '___'", property_name) + }) + .collect(); + + let where_clause = if conditions.is_empty() { + String::new() + } else { + format!(" WHERE {}", conditions.join(" AND ")) + }; + *document_query = format!( - "SELECT * FROM {}", - selected_document_type.name() + "SELECT * FROM {}{}", + selected_document_type.name(), + where_clause ); } + } else if index_resp.header_response.clicked() + && index_resp.body_response.is_none() + { + // Index closed (collapsed) + *selected_index = None; + // Rebuild the query without index constraint + *document_query = format!( + "SELECT * FROM {}", + selected_document_type.name() + ); } } }); // Check doc type toggling - if doc_resp.header_response.clicked() && doc_resp.body_response.is_some() + if doc_resp.header_response.clicked() + && doc_resp.body_response.is_some() { if let Ok(new_doc_type) = contract.contract.document_type_cloned_for_name(&doc_name) @@ -140,10 +157,8 @@ pub fn add_contract_chooser_panel( *selected_document_type = new_doc_type; *selected_data_contract = contract.clone(); *selected_index = None; - *document_query = format!( - "SELECT * FROM {}", - selected_document_type.name() - ); + *document_query = + format!("SELECT * FROM {}", selected_document_type.name()); } } else if doc_resp.header_response.clicked() && doc_resp.body_response.is_none() @@ -151,10 +166,8 @@ pub fn add_contract_chooser_panel( // Doc type collapsed again: still have doc type & contract // required, so do not clear them. Just clear index if any. *selected_index = None; - *document_query = format!( - "SELECT * FROM {}", - selected_document_type.name() - ); + *document_query = + format!("SELECT * FROM {}", selected_document_type.name()); } } }); diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs index dbeebec48..dadb51adf 100644 --- a/src/ui/contracts_documents/document_query_screen.rs +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -81,11 +81,17 @@ impl DocumentQueryScreen { fn show_input_field(&mut self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; ui.horizontal(|ui| { - ui.text_edit_singleline(&mut self.document_query); - let button = egui::Button::new(RichText::new("Fetch Documents").color(Color32::WHITE)) - .fill(Color32::from_rgb(0, 128, 255)) - .frame(true) - .rounding(3.0); + let button_width = 120.0; + let text_width = ui.available_width() - button_width; + + ui.add(egui::TextEdit::singleline(&mut self.document_query).desired_width(text_width)); + + let button = egui::Button::new( + egui::RichText::new("Fetch Documents").color(egui::Color32::WHITE), + ) + .fill(egui::Color32::from_rgb(0, 128, 255)) + .frame(true) + .rounding(3.0); if ui.add(button).clicked() { let parser = DocumentQueryTextInputParser::new(self.selected_data_contract.contract.clone()); From 1b94058e3351b1653fbdfb329d85f2fc58647de1 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 20 Dec 2024 16:22:20 -0500 Subject: [PATCH 08/16] ok --- src/ui/components/contract_chooser_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/components/contract_chooser_panel.rs b/src/ui/components/contract_chooser_panel.rs index 779804859..03bae801c 100644 --- a/src/ui/components/contract_chooser_panel.rs +++ b/src/ui/components/contract_chooser_panel.rs @@ -95,7 +95,7 @@ pub fn add_contract_chooser_panel( let index_resp = ui.collapsing(index_header_text, |ui| { // Show index properties if expanded for prop in &index.properties { - ui.label(format!("Property: {:?}", prop)); + ui.label(format!("{:?}", prop)); } }); From d42450cc90bba2f7f505cf14b9e3c2ba6b6ba8f9 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 20 Dec 2024 16:39:49 -0500 Subject: [PATCH 09/16] ok --- src/ui/components/contract_chooser_panel.rs | 2 +- .../document_query_screen.rs | 33 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/ui/components/contract_chooser_panel.rs b/src/ui/components/contract_chooser_panel.rs index 03bae801c..055ce2b21 100644 --- a/src/ui/components/contract_chooser_panel.rs +++ b/src/ui/components/contract_chooser_panel.rs @@ -48,7 +48,7 @@ pub fn add_contract_chooser_panel( ) .show(ctx, |ui| { ui.horizontal(|ui| { - ui.label("Search:"); + ui.label("Filter contracts:"); ui.text_edit_singleline(current_search_term); }); diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs index dadb51adf..072417ebd 100644 --- a/src/ui/contracts_documents/document_query_screen.rs +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -14,7 +14,7 @@ use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getter use dash_sdk::dpp::data_contract::document_type::{DocumentType, Index}; use dash_sdk::dpp::prelude::TimestampMillis; use dash_sdk::platform::Document; -use egui::{Color32, Context, Frame, Margin, RichText, ScrollArea, TextEdit, Ui}; +use egui::{Context, Frame, Margin, ScrollArea, TextEdit, Ui}; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -22,6 +22,7 @@ pub struct DocumentQueryScreen { pub app_context: Arc, error_message: Option<(String, MessageType, DateTime)>, contract_search_term: String, + document_search_term: String, document_query: String, selected_data_contract: QualifiedContract, selected_document_type: DocumentType, @@ -53,6 +54,7 @@ impl DocumentQueryScreen { app_context: app_context.clone(), error_message: None, contract_search_term: String::new(), + document_search_term: String::new(), document_query: format!("SELECT * FROM {}", selected_document_type.name()), selected_data_contract: dpns_contract, selected_document_type, @@ -129,6 +131,15 @@ impl DocumentQueryScreen { ui.separator(); ui.add_space(10.0); + if !self.matching_documents.is_empty() { + ui.horizontal(|ui| { + ui.label("Filter documents:"); + ui.text_edit_singleline(&mut self.document_search_term); + }); + } + + ui.add_space(5.0); + ScrollArea::vertical().show(ui, |ui| { ui.set_width(ui.available_width()); @@ -145,13 +156,31 @@ impl DocumentQueryScreen { )); } DocumentQueryStatus::Complete => { + // Convert docs to JSON strings let docs: Vec = self .matching_documents .iter() .map(|doc| serde_json::to_string_pretty(doc).unwrap()) .collect(); - let mut json_string_documents = docs.join("\n\n"); + // Filter documents based on the document_search_term + let filtered_docs: Vec<&String> = if self.document_search_term.is_empty() { + docs.iter().collect() + } else { + docs.iter() + .filter(|doc_str| { + doc_str + .to_lowercase() + .contains(&self.document_search_term.to_lowercase()) + }) + .collect() + }; + + let mut json_string_documents = filtered_docs + .iter() + .map(|s| s.to_string()) + .collect::>() + .join("\n\n"); ui.add( TextEdit::multiline(&mut json_string_documents) From 005bcee69340a05faf47f8c1f7819958601f9023 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Mon, 23 Dec 2024 14:34:50 -0500 Subject: [PATCH 10/16] more features and working well --- src/backend_task/contract.rs | 7 + src/context.rs | 5 + src/database/contracts.rs | 14 + src/ui/components/contract_chooser_panel.rs | 243 +++++++++------- .../add_contracts_screen.rs | 22 +- .../document_query_screen.rs | 273 +++++++++++++++--- 6 files changed, 425 insertions(+), 139 deletions(-) diff --git a/src/backend_task/contract.rs b/src/backend_task/contract.rs index a810fee7c..9e09e7436 100644 --- a/src/backend_task/contract.rs +++ b/src/backend_task/contract.rs @@ -10,6 +10,7 @@ pub(crate) enum ContractTask { FetchDPNSContract, FetchContract(Identifier, Option), FetchContracts(Vec), + RemoveContract(Identifier), } impl AppContext { @@ -62,6 +63,12 @@ impl AppContext { Err(e) => Err(format!("Error fetching DPNS contract: {}", e.to_string())), } } + ContractTask::RemoveContract(identifier) => self + .remove_contract(&identifier) + .map(|_| { + BackendTaskSuccessResult::Message("Successfully removed contract".to_string()) + }) + .map_err(|e| format!("Error removing contract: {}", e.to_string())), } } } diff --git a/src/context.rs b/src/context.rs index 8fee1db90..27b955c58 100644 --- a/src/context.rs +++ b/src/context.rs @@ -304,6 +304,11 @@ impl AppContext { Ok(contracts) } + // Remove contract from the database by ID + pub fn remove_contract(&self, contract_id: &Identifier) -> Result<()> { + self.db.remove_contract(contract_id.as_bytes(), &self) + } + pub(crate) fn received_transaction_finality( &self, tx: &Transaction, diff --git a/src/database/contracts.rs b/src/database/contracts.rs index 6f92374c8..f2b979b39 100644 --- a/src/database/contracts.rs +++ b/src/database/contracts.rs @@ -184,4 +184,18 @@ impl Database { Ok(contracts) } + + pub fn remove_contract( + &self, + contract_id: &[u8], + app_context: &AppContext, + ) -> rusqlite::Result<()> { + let network = app_context.network_string(); + let conn = self.conn.lock().unwrap(); + conn.execute( + "DELETE FROM contract WHERE contract_id = ? AND network = ?", + rusqlite::params![contract_id, network], + )?; + Ok(()) + } } diff --git a/src/ui/components/contract_chooser_panel.rs b/src/ui/components/contract_chooser_panel.rs index 055ce2b21..056f4a6c7 100644 --- a/src/ui/components/contract_chooser_panel.rs +++ b/src/ui/components/contract_chooser_panel.rs @@ -1,6 +1,9 @@ use crate::app::AppAction; +use crate::backend_task::contract::ContractTask; +use crate::backend_task::BackendTask; use crate::context::AppContext; use crate::model::qualified_contract::QualifiedContract; +use crate::ui::contracts_documents::document_query_screen::DOCUMENT_PRIVATE_FIELDS; use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dash_sdk::dpp::data_contract::document_type::Index; use dash_sdk::dpp::data_contract::{ @@ -8,6 +11,7 @@ use dash_sdk::dpp::data_contract::{ }; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use egui::{Color32, Context, Frame, Margin, RichText, SidePanel}; +use std::collections::HashMap; use std::sync::Arc; pub fn add_contract_chooser_panel( @@ -18,8 +22,10 @@ pub fn add_contract_chooser_panel( selected_document_type: &mut DocumentType, selected_index: &mut Option, document_query: &mut String, + pending_document_type: &mut DocumentType, + pending_fields_selection: &mut HashMap, ) -> AppAction { - let action = AppAction::None; + let mut action = AppAction::None; let contracts = app_context.get_contracts(None, None).unwrap_or_else(|e| { eprintln!("Error fetching contracts: {}", e); @@ -56,123 +62,164 @@ pub fn add_contract_chooser_panel( ui.vertical(|ui| { for contract in filtered_contracts { - let is_selected_contract = *selected_data_contract == *contract; - - let name_or_id = contract - .alias - .clone() - .unwrap_or(contract.contract.id().to_string(Encoding::Base58)); - - let contract_header_text = if is_selected_contract { - RichText::new(name_or_id).color(Color32::from_rgb(21, 101, 192)) - } else { - RichText::new(name_or_id) - }; - - ui.collapsing(contract_header_text, |ui| { - for (doc_name, doc_type) in contract.contract.document_types() { - let is_selected_doc_type = *selected_document_type == *doc_type; - - let doc_type_header_text = if is_selected_doc_type { - RichText::new(doc_name.clone()) - .color(Color32::from_rgb(21, 101, 192)) - } else { - RichText::new(doc_name.clone()) - }; - - let doc_resp = ui.collapsing(doc_type_header_text, |ui| { - // Display indexes as collapsible items - for (index_name, index) in doc_type.indexes() { - let is_selected_index = *selected_index == Some(index.clone()); - - let index_header_text = if is_selected_index { - RichText::new(format!("Index: {}", index_name)) - .color(Color32::from_rgb(21, 101, 192)) + ui.horizontal(|ui| { + let is_selected_contract = *selected_data_contract == *contract; + + let name_or_id = contract + .alias + .clone() + .unwrap_or(contract.contract.id().to_string(Encoding::Base58)); + + let contract_header_text = if is_selected_contract { + RichText::new(name_or_id).color(Color32::from_rgb(21, 101, 192)) + } else { + RichText::new(name_or_id) + }; + + ui.collapsing(contract_header_text, |ui| { + for (doc_name, doc_type) in contract.contract.document_types() { + let is_selected_doc_type = *selected_document_type == *doc_type; + + let doc_type_header_text = if is_selected_doc_type { + RichText::new(doc_name.clone()) + .color(Color32::from_rgb(21, 101, 192)) + } else { + RichText::new(doc_name.clone()) + }; + + let doc_resp = ui.collapsing(doc_type_header_text, |ui| { + // Display indexes as collapsible items + if doc_type.indexes().is_empty() { + ui.label("No indexes defined"); } else { - RichText::new(format!("Index: {}", index_name)) - }; + for (index_name, index) in doc_type.indexes() { + let is_selected_index = + *selected_index == Some(index.clone()); - let index_resp = ui.collapsing(index_header_text, |ui| { - // Show index properties if expanded - for prop in &index.properties { - ui.label(format!("{:?}", prop)); - } - }); - - // Handle toggling of index - // If the index is selected (expanded), build a WHERE clause for all properties: - if index_resp.header_response.clicked() - && index_resp.body_response.is_some() - { - *selected_index = Some(index.clone()); - if let Ok(new_doc_type) = contract - .contract - .document_type_cloned_for_name(&doc_name) - { - *selected_document_type = new_doc_type; - *selected_data_contract = contract.clone(); - - // Build the WHERE clause using all property names - let conditions: Vec = index - .property_names() - .iter() - .map(|property_name| { - format!("`{}` = '___'", property_name) - }) - .collect(); - - let where_clause = if conditions.is_empty() { - String::new() + let index_header_text = if is_selected_index { + RichText::new(format!("Index: {}", index_name)) + .color(Color32::from_rgb(21, 101, 192)) } else { - format!(" WHERE {}", conditions.join(" AND ")) + RichText::new(format!("Index: {}", index_name)) }; - *document_query = format!( - "SELECT * FROM {}{}", - selected_document_type.name(), - where_clause - ); + let index_resp = + ui.collapsing(index_header_text, |ui| { + // Show index properties if expanded + for prop in &index.properties { + ui.label(format!("{:?}", prop)); + } + }); + + // Handle toggling of index + // If the index is selected (expanded), build a WHERE clause for all properties: + if index_resp.header_response.clicked() + && index_resp.body_response.is_some() + { + *selected_index = Some(index.clone()); + if let Ok(new_doc_type) = contract + .contract + .document_type_cloned_for_name(&doc_name) + { + *selected_document_type = new_doc_type; + *selected_data_contract = contract.clone(); + + // Build the WHERE clause using all property names + let conditions: Vec = index + .property_names() + .iter() + .map(|property_name| { + format!("`{}` = '___'", property_name) + }) + .collect(); + + let where_clause = if conditions.is_empty() { + String::new() + } else { + format!( + " WHERE {}", + conditions.join(" AND ") + ) + }; + + *document_query = format!( + "SELECT * FROM {}{}", + selected_document_type.name(), + where_clause + ); + } + } else if index_resp.header_response.clicked() + && index_resp.body_response.is_none() + { + // Index closed (collapsed) + *selected_index = None; + // Rebuild the query without index constraint + *document_query = format!( + "SELECT * FROM {}", + selected_document_type.name() + ); + } } - } else if index_resp.header_response.clicked() - && index_resp.body_response.is_none() + } + }); + + // Check doc type toggling + if doc_resp.header_response.clicked() + && doc_resp.body_response.is_some() + { + if let Ok(new_doc_type) = + contract.contract.document_type_cloned_for_name(&doc_name) { - // Index closed (collapsed) + *pending_document_type = new_doc_type.clone(); + *selected_document_type = new_doc_type.clone(); + *selected_data_contract = contract.clone(); *selected_index = None; - // Rebuild the query without index constraint *document_query = format!( "SELECT * FROM {}", selected_document_type.name() ); + + // Now reinitialize the field selection + pending_fields_selection.clear(); + + // 1) Mark doc-type-defined fields as checked = true + for (field_name, _schema) in + new_doc_type.properties().iter() + { + pending_fields_selection + .insert(field_name.clone(), true); + } + for dash_field in DOCUMENT_PRIVATE_FIELDS { + pending_fields_selection + .insert(dash_field.to_string(), false); + } } - } - }); - - // Check doc type toggling - if doc_resp.header_response.clicked() - && doc_resp.body_response.is_some() - { - if let Ok(new_doc_type) = - contract.contract.document_type_cloned_for_name(&doc_name) + } else if doc_resp.header_response.clicked() + && doc_resp.body_response.is_none() { - *selected_document_type = new_doc_type; - *selected_data_contract = contract.clone(); + // Doc type collapsed again: still have doc type & contract + // required, so do not clear them. Just clear index if any. *selected_index = None; *document_query = format!("SELECT * FROM {}", selected_document_type.name()); } - } else if doc_resp.header_response.clicked() - && doc_resp.body_response.is_none() - { - // Doc type collapsed again: still have doc type & contract - // required, so do not clear them. Just clear index if any. - *selected_index = None; - *document_query = - format!("SELECT * FROM {}", selected_document_type.name()); } - } + }); + + // The Remove button + ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { + // Only show the remove button for non-DPNS contracts + if contract.alias != Some("dpns".to_string()) { + if ui.button("X").clicked() { + action |= AppAction::BackendTask(BackendTask::ContractTask( + ContractTask::RemoveContract( + contract.contract.id().clone(), + ), + )); + } + } + }); }); - - ui.add_space(5.0); } }); }); diff --git a/src/ui/contracts_documents/add_contracts_screen.rs b/src/ui/contracts_documents/add_contracts_screen.rs index daa72d433..dbb3296fc 100644 --- a/src/ui/contracts_documents/add_contracts_screen.rs +++ b/src/ui/contracts_documents/add_contracts_screen.rs @@ -112,18 +112,30 @@ impl AddContractsScreen { ui.add_space(50.0); ui.heading("🎉"); - ui.heading("Successfully added contracts"); + ui.heading("Successfully queried contracts"); ui.add_space(10.0); + ui.label("Found and added the following contracts:"); + ui.add_space(10.0); + let mut not_found = vec![]; if let AddContractsStatus::Complete(options) = &self.add_contracts_status { for id_string in self.contract_ids_input.clone() { - let trimmed_id_string = id_string.trim(); + let trimmed_id_string = id_string.trim().to_string(); if options.contains(&trimmed_id_string.to_string()) { - ui.colored_label(Color32::DARK_GREEN, format!("{} ✅", trimmed_id_string)); + ui.colored_label(Color32::DARK_GREEN, trimmed_id_string); } else { - ui.colored_label(Color32::RED, format!("{} ❌", trimmed_id_string)); + not_found.push(trimmed_id_string); } - ui.add_space(5.0); + } + } + + ui.add_space(20.0); + + if !not_found.is_empty() { + ui.label("The following contracts were not found:"); + ui.add_space(10.0); + for trimmed_id_string in not_found { + ui.colored_label(Color32::RED, trimmed_id_string); } } diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs index 072417ebd..9fca13c05 100644 --- a/src/ui/contracts_documents/document_query_screen.rs +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -1,4 +1,5 @@ use crate::app::{AppAction, DesiredAppAction}; +use crate::backend_task::contract::ContractTask; use crate::backend_task::document::DocumentTask::FetchDocuments; use crate::backend_task::BackendTask; use crate::context::AppContext; @@ -13,22 +14,48 @@ use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dash_sdk::dpp::data_contract::document_type::{DocumentType, Index}; use dash_sdk::dpp::prelude::TimestampMillis; -use dash_sdk::platform::Document; -use egui::{Context, Frame, Margin, ScrollArea, TextEdit, Ui}; +use dash_sdk::platform::{Document, Identifier}; +use egui::{Context, Frame, Margin, ScrollArea, Ui}; +use std::collections::HashMap; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; +/// A list of Dash-specific fields that do not appear in the +/// normal document_type properties. +pub const DOCUMENT_PRIVATE_FIELDS: &[&str] = &[ + "$id", + "$ownerId", + "$version", + "$revision", + "$createdAt", + "$updatedAt", + "$transferredAt", + "$createdAtBlockHeight", + "$updatedAtBlockHeight", + "$transferredAtBlockHeight", + "$createdAtCoreBlockHeight", + "$updatedAtCoreBlockHeight", + "$transferredAtCoreBlockHeight", +]; + pub struct DocumentQueryScreen { pub app_context: Arc, error_message: Option<(String, MessageType, DateTime)>, contract_search_term: String, document_search_term: String, document_query: String, + document_display_mode: DocumentDisplayMode, + document_fields_selection: HashMap, + show_fields_dropdown: bool, selected_data_contract: QualifiedContract, selected_document_type: DocumentType, selected_index: Option, matching_documents: Vec, document_query_status: DocumentQueryStatus, + confirm_remove_contract_popup: bool, + contract_to_remove: Option, + pending_document_type: DocumentType, + pending_fields_selection: HashMap, } pub enum DocumentQueryStatus { @@ -38,6 +65,12 @@ pub enum DocumentQueryStatus { ErrorMessage(String), } +#[derive(PartialEq, Eq, Clone)] +pub enum DocumentDisplayMode { + Json, + Yaml, +} + impl DocumentQueryScreen { pub fn new(app_context: &Arc) -> Self { let dpns_contract = QualifiedContract { @@ -50,17 +83,35 @@ impl DocumentQueryScreen { .document_type_cloned_for_name("domain") .expect("Expected to find domain document type in DPNS contract"); + let mut document_fields_selection = HashMap::new(); + for (field_name, _schema) in selected_document_type.properties().iter() { + document_fields_selection.insert(field_name.clone(), true); + } + for dash_field in DOCUMENT_PRIVATE_FIELDS { + document_fields_selection.insert((*dash_field).to_string(), false); + } + + let pending_document_type = selected_document_type.clone(); + let pending_fields_selection = document_fields_selection.clone(); + Self { app_context: app_context.clone(), error_message: None, contract_search_term: String::new(), document_search_term: String::new(), document_query: format!("SELECT * FROM {}", selected_document_type.name()), + document_display_mode: DocumentDisplayMode::Yaml, + document_fields_selection, + show_fields_dropdown: false, selected_data_contract: dpns_contract, selected_document_type, selected_index: None, matching_documents: vec![], document_query_status: DocumentQueryStatus::NotStarted, + confirm_remove_contract_popup: false, + contract_to_remove: None, + pending_document_type, + pending_fields_selection, } } @@ -95,6 +146,9 @@ impl DocumentQueryScreen { .frame(true) .rounding(3.0); if ui.add(button).clicked() { + self.selected_document_type = self.pending_document_type.clone(); + self.document_fields_selection = self.pending_fields_selection.clone(); + let parser = DocumentQueryTextInputParser::new(self.selected_data_contract.contract.clone()); match parser.parse_input(&self.document_query) { @@ -135,7 +189,53 @@ impl DocumentQueryScreen { ui.horizontal(|ui| { ui.label("Filter documents:"); ui.text_edit_singleline(&mut self.document_search_term); + + // Display mode toggle + ui.label("Display as:"); + if ui + .selectable_label( + self.document_display_mode == DocumentDisplayMode::Yaml, + "YAML", + ) + .clicked() + { + self.document_display_mode = DocumentDisplayMode::Yaml; + } + if ui + .selectable_label( + self.document_display_mode == DocumentDisplayMode::Json, + "JSON", + ) + .clicked() + { + self.document_display_mode = DocumentDisplayMode::Json; + } }); + + if ui.button("Select Properties").clicked() { + self.show_fields_dropdown = !self.show_fields_dropdown; + } + + if self.show_fields_dropdown { + egui::Window::new("Select Properties") + .collapsible(false) + .resizable(false) + .title_bar(false) + .show(ui.ctx(), |ui| { + ui.label("Check the fields to display:"); + + // For each field in the doc type’s properties + for (field_name, is_checked) in &mut self.document_fields_selection { + let text = format!("{}", field_name); + ui.checkbox(is_checked, text); + } + + ui.separator(); + if ui.button("Close").clicked() { + self.show_fields_dropdown = false; + } + }); + } } ui.add_space(5.0); @@ -155,40 +255,14 @@ impl DocumentQueryScreen { time_elapsed )); } - DocumentQueryStatus::Complete => { - // Convert docs to JSON strings - let docs: Vec = self - .matching_documents - .iter() - .map(|doc| serde_json::to_string_pretty(doc).unwrap()) - .collect(); - - // Filter documents based on the document_search_term - let filtered_docs: Vec<&String> = if self.document_search_term.is_empty() { - docs.iter().collect() - } else { - docs.iter() - .filter(|doc_str| { - doc_str - .to_lowercase() - .contains(&self.document_search_term.to_lowercase()) - }) - .collect() - }; - - let mut json_string_documents = filtered_docs - .iter() - .map(|s| s.to_string()) - .collect::>() - .join("\n\n"); - - ui.add( - TextEdit::multiline(&mut json_string_documents) - .desired_rows(10) - .desired_width(ui.available_width()) - .font(egui::TextStyle::Monospace), - ); - } + DocumentQueryStatus::Complete => match self.document_display_mode { + DocumentDisplayMode::Json => { + self.show_filtered_docs(ui, DocumentDisplayMode::Json); + } + DocumentDisplayMode::Yaml => { + self.show_filtered_docs(ui, DocumentDisplayMode::Yaml); + } + }, DocumentQueryStatus::ErrorMessage(ref message) => { self.error_message = @@ -201,6 +275,85 @@ impl DocumentQueryScreen { } }); } + + fn show_filtered_docs(&mut self, ui: &mut egui::Ui, display_mode: DocumentDisplayMode) { + // 1) Convert each Document to a filtered string + let mut doc_strings = Vec::new(); + + for doc in &self.matching_documents { + if let Some(stringed) = doc_to_filtered_string( + doc, + &self.document_fields_selection, // or the user’s selected fields + display_mode.clone(), + ) { + // Optionally also filter by `document_search_term` here + if self.document_search_term.is_empty() + || stringed + .to_lowercase() + .contains(&self.document_search_term.to_lowercase()) + { + doc_strings.push(stringed); + } + } + } + + // 2) Concatenate them all with spacing + let mut combined_string = doc_strings.join("\n\n"); + + // 3) Display in multiline text + ui.add( + egui::TextEdit::multiline(&mut combined_string) + .desired_rows(10) + .desired_width(ui.available_width()) + .font(egui::TextStyle::Monospace), + ); + } + + fn show_remove_contract_popup(&mut self, ui: &mut egui::Ui) -> AppAction { + // If no contract is set, nothing to confirm + let contract_to_remove = match &self.contract_to_remove { + Some(contract) => contract.clone(), + None => { + self.confirm_remove_contract_popup = false; + return AppAction::None; + } + }; + + let mut app_action = AppAction::None; + let mut is_open = true; + + egui::Window::new("Confirm Remove Contract") + .collapsible(false) + .open(&mut is_open) + .show(ui.ctx(), |ui| { + ui.label(format!( + "Are you sure you want to remove contract \"{}\"?", + contract_to_remove + )); + + // Confirm button + if ui.button("Confirm").clicked() { + app_action = AppAction::BackendTask(BackendTask::ContractTask( + ContractTask::RemoveContract(contract_to_remove), + )); + self.confirm_remove_contract_popup = false; + self.contract_to_remove = None; + } + + // Cancel button + if ui.button("Cancel").clicked() { + self.confirm_remove_contract_popup = false; + self.contract_to_remove = None; + } + }); + + // If user closes the popup window (the [x] button), also reset state + if !is_open { + self.confirm_remove_contract_popup = false; + self.contract_to_remove = None; + } + app_action + } } impl ScreenLike for DocumentQueryScreen { @@ -256,8 +409,19 @@ impl ScreenLike for DocumentQueryScreen { &mut self.selected_document_type, &mut self.selected_index, &mut self.document_query, + &mut self.pending_document_type, + &mut self.pending_fields_selection, ); + if let AppAction::BackendTask(BackendTask::ContractTask(ContractTask::RemoveContract( + contract_id, + ))) = action + { + action = AppAction::None; + self.confirm_remove_contract_popup = true; + self.contract_to_remove = Some(contract_id); + } + egui::CentralPanel::default() .frame( Frame::none() @@ -267,8 +431,45 @@ impl ScreenLike for DocumentQueryScreen { .show(ctx, |ui| { action |= self.show_input_field(ui); self.show_output(ui); + + if self.confirm_remove_contract_popup { + action |= self.show_remove_contract_popup(ui); + } }); action } } + +/// Convert a `Document` to a `serde_json::Value`, then filter out unselected fields, +/// then serialize the result to JSON/YAML. +fn doc_to_filtered_string( + doc: &Document, + selected_fields: &std::collections::HashMap, + display_mode: DocumentDisplayMode, +) -> Option { + // 1) Convert doc to a serde_json Value + let value = serde_json::to_value(doc).ok()?; + let obj = value.as_object()?; + + // 2) Build a new JSON object containing only the selected fields + let mut filtered_map = serde_json::Map::new(); + + for (field_name, &is_checked) in selected_fields { + if is_checked { + if let Some(field_value) = obj.get(field_name) { + filtered_map.insert(field_name.clone(), field_value.clone()); + } + } + } + + let filtered_value = serde_json::Value::Object(filtered_map); + + // 3) Convert filtered_value to the chosen format + let final_string = match display_mode { + DocumentDisplayMode::Json => serde_json::to_string_pretty(&filtered_value).ok()?, + DocumentDisplayMode::Yaml => serde_yaml::to_string(&filtered_value).ok()?, + }; + + Some(final_string) +} From 2f29a11626b4c6b4eb89bd98569ad40a95601a71 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Mon, 23 Dec 2024 15:06:39 -0500 Subject: [PATCH 11/16] columns in the field selection popup --- src/backend_task/contract.rs | 14 ++++++- .../document_query_screen.rs | 41 +++++++++++++++---- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/backend_task/contract.rs b/src/backend_task/contract.rs index 9e09e7436..5be66fc5a 100644 --- a/src/backend_task/contract.rs +++ b/src/backend_task/contract.rs @@ -26,7 +26,12 @@ impl AppContext { .db .insert_contract_if_not_exists(&data_contract, name.as_deref(), self) .map(|_| BackendTaskSuccessResult::FetchedContract(data_contract)) - .map_err(|e| e.to_string()), + .map_err(|e| { + format!( + "Error inserting contract into the database: {}", + e.to_string() + ) + }), Ok(None) => Err("Contract not found".to_string()), Err(e) => Err(format!("Error fetching contract: {}", e.to_string())), } @@ -39,7 +44,12 @@ impl AppContext { if let Some(contract) = &data_contract.1 { self.db .insert_contract_if_not_exists(contract, None, self) - .map_err(|e| e.to_string())?; + .map_err(|e| { + format!( + "Error inserting contract into the database: {}", + e.to_string() + ) + })?; results.push(Some(contract.clone())); } else { results.push(None); diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs index 9fca13c05..1057d958a 100644 --- a/src/ui/contracts_documents/document_query_screen.rs +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -217,18 +217,43 @@ impl DocumentQueryScreen { } if self.show_fields_dropdown { - egui::Window::new("Select Properties") + // 1) Partition fields into doc-type vs. dash + let dash_field_set: std::collections::HashSet<&str> = + DOCUMENT_PRIVATE_FIELDS.iter().cloned().collect(); + + let mut doc_type_fields = Vec::new(); + let mut dash_fields = Vec::new(); + + for (field_name, is_checked) in &mut self.document_fields_selection { + if dash_field_set.contains(field_name.as_str()) { + dash_fields.push((field_name, is_checked)); + } else { + doc_type_fields.push((field_name, is_checked)); + } + } + + egui::Window::new("Select Fields") .collapsible(false) - .resizable(false) + .resizable(true) + .min_width(300.0) .title_bar(false) .show(ui.ctx(), |ui| { ui.label("Check the fields to display:"); - - // For each field in the doc type’s properties - for (field_name, is_checked) in &mut self.document_fields_selection { - let text = format!("{}", field_name); - ui.checkbox(is_checked, text); - } + ui.add_space(10.0); + + ui.columns(2, |columns| { + columns[0].heading("Document Fields"); + columns[0].add_space(5.0); + for (field_name, is_checked) in &mut doc_type_fields { + columns[0].checkbox(is_checked, field_name.clone()); + } + + columns[1].heading("Universal Fields"); + columns[1].add_space(5.0); + for (field_name, is_checked) in &mut dash_fields { + columns[1].checkbox(is_checked, field_name.clone()); + } + }); ui.separator(); if ui.button("Close").clicked() { From 0082605d3f90b49b91f4bb8d1765a1a9433b0429 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Mon, 23 Dec 2024 17:09:15 -0500 Subject: [PATCH 12/16] reorg buttons --- .../contracts_documents/document_query_screen.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs index 1057d958a..5f8acf084 100644 --- a/src/ui/contracts_documents/document_query_screen.rs +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -190,6 +190,10 @@ impl DocumentQueryScreen { ui.label("Filter documents:"); ui.text_edit_singleline(&mut self.document_search_term); + if ui.button("Select Properties").clicked() { + self.show_fields_dropdown = !self.show_fields_dropdown; + } + // Display mode toggle ui.label("Display as:"); if ui @@ -212,10 +216,6 @@ impl DocumentQueryScreen { } }); - if ui.button("Select Properties").clicked() { - self.show_fields_dropdown = !self.show_fields_dropdown; - } - if self.show_fields_dropdown { // 1) Partition fields into doc-type vs. dash let dash_field_set: std::collections::HashSet<&str> = @@ -232,23 +232,23 @@ impl DocumentQueryScreen { } } - egui::Window::new("Select Fields") + egui::Window::new("Select Properties") .collapsible(false) .resizable(true) .min_width(300.0) .title_bar(false) .show(ui.ctx(), |ui| { - ui.label("Check the fields to display:"); + ui.label("Check the properties to display:"); ui.add_space(10.0); ui.columns(2, |columns| { - columns[0].heading("Document Fields"); + columns[0].heading("Document Properties"); columns[0].add_space(5.0); for (field_name, is_checked) in &mut doc_type_fields { columns[0].checkbox(is_checked, field_name.clone()); } - columns[1].heading("Universal Fields"); + columns[1].heading("Universal Properties"); columns[1].add_space(5.0); for (field_name, is_checked) in &mut dash_fields { columns[1].checkbox(is_checked, field_name.clone()); From d6f0e717c00b7e064a14275177b3fa32c881344d Mon Sep 17 00:00:00 2001 From: Paul DeLucia <69597248+pauldelucia@users.noreply.github.com> Date: Thu, 26 Dec 2024 10:20:38 -0500 Subject: [PATCH 13/16] feat: document query pagination (#142) * feat: document query pagination * update Cargo.toml with sdk fix --- Cargo.toml | 2 +- src/backend_task/document.rs | 40 ++++++++++++++++++- .../document_query_screen.rs | 8 ++-- src/utils/parsers.rs | 21 ---------- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 530324987..286258a12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ strum = { version = "0.26.1", features = ["derive"] } bs58 = "0.5.0" base64 = "0.22.1" copypasta = "0.10.1" -dash-sdk = { git = "https://github.com/dashpay/platform", rev = "a4f906acd127bc9856d72452eba002da16935209" } +dash-sdk = { git = "https://github.com/dashpay/platform", rev = "e2ed81f0a5af5ef74fc703097e2657349021094b" } thiserror = "1" serde = "1.0.197" serde_json = "1.0.120" diff --git a/src/backend_task/document.rs b/src/backend_task/document.rs index 90ae3bf15..f1d3caae1 100644 --- a/src/backend_task/document.rs +++ b/src/backend_task/document.rs @@ -1,11 +1,14 @@ use crate::backend_task::BackendTaskSuccessResult; use crate::context::AppContext; -use dash_sdk::platform::{Document, DocumentQuery, FetchMany}; +use dash_sdk::platform::proto::get_documents_request::get_documents_request_v0::Start; +use dash_sdk::platform::{Document, DocumentQuery, FetchMany, Identifier}; +use dash_sdk::query_types::IndexMap; use dash_sdk::Sdk; #[derive(Debug, Clone, PartialEq)] pub(crate) enum DocumentTask { FetchDocuments(DocumentQuery), + FetchAllDocuments(DocumentQuery), } impl AppContext { @@ -19,6 +22,41 @@ impl AppContext { .await .map(BackendTaskSuccessResult::Documents) .map_err(|e| format!("Error fetching documents: {}", e.to_string())), + DocumentTask::FetchAllDocuments(mut document_query) => { + // Initialize an empty IndexMap to accumulate documents + let mut all_docs: IndexMap> = IndexMap::new(); + + loop { + // Fetch a batch + let docs_batch_result = Document::fetch_many(sdk, document_query.clone()) + .await + .map_err(|e| format!("Error fetching documents: {}", e))?; + + let batch_len = docs_batch_result.len(); + + // Insert the batch into our master map + for (id, doc_opt) in docs_batch_result { + all_docs.insert(id, doc_opt); + } + + // If fewer than 100 results, we're done + if batch_len < 100 { + break; + } + + // Otherwise, set 'start' to the last document's identifier bytes + if let Some(last_doc_id) = all_docs.keys().last().cloned() { + // Convert the Identifier to bytes + let id_bytes = last_doc_id.to_buffer(); + document_query.start = Some(Start::StartAfter(id_bytes.to_vec())); + } else { + break; + } + } + + // Return all accumulated documents + Ok(BackendTaskSuccessResult::Documents(all_docs)) + } } } } diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs index 5f8acf084..579d6026d 100644 --- a/src/ui/contracts_documents/document_query_screen.rs +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -1,6 +1,6 @@ use crate::app::{AppAction, DesiredAppAction}; use crate::backend_task::contract::ContractTask; -use crate::backend_task::document::DocumentTask::FetchDocuments; +use crate::backend_task::document::DocumentTask::FetchAllDocuments; use crate::backend_task::BackendTask; use crate::context::AppContext; use crate::model::qualified_contract::QualifiedContract; @@ -159,9 +159,9 @@ impl DocumentQueryScreen { .expect("Time went backwards") .as_secs(); self.document_query_status = DocumentQueryStatus::WaitingForResult(now); - action = AppAction::BackendTask(BackendTask::DocumentTask(FetchDocuments( - parsed_query, - ))); + action = AppAction::BackendTask(BackendTask::DocumentTask( + FetchAllDocuments(parsed_query), + )); } Err(e) => { self.document_query_status = DocumentQueryStatus::ErrorMessage(format!( diff --git a/src/utils/parsers.rs b/src/utils/parsers.rs index 8fa75e8e9..9adc8ac1d 100644 --- a/src/utils/parsers.rs +++ b/src/utils/parsers.rs @@ -2,33 +2,12 @@ use dash_sdk::dpp::prelude::DataContract; use dash_sdk::platform::{DocumentQuery, DriveDocumentQuery}; -use std::{marker::PhantomData, str::FromStr}; pub(crate) trait TextInputParser { type Output; fn parse_input(&self, input: &str) -> Result; } -pub(crate) struct DefaultTextInputParser { - _t: PhantomData, -} - -impl DefaultTextInputParser { - pub(crate) fn new() -> Self { - DefaultTextInputParser { _t: PhantomData } - } -} - -impl TextInputParser for DefaultTextInputParser { - type Output = T; - - fn parse_input(&self, input: &str) -> Result { - input - .parse() - .map_err(|_| format!("Cannot parse as a {}", std::any::type_name::())) - } -} - pub(crate) struct DocumentQueryTextInputParser { data_contract: DataContract, } From ba9d1729aa1ba0ac55032f08731f5ecf5299a07a Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Thu, 26 Dec 2024 10:51:13 -0500 Subject: [PATCH 14/16] fix min width of pop up window --- src/ui/contracts_documents/document_query_screen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs index 579d6026d..0b0ba01de 100644 --- a/src/ui/contracts_documents/document_query_screen.rs +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -235,7 +235,7 @@ impl DocumentQueryScreen { egui::Window::new("Select Properties") .collapsible(false) .resizable(true) - .min_width(300.0) + .min_width(400.0) .title_bar(false) .show(ui.ctx(), |ui| { ui.label("Check the properties to display:"); From 55d063b37784811d28fce6aabec4a01b99f4ca5b Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 27 Dec 2024 13:24:53 -0500 Subject: [PATCH 15/16] feat: pagination ui --- src/app.rs | 3 + src/backend_task/document.rs | 78 +++---- src/backend_task/mod.rs | 5 +- .../document_query_screen.rs | 200 ++++++++++++++---- 4 files changed, 211 insertions(+), 75 deletions(-) diff --git a/src/app.rs b/src/app.rs index 475dbb46a..b2134aba2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -466,6 +466,9 @@ impl App for AppState { BackendTaskSuccessResult::FetchedContracts(_) => { self.visible_screen_mut().display_task_result(message); } + BackendTaskSuccessResult::PageDocuments(_, _) => { + self.visible_screen_mut().display_task_result(message); + } }, TaskResult::Error(message) => { self.visible_screen_mut() diff --git a/src/backend_task/document.rs b/src/backend_task/document.rs index f1d3caae1..39742dc30 100644 --- a/src/backend_task/document.rs +++ b/src/backend_task/document.rs @@ -8,7 +8,7 @@ use dash_sdk::Sdk; #[derive(Debug, Clone, PartialEq)] pub(crate) enum DocumentTask { FetchDocuments(DocumentQuery), - FetchAllDocuments(DocumentQuery), + FetchDocumentsPage(DocumentQuery), } impl AppContext { @@ -18,44 +18,48 @@ impl AppContext { sdk: &Sdk, ) -> Result { match task { - DocumentTask::FetchDocuments(drive_query) => Document::fetch_many(sdk, drive_query) - .await - .map(BackendTaskSuccessResult::Documents) - .map_err(|e| format!("Error fetching documents: {}", e.to_string())), - DocumentTask::FetchAllDocuments(mut document_query) => { - // Initialize an empty IndexMap to accumulate documents - let mut all_docs: IndexMap> = IndexMap::new(); - - loop { - // Fetch a batch - let docs_batch_result = Document::fetch_many(sdk, document_query.clone()) - .await - .map_err(|e| format!("Error fetching documents: {}", e))?; - - let batch_len = docs_batch_result.len(); - - // Insert the batch into our master map - for (id, doc_opt) in docs_batch_result { - all_docs.insert(id, doc_opt); - } - - // If fewer than 100 results, we're done - if batch_len < 100 { - break; - } - - // Otherwise, set 'start' to the last document's identifier bytes - if let Some(last_doc_id) = all_docs.keys().last().cloned() { - // Convert the Identifier to bytes - let id_bytes = last_doc_id.to_buffer(); - document_query.start = Some(Start::StartAfter(id_bytes.to_vec())); - } else { - break; - } + DocumentTask::FetchDocuments(document_query) => { + Document::fetch_many(sdk, document_query) + .await + .map(BackendTaskSuccessResult::Documents) + .map_err(|e| format!("Error fetching documents: {}", e.to_string())) + } + DocumentTask::FetchDocumentsPage(mut document_query) => { + // Set the limit for each page + document_query.limit = 100; + + // Initialize an empty IndexMap to accumulate documents for this page + let mut page_docs: IndexMap> = IndexMap::new(); + + // Fetch a single page + let docs_batch_result = Document::fetch_many(sdk, document_query.clone()) + .await + .map_err(|e| format!("Error fetching documents: {}", e))?; + + let batch_len = docs_batch_result.len(); + + // Insert the batch into the page map + for (id, doc_opt) in docs_batch_result { + page_docs.insert(id, doc_opt); } - // Return all accumulated documents - Ok(BackendTaskSuccessResult::Documents(all_docs)) + // Determine if there's a next page + let has_next_page = batch_len == 100; + + // If there's a next page, set the 'start' parameter for the next cursor + let next_cursor = if has_next_page { + page_docs.keys().last().cloned().map(|last_doc_id| { + let id_bytes = last_doc_id.to_buffer(); + Start::StartAfter(id_bytes.to_vec()) + }) + } else { + None + }; + + Ok(BackendTaskSuccessResult::PageDocuments( + page_docs, + next_cursor, + )) } } } diff --git a/src/backend_task/mod.rs b/src/backend_task/mod.rs index b804fc62d..4c5371357 100644 --- a/src/backend_task/mod.rs +++ b/src/backend_task/mod.rs @@ -10,7 +10,9 @@ use crate::model::qualified_identity::QualifiedIdentity; use contested_names::ScheduledDPNSVote; use dash_sdk::dpp::prelude::DataContract; use dash_sdk::dpp::voting::votes::Vote; -use dash_sdk::query_types::Documents; +use dash_sdk::platform::proto::get_documents_request::get_documents_request_v0::Start; +use dash_sdk::platform::{Document, Identifier}; +use dash_sdk::query_types::{Documents, IndexMap}; use std::sync::Arc; use tokio::sync::mpsc; @@ -45,6 +47,7 @@ pub(crate) enum BackendTaskSuccessResult { WithdrawalStatus(WithdrawStatusPartialData), FetchedContract(DataContract), FetchedContracts(Vec>), + PageDocuments(IndexMap>, Option), } impl BackendTaskSuccessResult {} diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs index 0b0ba01de..edd4d9eb7 100644 --- a/src/ui/contracts_documents/document_query_screen.rs +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -1,6 +1,6 @@ use crate::app::{AppAction, DesiredAppAction}; use crate::backend_task::contract::ContractTask; -use crate::backend_task::document::DocumentTask::FetchAllDocuments; +use crate::backend_task::document::DocumentTask::{self, FetchDocumentsPage}; // Updated import use crate::backend_task::BackendTask; use crate::context::AppContext; use crate::model::qualified_contract::QualifiedContract; @@ -14,7 +14,8 @@ use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dash_sdk::dpp::data_contract::document_type::{DocumentType, Index}; use dash_sdk::dpp::prelude::TimestampMillis; -use dash_sdk::platform::{Document, Identifier}; +use dash_sdk::platform::proto::get_documents_request::get_documents_request_v0::Start; +use dash_sdk::platform::{Document, DocumentQuery, Identifier}; use egui::{Context, Frame, Margin, ScrollArea, Ui}; use std::collections::HashMap; use std::sync::Arc; @@ -35,7 +36,6 @@ pub const DOCUMENT_PRIVATE_FIELDS: &[&str] = &[ "$transferredAtBlockHeight", "$createdAtCoreBlockHeight", "$updatedAtCoreBlockHeight", - "$transferredAtCoreBlockHeight", ]; pub struct DocumentQueryScreen { @@ -50,14 +50,20 @@ pub struct DocumentQueryScreen { selected_data_contract: QualifiedContract, selected_document_type: DocumentType, selected_index: Option, - matching_documents: Vec, + pub matching_documents: Vec, document_query_status: DocumentQueryStatus, confirm_remove_contract_popup: bool, contract_to_remove: Option, pending_document_type: DocumentType, pending_fields_selection: HashMap, + // Pagination fields + current_page: usize, + pub next_cursors: Vec, + has_next_page: bool, + previous_cursors: Vec, } +#[derive(PartialEq, Eq, Clone)] pub enum DocumentQueryStatus { NotStarted, WaitingForResult(TimestampMillis), @@ -112,6 +118,11 @@ impl DocumentQueryScreen { contract_to_remove: None, pending_document_type, pending_fields_selection, + // Initialize pagination fields + current_page: 1, + next_cursors: vec![], + has_next_page: false, + previous_cursors: Vec::new(), } } @@ -131,6 +142,28 @@ impl DocumentQueryScreen { } } + fn build_document_query_with_cursor(&self, cursor: &Start) -> DocumentQuery { + let mut query = DocumentQuery::new( + self.selected_data_contract.contract.clone(), + self.selected_document_type.name(), + ) + .expect("Expected to create a new DocumentQuery"); + if self.current_page == 1 { + query.start = None; + } else { + query.start = Some(cursor.clone()); + } + query + } + + fn get_previous_cursor(&mut self) -> Option { + self.previous_cursors.pop() + } + + fn get_next_cursor(&mut self) -> Option { + self.next_cursors.last().cloned() + } + fn show_input_field(&mut self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; ui.horizontal(|ui| { @@ -159,8 +192,11 @@ impl DocumentQueryScreen { .expect("Time went backwards") .as_secs(); self.document_query_status = DocumentQueryStatus::WaitingForResult(now); + self.current_page = 1; // Reset to first page + self.next_cursors = vec![]; // Reset cursor + self.previous_cursors.clear(); // Clear previous cursors action = AppAction::BackendTask(BackendTask::DocumentTask( - FetchAllDocuments(parsed_query), + FetchDocumentsPage(parsed_query), )); } Err(e) => { @@ -181,7 +217,8 @@ impl DocumentQueryScreen { action } - fn show_output(&mut self, ui: &mut Ui) { + fn show_output(&mut self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; ui.separator(); ui.add_space(10.0); @@ -265,40 +302,118 @@ impl DocumentQueryScreen { ui.add_space(5.0); - ScrollArea::vertical().show(ui, |ui| { - ui.set_width(ui.available_width()); - - match self.document_query_status { - DocumentQueryStatus::WaitingForResult(start_time) => { - let time_elapsed = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs() - - start_time; - ui.label(format!( - "Fetching documents... Time taken so far: {}", - time_elapsed - )); - } - DocumentQueryStatus::Complete => match self.document_display_mode { - DocumentDisplayMode::Json => { - self.show_filtered_docs(ui, DocumentDisplayMode::Json); + let pagination_height = 30.0; + let max_scroll_height = ui.available_height() - pagination_height; + + ScrollArea::vertical() + .max_height(max_scroll_height) + .show(ui, |ui| { + ui.set_width(ui.available_width()); + + match self.document_query_status { + DocumentQueryStatus::WaitingForResult(start_time) => { + let time_elapsed = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + - start_time; + ui.horizontal(|ui| { + ui.label(format!( + "Fetching documents... Time taken so far: {} seconds", + time_elapsed + )); + ui.spinner(); + }); } - DocumentDisplayMode::Yaml => { - self.show_filtered_docs(ui, DocumentDisplayMode::Yaml); + DocumentQueryStatus::Complete => match self.document_display_mode { + DocumentDisplayMode::Json => { + self.show_filtered_docs(ui, DocumentDisplayMode::Json); + } + DocumentDisplayMode::Yaml => { + self.show_filtered_docs(ui, DocumentDisplayMode::Yaml); + } + }, + + DocumentQueryStatus::ErrorMessage(ref message) => { + self.error_message = + Some((message.to_string(), MessageType::Error, Utc::now())); + ui.colored_label(egui::Color32::DARK_RED, message); + } + _ => { + // Nothing } - }, + } + }); - DocumentQueryStatus::ErrorMessage(ref message) => { - self.error_message = - Some((message.to_string(), MessageType::Error, Utc::now())); - ui.colored_label(egui::Color32::DARK_RED, message); + ui.add_space(10.0); + + if self.document_query_status == DocumentQueryStatus::Complete { + ui.horizontal(|ui| { + if self.current_page > 1 { + if ui.button("Previous Page").clicked() { + // Handle Previous Page + if let Some(prev_cursor) = self.get_previous_cursor() { + self.document_query_status = DocumentQueryStatus::WaitingForResult( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(), + ); + self.current_page -= 1; + self.next_cursors.pop(); + let parsed_query = self.build_document_query_with_cursor(&prev_cursor); + action = AppAction::BackendTask(BackendTask::DocumentTask( + DocumentTask::FetchDocumentsPage(parsed_query), + )); + } else { + self.document_query_status = DocumentQueryStatus::WaitingForResult( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(), + ); + self.current_page = 1; + let next_cursor = self.get_next_cursor().unwrap(); + let parsed_query = self.build_document_query_with_cursor(&next_cursor); + action = AppAction::BackendTask(BackendTask::DocumentTask( + DocumentTask::FetchDocumentsPage(parsed_query), + )); + } + } } - _ => { - // Nothing + + ui.label(format!("Page {}", self.current_page)); + + if self.has_next_page { + if ui.button("Next Page").clicked() { + // Handle Next Page + if let Some(next_cursor) = &self.get_next_cursor() { + self.document_query_status = DocumentQueryStatus::WaitingForResult( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(), + ); + if self.current_page > 1 { + self.previous_cursors.push( + self.next_cursors + .get(self.next_cursors.len() - 2) + .unwrap() + .clone(), + ); + } + self.current_page += 1; + let parsed_query = self.build_document_query_with_cursor(next_cursor); + action = AppAction::BackendTask(BackendTask::DocumentTask( + DocumentTask::FetchDocumentsPage(parsed_query), + )); + } + } } - } - }); + }); + } + + action } fn show_filtered_docs(&mut self, ui: &mut egui::Ui, display_mode: DocumentDisplayMode) { @@ -401,8 +516,19 @@ impl ScreenLike for DocumentQueryScreen { .collect(); self.document_query_status = DocumentQueryStatus::Complete; } + BackendTaskSuccessResult::PageDocuments(page_docs, next_cursor) => { + self.matching_documents = page_docs + .iter() + .filter_map(|(_, doc)| doc.clone()) + .collect(); + self.has_next_page = next_cursor.is_some(); + if let Some(cursor) = next_cursor { + self.next_cursors.push(cursor.clone()); + } + self.document_query_status = DocumentQueryStatus::Complete; + } _ => { - // Nothing + // Handle other variants } } } @@ -455,7 +581,7 @@ impl ScreenLike for DocumentQueryScreen { ) .show(ctx, |ui| { action |= self.show_input_field(ui); - self.show_output(ui); + action |= self.show_output(ui); if self.confirm_remove_contract_popup { action |= self.show_remove_contract_popup(ui); From 1a469f731418a5d288b0d123e7187bef6d71a3c2 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Fri, 27 Dec 2024 13:33:02 -0500 Subject: [PATCH 16/16] fix unwrap --- src/ui/contracts_documents/document_query_screen.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ui/contracts_documents/document_query_screen.rs b/src/ui/contracts_documents/document_query_screen.rs index edd4d9eb7..6bb3b8bef 100644 --- a/src/ui/contracts_documents/document_query_screen.rs +++ b/src/ui/contracts_documents/document_query_screen.rs @@ -373,7 +373,8 @@ impl DocumentQueryScreen { .as_secs(), ); self.current_page = 1; - let next_cursor = self.get_next_cursor().unwrap(); + let next_cursor = + self.get_next_cursor().unwrap_or(Start::StartAfter(vec![])); // Doesn't matter what the value is let parsed_query = self.build_document_query_with_cursor(&next_cursor); action = AppAction::BackendTask(BackendTask::DocumentTask( DocumentTask::FetchDocumentsPage(parsed_query), @@ -398,7 +399,7 @@ impl DocumentQueryScreen { self.previous_cursors.push( self.next_cursors .get(self.next_cursors.len() - 2) - .unwrap() + .expect("Expected a previous cursor") .clone(), ); }