From abe3ac97b582068f5c2de8fe12bcd5b56d3ce9b399517b76f05ae65a025a76be Mon Sep 17 00:00:00 2001 From: Alex Wied <2+alex@noreply.code.graphofliberty.org> Date: Mon, 31 Aug 2026 13:45:15 -0400 Subject: [PATCH] . --- publish/src/app.rs | 273 +++++++++++++++++++++++-------- publish/src/main.rs | 1 - publish/src/window/inferences.rs | 106 ------------ publish/src/window/mod.rs | 1 - 4 files changed, 208 insertions(+), 173 deletions(-) delete mode 100644 publish/src/window/inferences.rs delete mode 100644 publish/src/window/mod.rs diff --git a/publish/src/app.rs b/publish/src/app.rs index e0be33b..a9db664 100644 --- a/publish/src/app.rs +++ b/publish/src/app.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::ops::Sub; use crate::navigator::Navigator; use crate::rdf::term_helper::{TermHelper, TermHelperMut}; @@ -12,7 +12,7 @@ use iced::keyboard::{Event, key}; use iced::widget::button::Style; use iced::widget::{button, center, column, combo_box, container, grid, mouse_area, opaque, operation, pick_list, row, scrollable, space, stack, table, text, text_input, toggler}; use iced::window::Settings; -use iced::{Background, Color, Element, Length, Subscription, Task, color, keyboard, window}; +use iced::{color, keyboard, window, Background, Color, Element, Font, Length, Subscription, Task, font}; use iced::advanced::text::Wrapping; use iced::widget::grid::Sizing; use ldp::middleware::BasicAuthMiddleware; @@ -21,7 +21,7 @@ use ldp::reqwest::{Client, Url}; use ldp::reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions}; use oxigraph::io::RdfFormat; -use oxigraph::model::{BaseDirection, Graph, NamedNode, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term, TermRef, Triple, TripleRef}; +use oxigraph::model::{BaseDirection, Graph, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term, TermRef, Triple, TripleRef}; use tracing::{debug, error, trace}; use gl_graph::class::Class; use gl_graph::ontology::{ConnectedOntology, OntologyBuilder, ReadOnlyEntity, ResourceDescription}; @@ -77,6 +77,7 @@ pub(crate) enum Message { RunInference, SetInferredTriples(Graph), ShowInferencesWindow, + ShowReadOnlyWindow, Event(Event), } @@ -110,6 +111,7 @@ pub(crate) struct Publisher { resource_descriptions: HashMap, window_id: window::Id, inference_window_id: Option, + read_only_window_id: Option, url_input: String, url_input_valid: bool, navigator: Navigator, @@ -167,6 +169,7 @@ impl Publisher { resource_descriptions: HashMap::new(), window_id: id, inference_window_id: None, + read_only_window_id: None, url_input: starting_url.to_string(), url_input_valid: true, navigator, @@ -237,8 +240,10 @@ impl Publisher { Message::WindowClosed(id) => { if self.window_id == id { task = iced::exit(); - } else { + } else if let Some(search_state) = &self.search_state && search_state.window_id == id { self.search_state = None; + } else if let Some(infrence_window_id) = self.inference_window_id && infrence_window_id == id { + self.inference_window_id = None; } } Message::URLInputChanged(value) => { @@ -633,6 +638,15 @@ impl Publisher { task = window_task.map(|_| Message::None); } } + Message::ShowReadOnlyWindow => { + if let Some(id) = self.read_only_window_id { + task = window::gain_focus(id); + } else { + let (id, window_task) = window::open(Settings::default()); + self.read_only_window_id = Some(id); + task = window_task.map(|_| Message::None); + } + } Message::Event(Event::KeyPressed { key: keyboard::Key::Named(key::Named::Tab), modifiers, @@ -654,6 +668,8 @@ impl Publisher { "Graph of Liberty Publisher - Inferences".to_string() } else if let Some(search_state) = &self.search_state && window == search_state.window_id { "Graph of Liberty Publisher - Search".to_string() + } else if let Some(read_only_window_id) = &self.read_only_window_id { + "Graph of Liberty Publisher - Read Only Triples".to_string() } else { "Graph of Liberty Publisher".to_string() } @@ -666,6 +682,186 @@ impl Publisher { ]) } + fn view_inferred_class_table<'a>(&'a self, triples: impl IntoIterator>) -> Element<'a, Message> { + let bold = |t| text(t).font(Font { weight: font::Weight::Bold, ..Font::default() }); + + let subject_column = table::column(bold("Subject"), |(subject, _): (NamedNodeRef, _)| { + text(self.curie_helper + .abbreviate(Some(self.document.origin()), subject.as_str()) + .unwrap_or_else(|| subject.as_str().to_string())) + }); + + let class_column = table::column(bold("Classes"), |(_, classes): (_, BTreeSet)| { + let textual_classes = classes.iter() + .map(|class| { + let curie = self.curie_helper + .abbreviate(Some(self.document.origin()), class.as_str()) + .unwrap_or_else(|| class.as_str().to_string()); + + let label = self.resource_descriptions + .get(&class.into_owned()) + .and_then(|description| description.label.clone()); + + if let Some(label) = label { + format!("{curie} ({label})") + } else { + curie + } + }).collect::>() + .join("\n"); + + text(textual_classes) + }); + + let mut inferred_classes: BTreeMap> = BTreeMap::new(); + for triple in triples { + if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject && + let TermRef::NamedNode(class) = triple.object { + inferred_classes.entry(subject) + .and_modify(|classes| { classes.insert(class); }) + .or_insert(BTreeSet::from_iter([class])); + } + } + + table([subject_column, class_column], inferred_classes).into() + } + + fn view_triple_table<'a>(&'a self, triples: impl IntoIterator>) -> Element<'a, Message> { + let bold = |t| text(t).font(Font { weight: font::Weight::Bold, ..Font::default() }); + + let subject_column = table::column(bold("Subject"), |triple: TripleRef| { + let curie = if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject { + Some(self.curie_helper + .abbreviate(Some(self.document.origin()), subject.as_str()) + .unwrap_or_else(|| subject.as_str().to_string())) + } else { + None + }; + + curie.map(text) + }); + + let predicate_column = table::column(bold("Predicate"), |triple: TripleRef| { + let curie = self.curie_helper + .abbreviate(None, triple.predicate.as_str()) + .unwrap_or_else(|| triple.predicate.as_str().to_string()); + + let label = self.resource_descriptions + .get(&triple.predicate.into_owned()) + .and_then(|description| description.label.clone()); + + if let Some(label) = label { + text(format!("{curie} ({label})")) + } else { + text(curie) + } + }); + + let object_column = table::column(bold("Object"), |triple: TripleRef| { + match triple.object { + TermRef::NamedNode(node) => { + let curie = self.curie_helper + .abbreviate(Some(self.document.origin()), node.as_str()) + .unwrap_or_else(|| node.as_str().to_string()); + + let label = self.resource_descriptions + .get(&node.into_owned()) + .and_then(|description| description.label.clone()); + + if let Some(label) = label { + text(format!("{curie} ({label})")) + } else { + text(curie) + } + } + _ => text(triple.object.to_string()), + } + }); + + table([subject_column, predicate_column, object_column], triples).into() + } + + fn view_search_window<'a>(&'a self, search_state: &'a SearchState) -> Element<'a, Message> { + let search_input = text_input("Query", &search_state.query) + .on_input(Message::QueryUpdated) + .id("query"); + + let type_selector = pick_list( + search_state.entity_class_selection.as_ref(), + Class::ALL, + ToString::to_string, + ).on_select(Message::UpdateQueryClass); + + let mut columns = vec![table::column(text("CURIE"), |document: &HashMap| { + let iri = document.get(&Schema::iri_field()) + .and_then(|value| value.as_str()) + .unwrap_or_default(); + + let abbreviated_iri = document.get(&Schema::curie_field()) + .and_then(|value| value.as_str()) + .unwrap_or_default(); + + button(text(abbreviated_iri).wrapping(Wrapping::Word)) + .on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri))) + .style(button::text) + })]; + + let fields = search_state.entity_class_selection + .as_ref() + .map(|class| { + class.fields(language::ENGLISH_TAG.clone()) + }).unwrap_or_default(); + + for (field, label) in fields { + columns.push( + table::column(text(label), move |document: &HashMap| { + let iri = document.get(&Schema::iri_field()) + .and_then(|value| value.as_str()) + .unwrap_or_default(); + + let value = document + .get(&field) + .and_then(|value| value.as_str()) + .unwrap_or_default(); + + button(text(value).wrapping(Wrapping::Word)) + .on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri))) + .style(button::text) + }) + .width(Length::Fixed(256.0)), + ); + } + + let results_table = if columns.is_empty() { + None + } else { + Some(scrollable(table(columns, &search_state.results))) + }; + + column![row![search_input, type_selector], results_table].into() + } + + fn view_inference_window(&self) -> Element<'_, Message> { + let classes = self.inferred_triples.triples_for_predicate(vocab::rdf::TYPE); + + column![ + self.view_inferred_class_table(classes), + scrollable(self.view_triple_table(&self.inferred_triples)), + ].into() + } + + fn view_read_only_window(&self) -> Element<'_, Message> { + let read_only_triples = self.document + .dataset() + .iter_both() + .filter_map(|(_, quad, state)| if state.read_only { Some(quad) } else { None }) + .map(|quad| TripleRef::from(quad.as_ref())); + + column![ + scrollable(self.view_triple_table(read_only_triples)), + ].into() + } + fn view_row<'a>( &'a self, key: QuadKey, @@ -835,66 +1031,6 @@ impl Publisher { .into() } - fn view_search_window<'a>(&'a self, search_state: &'a SearchState) -> Element<'a, Message> { - let search_input = text_input("Query", &search_state.query) - .on_input(Message::QueryUpdated) - .id("query"); - - let type_selector = pick_list( - search_state.entity_class_selection.as_ref(), - Class::ALL, - ToString::to_string, - ).on_select(Message::UpdateQueryClass); - - let mut columns = vec![table::column(text("CURIE"), |document: &HashMap| { - let iri = document.get(&Schema::iri_field()) - .and_then(|value| value.as_str()) - .unwrap_or_default(); - - let abbreviated_iri = document.get(&Schema::curie_field()) - .and_then(|value| value.as_str()) - .unwrap_or_default(); - - button(text(abbreviated_iri).wrapping(Wrapping::Word)) - .on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri))) - .style(button::text) - })]; - - let fields = search_state.entity_class_selection - .as_ref() - .map(|class| { - class.fields(language::ENGLISH_TAG.clone()) - }).unwrap_or_default(); - - for (field, label) in fields { - columns.push( - table::column(text(label), move |document: &HashMap| { - let iri = document.get(&Schema::iri_field()) - .and_then(|value| value.as_str()) - .unwrap_or_default(); - - let value = document - .get(&field) - .and_then(|value| value.as_str()) - .unwrap_or_default(); - - button(text(value).wrapping(Wrapping::Word)) - .on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri))) - .style(button::text) - }) - .width(Length::Fixed(256.0)), - ); - } - - let results_table = if columns.is_empty() { - None - } else { - Some(scrollable(table(columns, &search_state.results))) - }; - - column![row![search_input, type_selector], results_table].into() - } - pub(crate) fn view(&self, window: window::Id) -> Element<'_, Message> { if let Some(search_state) = &self.search_state && search_state.window_id == window { @@ -902,10 +1038,15 @@ impl Publisher { } if let Some(inference_window_id) = self.inference_window_id && window == inference_window_id { - return crate::window::inferences::view_inference_window(&self.resource_descriptions, self.document.origin(), &self.curie_helper, &self.inferred_triples); + return self.view_inference_window(); + } + + if let Some(read_only_window_id) = self.read_only_window_id && window == read_only_window_id { + return self.view_read_only_window(); } let show_inferences_button = button("\u{1f9e0}").on_press(Message::ShowInferencesWindow); + let show_read_only_button = button("\u{1f6ab}").on_press(Message::ShowReadOnlyWindow); let back_button = button("\u{1f870}").on_press(Message::NavigateBack); let forward_button = button("\u{1f872}").on_press(Message::NavigateForward); let new_document_button = button("New Document").on_press(Message::ShowNewDocumentButtons); @@ -923,6 +1064,7 @@ impl Publisher { rows = self.document .dataset() .iter_both() + .filter(|(_, _, state)| !state.read_only) .map(|(key, quad, state)| self.view_row(key, quad, state)) .collect(); @@ -955,6 +1097,7 @@ impl Publisher { let content = navigation_area(column![ row![ show_inferences_button, + show_read_only_button, new_document_button, add_row_button, back_button, diff --git a/publish/src/main.rs b/publish/src/main.rs index 45a3a49..89241e2 100644 --- a/publish/src/main.rs +++ b/publish/src/main.rs @@ -6,7 +6,6 @@ mod rdf; mod theme; mod widget; mod tasks; -pub mod window; use std::collections::HashMap; use crate::app::Publisher; diff --git a/publish/src/window/inferences.rs b/publish/src/window/inferences.rs deleted file mode 100644 index 3258564..0000000 --- a/publish/src/window/inferences.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap}; -use iced::{font, Element, Font}; -use iced::widget::{column, scrollable, span, table, text}; -use oxigraph::model::{Graph, NamedNode, NamedNodeRef, NamedOrBlankNodeRef, TermRef, TripleRef}; -use url::Url; -use gl_graph::{vocab, CurieHelper}; -use gl_graph::ontology::ResourceDescription; -use crate::app::Message; - -pub(crate) fn view_inference_window<'a>(resource_descriptions: &HashMap, origin: &Url, curie_helper: &CurieHelper, inferences: &'a Graph) -> Element<'a, Message> { - let bold = |t| text(t).font(Font { weight: font::Weight::Bold, ..Font::default() }); - - let inferred_class_subject_column = table::column(bold("Subject"), |(subject, _): (NamedNodeRef, _)| { - text(curie_helper - .abbreviate(Some(origin), subject.as_str()) - .unwrap_or_else(|| subject.as_str().to_string())) - }); - - let inferred_class_class_column = table::column(bold("Classes"), |(_, classes): (_, BTreeSet)| { - let textual_classes = classes.iter() - .map(|class| { - let curie = curie_helper - .abbreviate(Some(origin), class.as_str()) - .unwrap_or_else(|| class.as_str().to_string()); - - let label = resource_descriptions - .get(&class.into_owned()) - .and_then(|description| description.label.clone()); - - if let Some(label) = label { - format!("{curie} ({label})") - } else { - curie - } - }).collect::>() - .join("\n"); - - text(textual_classes) - }); - - let mut inferred_classes: BTreeMap> = BTreeMap::new(); - for triple in inferences.triples_for_predicate(vocab::rdf::TYPE) { - if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject && let TermRef::NamedNode(class) = triple.object { - inferred_classes.entry(subject) - .and_modify(|classes| { classes.insert(class); }) - .or_insert(BTreeSet::from_iter([class])); - } - } - let inferred_class_table = table([inferred_class_subject_column, inferred_class_class_column], inferred_classes); - - let subject_column = table::column(bold("Subject"), |triple: TripleRef| { - let curie = if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject { - Some(curie_helper - .abbreviate(Some(origin), subject.as_str()) - .unwrap_or_else(|| subject.as_str().to_string())) - } else { - None - }; - - curie.map(text) - }); - - let predicate_column = table::column(bold("Predicate"), |triple: TripleRef| { - let curie = curie_helper - .abbreviate(None, triple.predicate.as_str()) - .unwrap_or_else(|| triple.predicate.as_str().to_string()); - - let label = resource_descriptions - .get(&triple.predicate.into_owned()) - .and_then(|description| description.label.clone()); - - if let Some(label) = label { - text(format!("{curie} ({label})")) - } else { - text(curie) - } - }); - - let object_column = table::column(bold("Object"), |triple: TripleRef| { - match triple.object { - TermRef::NamedNode(node) => { - let curie = curie_helper - .abbreviate(Some(origin), node.as_str()) - .unwrap_or_else(|| node.as_str().to_string()); - - let label = resource_descriptions - .get(&node.into_owned()) - .and_then(|description| description.label.clone()); - - if let Some(label) = label { - text(format!("{curie} ({label})")) - } else { - text(curie) - } - } - _ => text(triple.object.to_string()), - } - }); - - let all_inferences_table = table([subject_column, predicate_column, object_column], inferences); - - column![ - inferred_class_table, - scrollable(all_inferences_table), - ].into() -} \ No newline at end of file diff --git a/publish/src/window/mod.rs b/publish/src/window/mod.rs deleted file mode 100644 index a3c410b..0000000 --- a/publish/src/window/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod inferences; \ No newline at end of file