This commit is contained in:
2026-08-08 23:50:04 -04:00
parent 9177c8902b
commit 79894f654d
26 changed files with 748 additions and 411 deletions
+148 -97
View File
@@ -3,6 +3,8 @@ use crate::rdf::ontology::{LabeledIri, Ontology};
use crate::rdf::term_helper::{TermHelper, TermHelperMut};
use crate::widget::iri_input::iri_input;
use crate::widget::navigation_area::navigation_area;
use gl_graph::CurieHelper;
use gl_graph::inference::InferenceEngine;
use gl_search::language;
use gl_search::{Schema, SearchDocument, SearchIndex, Value};
use http::StatusCode;
@@ -11,7 +13,10 @@ use iced::keyboard::{Event, key};
use iced::widget::button::Style;
use iced::widget::grid::Sizing;
use iced::widget::text::Wrapping;
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::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 ldp::middleware::BasicAuthMiddleware;
@@ -21,11 +26,12 @@ use ldp::reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions};
use oxigraph::io::RdfFormat;
use oxigraph::model::vocab::{rdf, rdfs};
use oxigraph::model::{BaseDirection, Dataset, Graph, NamedNode, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term, TripleRef};
use oxigraph::model::{
BaseDirection, Dataset, Graph, NamedNode, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term,
TripleRef,
};
use oxigraph::store::Store;
use tracing::{debug_span, error, trace};
use gl_graph::CurieHelper;
use gl_graph::inference::InferenceEngine;
#[derive(Clone, Debug)]
pub(crate) enum Message {
@@ -139,9 +145,12 @@ impl Publisher {
let mut abbreviated_datatypes = ontology
.datatypes()
.into_iter()
.map(|node| curie_helper.abbreviate(None, node.as_str())
.unwrap_or(node.as_str().to_string())
).collect::<Vec<_>>();
.map(|node| {
curie_helper
.abbreviate(None, node.as_str())
.unwrap_or(node.as_str().to_string())
})
.collect::<Vec<_>>();
abbreviated_datatypes.sort();
let (id, task) = window::open(Settings::default());
@@ -246,21 +255,15 @@ impl Publisher {
});
}
Message::LoadDocument(document) => {
let messages = document
.dataset()
.quads
.iter()
.map(|(key, quad)| {
let datatype_state = combo_box::State::new(
self.abbreviated_datatypes.clone(),
);
let messages = document.dataset().quads.iter().map(|(key, quad)| {
let datatype_state = combo_box::State::new(self.abbreviated_datatypes.clone());
let state = RowState {
read_only: self.ontology.is_read_only(quad.as_ref()),
datatype_state,
};
Message::AddRow(Some((key, state)))
});
let state = RowState {
read_only: self.ontology.is_read_only(quad.as_ref()),
datatype_state,
};
Message::AddRow(Some((key, state)))
});
task = Task::batch(messages.map(Task::done))
.chain(Task::done(Message::RunInference))
.chain(Task::done(Message::ResetState));
@@ -277,9 +280,7 @@ impl Publisher {
quad.object = Term::NamedNode(empty_node);
let key = self.document.dataset_mut().quads.insert(quad);
let datatype_state = combo_box::State::new(
self.abbreviated_datatypes.clone(),
);
let datatype_state = combo_box::State::new(self.abbreviated_datatypes.clone());
let state = RowState {
read_only: false,
datatype_state,
@@ -306,14 +307,14 @@ impl Publisher {
let selected_entity_class = match action {
SearchResultClickAction::Predicate(_) => Some(rdf::PROPERTY),
SearchResultClickAction::Object(key) => {
self.document
.dataset()
.quads
.get(key)
.and_then(|quad| if quad.predicate == rdf::TYPE {
self.document.dataset().quads.get(key).and_then(|quad| {
if quad.predicate == rdf::TYPE {
Some(rdfs::CLASS)
} else { None })
},
} else {
None
}
})
}
SearchResultClickAction::URLInput => None,
};
@@ -332,7 +333,10 @@ impl Publisher {
}
if let Some(selected_entity_class) = selected_entity_class {
let selection = self.ontology.info(&selected_entity_class.into_owned(), &*language::ENGLISH_OR_UNTAGGED);
let selection = self.ontology.info(
&selected_entity_class.into_owned(),
&*language::ENGLISH_OR_UNTAGGED,
);
task = task.chain(Task::done(Message::QueryTypeUpdated(selection)));
}
}
@@ -344,13 +348,16 @@ impl Publisher {
}
Message::QueryUpdated(new_query) => {
if let Some(search_state) = &mut self.search_state {
let category_id = search_state.entity_selection
let category_id = search_state
.entity_selection
.clone()
.and_then(|info| self.ontology.category_id(&info.iri));
let query = new_query.clone();
search_state.query = new_query;
let results = self.index.query(category_id, query.as_str(), Schema::all_fields(), 25)
let results = self
.index
.query(category_id, query.as_str(), Schema::all_fields(), 25)
.expect("Unable to complete search");
task = Task::done(Message::SetSearchResults(results));
};
@@ -548,7 +555,8 @@ impl Publisher {
self.show_inferred_properties = value;
}
Message::RunInference => {
let dataset = self.document
let dataset = self
.document
.dataset()
.quads
.iter()
@@ -556,18 +564,19 @@ impl Publisher {
.collect();
let engine = self.inference_engine.clone();
task = Task::perform(tokio::task::spawn_blocking(move || {
engine.run(&dataset).unwrap()
}), |dataset| Message::SetInferredTriples(dataset.unwrap()));
task = Task::perform(
tokio::task::spawn_blocking(move || engine.run(&dataset).unwrap()),
|dataset| Message::SetInferredTriples(dataset.unwrap()),
);
}
Message::SetInferredTriples(dataset) => {
self.inferred_triples = Graph::from_iter(&dataset);
}
Message::Event(Event::KeyPressed {
key: keyboard::Key::Named(key::Named::Tab),
modifiers,
..
}) => {
key: keyboard::Key::Named(key::Named::Tab),
modifiers,
..
}) => {
if modifiers.shift() {
task = operation::focus_previous();
} else {
@@ -621,22 +630,34 @@ impl Publisher {
subject_input_base.on_input(move |value| Message::SubjectUpdated(key, value))
};
let predicate_input_base = iri_input(&self.curie_helper, "Predicate", Some(&base), triple.predicate.as_str());
let predicate_input_base = iri_input(
&self.curie_helper,
"Predicate",
Some(&base),
triple.predicate.as_str(),
);
let predicate_input = if self.ontology.is_read_only(triple.as_ref()) {
predicate_input_base
} else {
predicate_input_base.on_input(move |value| Message::PredicateUpdated(key, value))
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Predicate(key)))
predicate_input_base
.on_input(move |value| Message::PredicateUpdated(key, value))
.on_control_click(Message::OpenQueryWindow(
SearchResultClickAction::Predicate(key),
))
.on_shift_click(Message::NavigateToPredicate(key))
};
let predicate_info = self.ontology.info(&triple.predicate, &*language::ENGLISH_OR_UNTAGGED);
let predicate_info = self
.ontology
.info(&triple.predicate, &*language::ENGLISH_OR_UNTAGGED);
let predicate_label = container(text(predicate_info.label));
let term = TermHelper::new(&triple.object);
let value_label = term.value_as_named_node().and_then(|node| {
let info = self.ontology.info(&node.into_owned(), &*language::ENGLISH_OR_UNTAGGED);
let info = self
.ontology
.info(&node.into_owned(), &*language::ENGLISH_OR_UNTAGGED);
Some(container(text(info.label)))
});
@@ -660,7 +681,8 @@ impl Publisher {
if self.ontology.is_read_only(triple.as_ref()) {
base.into()
} else {
base.on_input(move |value| Message::ObjectUpdated(key, value)).into()
base.on_input(move |value| Message::ObjectUpdated(key, value))
.into()
}
} else {
let base = iri_input(&self.curie_helper, "Object", Some(&base), value.as_str())
@@ -670,12 +692,15 @@ impl Publisher {
} else {
base.align_x(value_alignment)
.on_input(move |value| Message::ObjectUpdated(key, value))
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Object(key)))
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Object(
key,
)))
.into()
}
};
let selected_datatype = term.datatype()
let selected_datatype = term
.datatype()
.and_then(|node| self.curie_helper.abbreviate(None, node.as_str()));
let datatype_selector: Element<Message> = if state.read_only {
@@ -690,7 +715,8 @@ impl Publisher {
.on_input(move |input| {
let value = if input.is_empty() { None } else { Some(input) };
Message::DatatypeUpdated(key, value)
}).into()
})
.into()
};
let language = term.language().unwrap_or("en").to_owned();
@@ -748,7 +774,9 @@ impl Publisher {
entities: impl IntoIterator<Item = LabeledIri>,
) -> Element<'_, Message> {
let buttons = entities.into_iter().map(|entity| {
let abbreviation = self.curie_helper.abbreviate(None, entity.iri.as_str())
let abbreviation = self
.curie_helper
.abbreviate(None, entity.iri.as_str())
.unwrap_or_else(|| entity.iri.as_str().to_string());
let label = format!("{} ({})", entity.label, abbreviation);
@@ -766,35 +794,41 @@ impl Publisher {
if let Some(search_state) = &self.search_state
&& search_state.window_id == window
{
let search_input =
text_input("Query", &search_state.query).on_input(Message::QueryUpdated)
.id("query");
let search_input = text_input("Query", &search_state.query)
.on_input(Message::QueryUpdated)
.id("query");
let entities = Vec::new(); /*self.ontology
.searchable_classes(&*language::ENGLISH_OR_UNTAGGED)
.into_iter()
.collect::<Vec<_>>();*/
.searchable_classes(&*language::ENGLISH_OR_UNTAGGED)
.into_iter()
.collect::<Vec<_>>();*/
let type_selector = pick_list(search_state.entity_selection.as_ref(), entities, ToString::to_string)
.on_select(|selection| Message::QueryTypeUpdated(selection));
let type_selector = pick_list(
search_state.entity_selection.as_ref(),
entities,
ToString::to_string,
)
.on_select(|selection| Message::QueryTypeUpdated(selection));
let mut columns = vec![
table::column(text("CURIE"), |document: &SearchDocument| {
let iri = document.get_first(Schema::iri_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
let mut columns = vec![table::column(text("CURIE"), |document: &SearchDocument| {
let iri = document
.get_first(Schema::iri_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
let abbreviated_iri = self.curie_helper.abbreviate(None, iri)
.unwrap_or_else(|| iri.to_string());
let abbreviated_iri = self
.curie_helper
.abbreviate(None, iri)
.unwrap_or_else(|| iri.to_string());
button(text(abbreviated_iri).wrapping(Wrapping::Word))
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
.style(button::text)
})
];
button(text(abbreviated_iri).wrapping(Wrapping::Word))
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
.style(button::text)
})];
let fields = if let Some(selection) = search_state.entity_selection.as_ref() {
self.ontology.fields_for_class(&selection.iri, &*language::ENGLISH_OR_UNTAGGED)
self.ontology
.fields_for_class(&selection.iri, &*language::ENGLISH_OR_UNTAGGED)
.expect("Unable to load fields for class")
} else {
vec![]
@@ -804,18 +838,21 @@ impl Publisher {
let field_name = field.name.clone();
columns.push(
table::column(text(field.label), move |document: &SearchDocument| {
let iri = document.get_first(Schema::iri_field())
let iri = document
.get_first(Schema::iri_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
let value = document.get_first(Schema::field(&field_name, Some(language::ENGLISH_PRIMARY)))
let value = document
.get_first(Schema::field(&field_name, Some(language::ENGLISH_PRIMARY)))
.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))
})
.width(Length::Fixed(256.0)),
);
}
@@ -825,10 +862,7 @@ impl Publisher {
Some(scrollable(table(columns, &search_state.results)))
};
return column![
row![search_input, type_selector],
results_table
].into();
return column![row![search_input, type_selector], results_table].into();
}
let back_button = button("\u{1f870}").on_press(Message::NavigateBack);
@@ -841,7 +875,9 @@ impl Publisher {
.on_input(Message::URLInputChanged)
.on_submit(Message::URLInputSubmitted)
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::URLInput))
.style(crate::theme::styles::text_input::required(!self.url_input_valid));
.style(crate::theme::styles::text_input::required(
!self.url_input_valid,
));
let mut rows: Vec<Element<Message>> = vec![];
rows = self
@@ -854,7 +890,9 @@ impl Publisher {
let body: Element<Message> = if self.show_new_document_buttons {
let subclasses = Vec::new(); // TODO self.ontology.subclasses_of(vocab::rda::ENTITY.into_owned());
let labeled_subclasses = subclasses.iter().map(|iri| self.ontology.info(iri, &*language::ENGLISH_OR_UNTAGGED));
let labeled_subclasses = subclasses
.iter()
.map(|iri| self.ontology.info(iri, &*language::ENGLISH_OR_UNTAGGED));
column![self.view_new_entity_buttons(labeled_subclasses)].into()
} else {
column(rows).into()
@@ -864,24 +902,38 @@ impl Publisher {
let subject_column = table::column("Subject", |triple: TripleRef| {
let curie = if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject {
self.curie_helper.abbreviate(None, subject.as_str())
} else { None };
} else {
None
};
curie.map(text)
});
let predicate_column = table::column("Predicate", |triple: TripleRef| {
let predicate_info = self.ontology.info(&triple.predicate.into_owned(), &*language::ENGLISH_OR_UNTAGGED);
let predicate_info = self.ontology.info(
&triple.predicate.into_owned(),
&*language::ENGLISH_OR_UNTAGGED,
);
let label = if predicate_info.label.is_empty() {
self.curie_helper
.abbreviate(None, triple.predicate.as_str())
.unwrap_or(triple.predicate.as_str().to_string())
} else { predicate_info.label };
} else {
predicate_info.label
};
text(label)
});
let object_column = table::column("Object", |triple: TripleRef| text(triple.object.to_string()));
Some(table([subject_column, predicate_column, object_column], self.inferred_triples.iter()))
} else { None };
let object_column = table::column("Object", |triple: TripleRef| {
text(triple.object.to_string())
});
Some(table(
[subject_column, predicate_column, object_column],
self.inferred_triples.iter(),
))
} else {
None
};
let save_button_base = button(text("\u{1f4be}"));
let save_button = if self.modified {
@@ -890,14 +942,13 @@ impl Publisher {
save_button_base
};
let inferred_type_toggle = toggler(self.show_inferred_types)
.on_toggle(Message::SetInferredTypes);
let inferred_type_toggle =
toggler(self.show_inferred_types).on_toggle(Message::SetInferredTypes);
let inferred_property_toggle = toggler(self.show_inferred_properties)
.on_toggle(Message::SetInferredProperties);
let inferred_property_toggle =
toggler(self.show_inferred_properties).on_toggle(Message::SetInferredProperties);
let read_only_toggle = toggler(self.show_read_only)
.on_toggle(Message::SetReadOnly);
let read_only_toggle = toggler(self.show_read_only).on_toggle(Message::SetReadOnly);
let footer = row![
space::horizontal(),
@@ -923,8 +974,8 @@ impl Publisher {
scrollable(body),
scrollable(inference_table),
])
.on_back_click(Message::NavigateBack)
.on_forward_click(Message::NavigateForward);
.on_back_click(Message::NavigateBack)
.on_forward_click(Message::NavigateForward);
if self.show_overwrite_confirmation {
let confirmation_modal = container(column![
@@ -973,4 +1024,4 @@ where
)
]
.into()
}
}