Files
tools/publish/src/app.rs
T

1028 lines
39 KiB
Rust
Raw Normal View History

2026-08-05 11:21:25 -04:00
use crate::navigator::Navigator;
use crate::rdf::ontology::{LabeledIri, Ontology};
2026-06-08 19:33:49 -04:00
use crate::rdf::term_helper::{TermHelper, TermHelperMut};
2026-08-05 11:21:25 -04:00
use crate::widget::iri_input::iri_input;
use crate::widget::navigation_area::navigation_area;
2026-08-08 23:50:04 -04:00
use gl_graph::CurieHelper;
use gl_graph::inference::InferenceEngine;
2026-08-05 11:21:25 -04:00
use gl_search::language;
2026-08-05 18:33:30 -04:00
use gl_search::{Schema, SearchDocument, SearchIndex, Value};
2026-06-08 19:33:49 -04:00
use http::StatusCode;
use iced::alignment::Horizontal;
2026-08-05 11:21:25 -04:00
use iced::keyboard::{Event, key};
2026-06-08 19:33:49 -04:00
use iced::widget::button::Style;
use iced::widget::grid::Sizing;
2026-08-05 11:21:25 -04:00
use iced::widget::text::Wrapping;
2026-08-08 23:50:04 -04:00
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,
};
2026-06-08 19:33:49 -04:00
use iced::window::Settings;
2026-08-05 11:21:25 -04:00
use iced::{Background, Color, Element, Length, Subscription, Task, color, keyboard, window};
2026-06-08 19:33:49 -04:00
use ldp::middleware::BasicAuthMiddleware;
use ldp::model::{KeyedDataset, QuadKey};
use ldp::reqwest::{Client, Url};
use ldp::reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions};
use oxigraph::io::RdfFormat;
2026-06-09 20:09:16 -04:00
use oxigraph::model::vocab::{rdf, rdfs};
2026-08-08 23:50:04 -04:00
use oxigraph::model::{
BaseDirection, Dataset, Graph, NamedNode, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term,
TripleRef,
};
2026-08-07 19:28:08 -04:00
use oxigraph::store::Store;
2026-08-05 11:21:25 -04:00
use tracing::{debug_span, error, trace};
2026-06-08 19:33:49 -04:00
2026-06-30 19:25:15 -04:00
#[derive(Clone, Debug)]
2026-06-08 19:33:49 -04:00
pub(crate) enum Message {
None,
WindowClosed(window::Id),
URLInputChanged(String),
URLInputSubmitted,
2026-07-14 21:31:15 -04:00
NavigateTo(Url),
2026-06-08 19:33:49 -04:00
FetchDocument(Url),
LoadDocument(RdfSource<KeyedDataset<RowState>>),
ShowNewDocumentButtons,
HideNewDocumentButtons,
ShowError(String),
2026-08-07 19:28:08 -04:00
AddRow(Option<(QuadKey, RowState)>),
2026-06-08 19:33:49 -04:00
DeleteRow(QuadKey),
DeleteAllRows,
OpenQueryWindow(SearchResultClickAction),
HoverRow(QuadKey),
UnhoverRow(QuadKey),
QueryUpdated(String),
2026-06-30 19:25:15 -04:00
SetSearchResults(Vec<SearchDocument>),
2026-07-01 21:28:12 -04:00
QueryTypeUpdated(LabeledIri),
2026-06-08 19:33:49 -04:00
SearchResultClicked(NamedNode),
DatatypeUpdated(QuadKey, Option<String>),
LanguageUpdated(QuadKey, Option<String>),
2026-07-24 19:25:07 -04:00
SubjectUpdated(QuadKey, String),
2026-07-08 21:07:52 -04:00
PredicateUpdated(QuadKey, String),
ObjectUpdated(QuadKey, String),
2026-06-08 19:33:49 -04:00
DirectionToggled(QuadKey, BaseDirection),
SaveGraph(bool),
ShowOverwriteConfirmationModal,
HideOverwriteConfirmationModal,
ConfirmOverwrite,
NewDocument(NamedNode),
2026-07-12 13:52:13 -04:00
NavigateToPredicate(QuadKey),
NavigateToObject(QuadKey),
2026-07-14 21:31:15 -04:00
NavigateBack,
NavigateForward,
2026-06-08 19:33:49 -04:00
ResetState,
2026-07-30 12:40:22 -04:00
SetReadOnly(bool),
2026-08-07 19:28:08 -04:00
SetInferredTypes(bool),
SetInferredProperties(bool),
RunInference,
SetInferredTriples(Dataset),
2026-08-05 11:21:25 -04:00
Event(Event),
2026-06-08 19:33:49 -04:00
}
#[derive(Default, Debug, Clone)]
pub(crate) struct RowState {
read_only: bool,
datatype_state: combo_box::State<String>,
}
#[derive(Debug, Clone)]
pub(crate) enum SearchResultClickAction {
URLInput,
Predicate(QuadKey),
Object(QuadKey),
}
struct SearchState {
window_id: window::Id,
action: SearchResultClickAction,
2026-07-10 12:20:51 -04:00
entity_selection: Option<LabeledIri>,
2026-06-08 19:33:49 -04:00
query: String,
2026-06-30 19:25:15 -04:00
results: Vec<SearchDocument>,
2026-06-08 19:33:49 -04:00
}
pub(crate) struct Publisher {
http_client: ClientWithMiddleware,
2026-07-13 14:05:40 -04:00
curie_helper: CurieHelper,
2026-06-08 19:33:49 -04:00
ontology: Ontology,
2026-08-07 19:28:08 -04:00
inference_engine: InferenceEngine,
2026-06-08 19:33:49 -04:00
abbreviated_datatypes: Vec<String>,
window_id: window::Id,
url_input: String,
2026-07-28 15:09:33 -04:00
url_input_valid: bool,
2026-07-14 21:31:15 -04:00
navigator: Navigator<Url>,
2026-06-08 19:33:49 -04:00
document: RdfSource<KeyedDataset<RowState>>,
2026-08-07 19:28:08 -04:00
inferred_triples: Graph,
2026-07-30 12:40:22 -04:00
show_read_only: bool,
2026-08-07 19:28:08 -04:00
show_inferred_types: bool,
show_inferred_properties: bool,
2026-06-08 19:33:49 -04:00
hovered_row: Option<QuadKey>,
search_state: Option<SearchState>,
index: SearchIndex,
2026-07-20 23:53:33 -04:00
traversal: Option<Dataset>,
2026-06-08 19:33:49 -04:00
show_overwrite_confirmation: bool,
modified: bool,
show_new_document_buttons: bool,
}
impl Publisher {
pub(crate) fn new() -> (Self, Task<Message>) {
2026-07-13 14:05:40 -04:00
let curie_helper = CurieHelper::new(Ontology::prefixes().clone());
2026-06-30 19:25:15 -04:00
let ontology = debug_span!("Ontology Creation").in_scope(|| {
2026-06-29 15:20:02 -04:00
Ontology::builder()
.with_path("/home/alex/.local/share/org.graphofliberty.desktop/ontology")
.build()
.expect("Failed to build ontology")
});
2026-06-08 19:33:49 -04:00
2026-08-07 19:28:08 -04:00
let inference_engine = InferenceEngine::new(ontology.store().clone());
2026-06-30 19:25:15 -04:00
let index = SearchIndex::builder()
2026-07-02 14:09:54 -04:00
.with_path("/home/alex/.local/share/org.graphofliberty.desktop/index")
2026-06-30 19:25:15 -04:00
.build()
.expect("Failed to build search index");
2026-06-08 19:33:49 -04:00
let mut abbreviated_datatypes = ontology
.datatypes()
2026-07-10 23:14:50 -04:00
.into_iter()
2026-08-08 23:50:04 -04:00
.map(|node| {
curie_helper
.abbreviate(None, node.as_str())
.unwrap_or(node.as_str().to_string())
})
.collect::<Vec<_>>();
2026-06-08 19:33:49 -04:00
abbreviated_datatypes.sort();
let (id, task) = window::open(Settings::default());
let client = Client::new();
let http_client = ClientBuilder::new(client.clone())
.with(BasicAuthMiddleware::new(
"fedoraAdmin".to_string(),
Some("fedoraAdmin".to_string()),
))
.build();
2026-07-14 21:31:15 -04:00
let starting_url = Url::parse("http://fedora.quill.lan/rest/").unwrap();
let navigator = Navigator::new(starting_url.clone());
let document = RdfSource::new(starting_url.clone());
2026-06-08 19:33:49 -04:00
(
Self {
http_client,
2026-07-13 14:05:40 -04:00
curie_helper,
2026-06-08 19:33:49 -04:00
ontology,
2026-08-07 19:28:08 -04:00
inference_engine,
2026-06-08 19:33:49 -04:00
abbreviated_datatypes,
window_id: id,
2026-07-14 21:31:15 -04:00
url_input: starting_url.to_string(),
2026-07-28 15:09:33 -04:00
url_input_valid: true,
2026-07-14 21:31:15 -04:00
navigator,
2026-06-08 19:33:49 -04:00
document,
2026-08-07 19:28:08 -04:00
inferred_triples: Graph::new(),
2026-07-30 12:40:22 -04:00
show_read_only: false,
2026-08-07 19:28:08 -04:00
show_inferred_types: false,
show_inferred_properties: false,
2026-06-08 19:33:49 -04:00
hovered_row: None,
search_state: None,
index,
2026-07-13 14:05:40 -04:00
traversal: None,
2026-06-08 19:33:49 -04:00
show_overwrite_confirmation: false,
modified: false,
show_new_document_buttons: false,
},
task.map(|_| Message::None),
)
}
pub(crate) fn update(&mut self, message: Message) -> Task<Message> {
let mut task = Task::none();
2026-06-30 19:25:15 -04:00
trace!(?message);
2026-06-08 19:33:49 -04:00
match message {
Message::WindowClosed(id) => {
if self.window_id == id {
task = iced::exit();
} else {
self.search_state = None;
}
}
Message::URLInputChanged(value) => {
2026-07-28 15:09:33 -04:00
if Url::parse(&value).is_ok() {
self.url_input_valid = true;
} else {
self.url_input_valid = false;
}
2026-06-08 19:33:49 -04:00
self.url_input = value;
}
Message::URLInputSubmitted => {
2026-07-30 12:40:22 -04:00
if let Ok(url) = Url::parse(&self.url_input) {
if &url != self.navigator.current() {
self.navigator.goto(url.clone());
}
task = Task::done(Message::NavigateTo(url));
2026-07-20 23:53:33 -04:00
}
2026-07-14 21:31:15 -04:00
}
Message::NavigateTo(url) => {
2026-06-08 19:33:49 -04:00
task = Task::done(Message::DeleteAllRows)
.chain(Task::done(Message::ResetState))
.chain(Task::done(Message::FetchDocument(url)));
}
Message::FetchDocument(url) => {
let client = self.http_client.clone();
task = Task::future(async {
let request = ResourceRequestBuilder::with_client_and_url(client, url)
.accept_rdf_format(RdfFormat::Turtle)
.follow_described_by(true)
.build();
match request.send().await {
Ok(resource) => match resource.into_rdf_source().await {
Ok(rdf_source) => Message::LoadDocument(rdf_source),
Err(err) => {
Message::ShowError(format!("Unable to parse response: {err}"))
}
},
Err(ldp::Error::Reqwest(err)) => {
if err.status() == Some(StatusCode::NOT_FOUND) {
Message::ShowNewDocumentButtons
} else {
Message::ShowError(err.to_string())
}
}
Err(err) => Message::ShowError(format!("Request failed: {err}")),
}
});
}
Message::LoadDocument(document) => {
2026-08-08 23:50:04 -04:00
let messages = document.dataset().quads.iter().map(|(key, quad)| {
let datatype_state = combo_box::State::new(self.abbreviated_datatypes.clone());
2026-08-07 19:28:08 -04:00
2026-08-08 23:50:04 -04:00
let state = RowState {
read_only: self.ontology.is_read_only(quad.as_ref()),
datatype_state,
};
Message::AddRow(Some((key, state)))
});
2026-08-07 19:28:08 -04:00
task = Task::batch(messages.map(Task::done))
.chain(Task::done(Message::RunInference))
.chain(Task::done(Message::ResetState));
2026-06-08 19:33:49 -04:00
self.document = document;
}
Message::ShowError(error) => {
error!(error);
}
Message::AddRow(None) => {
2026-07-10 23:14:50 -04:00
let mut quad = self.document.new_quad();
let empty_node = NamedNode::new_unchecked("");
quad.predicate = empty_node.clone();
quad.object = Term::NamedNode(empty_node);
2026-06-08 19:33:49 -04:00
let key = self.document.dataset_mut().quads.insert(quad);
2026-08-08 23:50:04 -04:00
let datatype_state = combo_box::State::new(self.abbreviated_datatypes.clone());
2026-06-08 19:33:49 -04:00
let state = RowState {
2026-08-07 19:28:08 -04:00
read_only: false,
2026-06-08 19:33:49 -04:00
datatype_state,
};
2026-08-07 19:28:08 -04:00
task = Task::done(Message::AddRow(Some((key, state))));
}
Message::AddRow(Some((key, state))) => {
2026-06-08 19:33:49 -04:00
self.document
.dataset_mut()
.associated_data
.insert(key, state);
2026-08-07 19:28:08 -04:00
2026-06-08 19:33:49 -04:00
self.modified = true;
}
Message::DeleteRow(key) => {
self.document.dataset_mut().remove(key);
self.modified = true;
}
Message::DeleteAllRows => {
self.document.dataset_mut().clear();
self.modified = true;
}
Message::OpenQueryWindow(action) => {
2026-07-10 12:20:51 -04:00
let selected_entity_class = match action {
SearchResultClickAction::Predicate(_) => Some(rdf::PROPERTY),
SearchResultClickAction::Object(key) => {
2026-08-08 23:50:04 -04:00
self.document.dataset().quads.get(key).and_then(|quad| {
if quad.predicate == rdf::TYPE {
2026-07-10 12:20:51 -04:00
Some(rdfs::CLASS)
2026-08-08 23:50:04 -04:00
} else {
None
}
})
}
2026-07-10 12:20:51 -04:00
SearchResultClickAction::URLInput => None,
};
2026-07-12 13:52:13 -04:00
if let Some(search_state) = &mut self.search_state {
search_state.action = action;
} else {
let (id, window_task) = window::open(Settings::default());
self.search_state = Some(SearchState {
window_id: id,
action,
entity_selection: None,
query: String::new(),
results: Vec::new(),
});
2026-07-25 23:08:19 -04:00
task = window_task.then(|_| operation::focus("query"));
2026-07-12 13:52:13 -04:00
}
2026-07-10 12:20:51 -04:00
if let Some(selected_entity_class) = selected_entity_class {
2026-08-08 23:50:04 -04:00
let selection = self.ontology.info(
&selected_entity_class.into_owned(),
&*language::ENGLISH_OR_UNTAGGED,
);
2026-07-10 12:20:51 -04:00
task = task.chain(Task::done(Message::QueryTypeUpdated(selection)));
2026-06-17 20:50:26 -04:00
}
}
Message::QueryTypeUpdated(type_) => {
if let Some(search_state) = &mut self.search_state {
2026-07-10 12:20:51 -04:00
search_state.entity_selection = Some(type_);
2026-07-01 21:28:12 -04:00
task = Task::done(Message::QueryUpdated(search_state.query.clone()));
2026-06-08 19:33:49 -04:00
}
}
Message::QueryUpdated(new_query) => {
if let Some(search_state) = &mut self.search_state {
2026-08-08 23:50:04 -04:00
let category_id = search_state
.entity_selection
2026-07-10 12:20:51 -04:00
.clone()
2026-07-20 23:53:33 -04:00
.and_then(|info| self.ontology.category_id(&info.iri));
2026-06-09 20:09:16 -04:00
2026-06-30 19:25:15 -04:00
let query = new_query.clone();
search_state.query = new_query;
2026-08-08 23:50:04 -04:00
let results = self
.index
.query(category_id, query.as_str(), Schema::all_fields(), 25)
2026-07-22 21:41:57 -04:00
.expect("Unable to complete search");
task = Task::done(Message::SetSearchResults(results));
2026-06-08 19:33:49 -04:00
};
}
2026-06-29 23:10:22 -04:00
Message::SetSearchResults(results) => {
if let Some(search_state) = &mut self.search_state {
search_state.results = results;
}
}
2026-06-08 19:33:49 -04:00
Message::SearchResultClicked(node) => {
if let Some(search_state) = &self.search_state {
match search_state.action {
SearchResultClickAction::URLInput => {
task = Task::done(Message::URLInputChanged(node.as_str().to_string()))
.chain(Task::done(Message::URLInputSubmitted));
}
SearchResultClickAction::Predicate(key) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
quad.predicate = node;
}
}
SearchResultClickAction::Object(key) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
quad.object = Term::NamedNode(node);
}
}
}
task = task.chain(window::close(search_state.window_id));
self.modified = true;
}
}
Message::HoverRow(index) => {
self.hovered_row = Some(index);
}
Message::UnhoverRow(index) if self.hovered_row == Some(index) => {
self.hovered_row = None;
}
Message::DatatypeUpdated(key, Some(maybe_prefixed_iri)) => {
let node = self
2026-07-13 14:05:40 -04:00
.curie_helper
2026-07-24 19:25:07 -04:00
.expand(None, &maybe_prefixed_iri)
2026-07-13 14:05:40 -04:00
.unwrap_or(maybe_prefixed_iri);
2026-06-08 19:33:49 -04:00
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let mut term = TermHelperMut::new(&mut quad.object);
2026-07-13 14:05:40 -04:00
term.set_datatype(Some(NamedNode::new_unchecked(node)));
2026-06-08 19:33:49 -04:00
self.modified = true;
}
}
Message::DatatypeUpdated(key, None) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let mut term = TermHelperMut::new(&mut quad.object);
term.set_datatype(None);
self.modified = true;
}
}
Message::LanguageUpdated(key, Some(language)) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let mut term = TermHelperMut::new(&mut quad.object);
term.set_language(language.as_str());
self.modified = true;
}
}
Message::LanguageUpdated(key, None) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let mut term = TermHelperMut::new(&mut quad.object);
term.set_language("en");
self.modified = true;
}
}
2026-07-24 19:25:07 -04:00
Message::SubjectUpdated(key, value) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
quad.subject = NamedOrBlankNode::NamedNode(NamedNode::new_unchecked(value));
self.modified = true;
}
}
2026-07-08 21:07:52 -04:00
Message::PredicateUpdated(key, value) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
quad.predicate = NamedNode::new_unchecked(value);
self.modified = true;
}
}
Message::ObjectUpdated(key, value) => {
2026-06-08 19:33:49 -04:00
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let mut term = TermHelperMut::new(&mut quad.object);
term.set_value(value);
self.modified = true;
}
}
Message::DirectionToggled(key, direction) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let mut term = TermHelperMut::new(&mut quad.object);
term.set_direction(direction);
self.modified = true;
}
}
Message::SaveGraph(overwrite) => {
let client = self.http_client.clone();
let options = SerializationOptions::from_format(RdfFormat::Turtle)
2026-07-07 22:06:39 -04:00
.with_filter(self.ontology.exclude_read_only());
2026-06-08 19:33:49 -04:00
let request = self
.document
.to_update(options)
.expect("Failed to generate PUT request");
let url = self.document.origin().clone();
task = Task::future(async move {
match request.send(client, overwrite).await {
Ok(RdfSourceUpdateResponse::Success) => Message::FetchDocument(url),
Ok(RdfSourceUpdateResponse::DocumentModified(_)) => {
Message::ShowOverwriteConfirmationModal
}
Err(err) => Message::ShowError(format!("Failed to save graph: {err}")),
}
});
}
Message::ShowOverwriteConfirmationModal => {
self.show_overwrite_confirmation = true;
}
Message::HideOverwriteConfirmationModal => {
self.show_overwrite_confirmation = false;
}
Message::ConfirmOverwrite => {
task = Task::done(Message::SaveGraph(true))
.chain(Task::done(Message::HideOverwriteConfirmationModal));
}
Message::ShowNewDocumentButtons => {
self.show_new_document_buttons = true;
}
Message::HideNewDocumentButtons => {
self.show_new_document_buttons = false;
}
Message::NewDocument(class) => {
let url = Url::parse(self.url_input.as_str()).expect("Invalid URL");
let subject = NamedNode::new_unchecked(url.as_str());
self.document = RdfSource::new(url);
2026-08-07 19:28:08 -04:00
/*let template_quads = self
2026-06-08 19:33:49 -04:00
.ontology
.template_triples(class.as_ref(), subject.as_ref())
.map(|triples| {
triples
.map(|triple| self.document.quad_from_triple(triple))
.collect::<Vec<_>>()
2026-07-08 21:07:52 -04:00
})
.unwrap_or_else(|| {
2026-07-10 23:14:50 -04:00
let mut new_quad = self.document.new_quad();
new_quad.object = Term::NamedNode(class);
vec![new_quad]
2026-07-08 21:07:52 -04:00
});
2026-06-08 19:33:49 -04:00
2026-07-08 21:07:52 -04:00
task = Task::done(Message::HideNewDocumentButtons);
let new_keys = self.document.dataset_mut().extend(template_quads.into_iter());
let messages = new_keys.map(|triple| Message::AddRow(Some(triple)));
2026-08-07 19:28:08 -04:00
task = task.chain(Task::batch(messages.map(Task::done)));*/
2026-06-08 19:33:49 -04:00
}
Message::ResetState => {
self.modified = false;
self.show_new_document_buttons = false;
self.show_overwrite_confirmation = false;
}
2026-07-12 13:52:13 -04:00
Message::NavigateToPredicate(key) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
2026-07-14 21:31:15 -04:00
let url = Url::parse(quad.predicate.as_str()).expect("Invalid URL");
task = Task::done(Message::URLInputChanged(url.to_string()))
.chain(Task::done(Message::URLInputSubmitted));
2026-07-12 13:52:13 -04:00
}
}
Message::NavigateToObject(key) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
if let Term::NamedNode(node) = &quad.object {
2026-07-14 21:31:15 -04:00
let url = Url::parse(node.as_str()).expect("Invalid URL");
task = Task::done(Message::URLInputChanged(url.to_string()))
.chain(Task::done(Message::URLInputSubmitted));
2026-07-12 13:52:13 -04:00
}
}
}
2026-07-14 21:31:15 -04:00
Message::NavigateBack => {
let url = self.navigator.back().clone();
task = Task::done(Message::URLInputChanged(url.to_string()))
.chain(Task::done(Message::NavigateTo(url)));
}
Message::NavigateForward => {
let url = self.navigator.forward().clone();
task = Task::done(Message::URLInputChanged(url.to_string()))
.chain(Task::done(Message::NavigateTo(url)));
}
2026-07-30 12:40:22 -04:00
Message::SetReadOnly(value) => {
self.show_read_only = value;
}
2026-08-07 19:28:08 -04:00
Message::SetInferredTypes(value) => {
self.show_inferred_types = value;
}
Message::SetInferredProperties(value) => {
self.show_inferred_properties = value;
}
Message::RunInference => {
2026-08-08 23:50:04 -04:00
let dataset = self
.document
2026-08-07 19:28:08 -04:00
.dataset()
.quads
.iter()
.map(|(_, quad)| quad)
.collect();
let engine = self.inference_engine.clone();
2026-08-08 23:50:04 -04:00
task = Task::perform(
tokio::task::spawn_blocking(move || engine.run(&dataset).unwrap()),
|dataset| Message::SetInferredTriples(dataset.unwrap()),
);
2026-08-07 19:28:08 -04:00
}
Message::SetInferredTriples(dataset) => {
self.inferred_triples = Graph::from_iter(&dataset);
}
2026-08-05 11:21:25 -04:00
Message::Event(Event::KeyPressed {
2026-08-08 23:50:04 -04:00
key: keyboard::Key::Named(key::Named::Tab),
modifiers,
..
}) => {
2026-08-05 11:21:25 -04:00
if modifiers.shift() {
task = operation::focus_previous();
} else {
task = operation::focus_next();
}
}
2026-06-08 19:33:49 -04:00
_ => {}
}
task
}
pub(crate) fn title(&self, _window: window::Id) -> String {
"Graph of Liberty Publisher".to_string()
}
pub(crate) fn subscription(&self) -> Subscription<Message> {
2026-06-23 21:55:52 -04:00
Subscription::batch([
window::close_events().map(Message::WindowClosed),
2026-08-05 11:21:25 -04:00
keyboard::listen().map(Message::Event),
2026-06-23 21:55:52 -04:00
])
2026-06-08 19:33:49 -04:00
}
fn view_row<'a>(
&'a self,
key: QuadKey,
triple: &'a Quad,
state: &'a RowState,
) -> Element<'a, Message> {
const BUTTON_WIDTH: Length = Length::Fixed(35.0);
let delete_button = button("\u{274c}")
.style(|_, _| Style::default().with_background(Background::Color(color!(255, 0, 0))))
.on_press(Message::DeleteRow(key));
let button_area = if self.hovered_row == Some(key) && !state.read_only {
container(delete_button).width(BUTTON_WIDTH)
} else {
container(space()).width(BUTTON_WIDTH)
};
2026-07-24 19:25:07 -04:00
let base = self.document.origin().as_str();
let subject = match &triple.subject {
NamedOrBlankNode::NamedNode(subject) => subject.as_str(),
_ => "",
};
let subject_input_base = iri_input(&self.curie_helper, "Subject", Some(&base), subject);
let subject_input = if self.ontology.is_read_only(triple.as_ref()) {
subject_input_base
} else {
subject_input_base.on_input(move |value| Message::SubjectUpdated(key, value))
};
2026-08-08 23:50:04 -04:00
let predicate_input_base = iri_input(
&self.curie_helper,
"Predicate",
Some(&base),
triple.predicate.as_str(),
);
2026-07-10 23:14:50 -04:00
let predicate_input = if self.ontology.is_read_only(triple.as_ref()) {
predicate_input_base
} else {
2026-08-08 23:50:04 -04:00
predicate_input_base
.on_input(move |value| Message::PredicateUpdated(key, value))
.on_control_click(Message::OpenQueryWindow(
SearchResultClickAction::Predicate(key),
))
2026-07-12 13:52:13 -04:00
.on_shift_click(Message::NavigateToPredicate(key))
2026-07-10 23:14:50 -04:00
};
2026-06-08 19:33:49 -04:00
2026-08-08 23:50:04 -04:00
let predicate_info = self
.ontology
.info(&triple.predicate, &*language::ENGLISH_OR_UNTAGGED);
2026-07-13 14:05:40 -04:00
let predicate_label = container(text(predicate_info.label));
2026-06-08 19:33:49 -04:00
let term = TermHelper::new(&triple.object);
2026-06-29 23:10:22 -04:00
let value_label = term.value_as_named_node().and_then(|node| {
2026-08-08 23:50:04 -04:00
let info = self
.ontology
.info(&node.into_owned(), &*language::ENGLISH_OR_UNTAGGED);
2026-07-01 17:42:23 -04:00
Some(container(text(info.label)))
2026-06-29 23:10:22 -04:00
});
2026-06-08 19:33:49 -04:00
let value = term
.value_as_named_node()
2026-06-26 11:28:44 -04:00
.map(|node| node.as_str().to_string())
2026-06-08 19:33:49 -04:00
.unwrap_or(term.value().to_string());
let value_alignment = term
.direction()
.map(|direction| match direction {
BaseDirection::Ltr => Horizontal::Left,
BaseDirection::Rtl => Horizontal::Right,
})
.unwrap_or(Horizontal::Left);
2026-07-25 23:08:19 -04:00
// If the datatype is something, then it's not a named node, and we should use a plain
// text_input as opposed to an iri_input.
let object_input: Element<Message> = if term.datatype().is_some() {
2026-08-05 11:21:25 -04:00
let base = text_input("Object", value.clone());
2026-07-25 23:08:19 -04:00
if self.ontology.is_read_only(triple.as_ref()) {
base.into()
} else {
2026-08-08 23:50:04 -04:00
base.on_input(move |value| Message::ObjectUpdated(key, value))
.into()
2026-07-25 23:08:19 -04:00
}
2026-07-10 23:14:50 -04:00
} else {
2026-07-25 23:08:19 -04:00
let base = iri_input(&self.curie_helper, "Object", Some(&base), value.as_str())
.on_shift_click(Message::NavigateToObject(key));
if self.ontology.is_read_only(triple.as_ref()) {
base.into()
} else {
base.align_x(value_alignment)
.on_input(move |value| Message::ObjectUpdated(key, value))
2026-08-08 23:50:04 -04:00
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Object(
key,
)))
2026-07-25 23:08:19 -04:00
.into()
}
2026-07-10 23:14:50 -04:00
};
2026-06-23 21:55:52 -04:00
2026-08-08 23:50:04 -04:00
let selected_datatype = term
.datatype()
2026-07-24 19:25:07 -04:00
.and_then(|node| self.curie_helper.abbreviate(None, node.as_str()));
2026-07-13 14:05:40 -04:00
2026-06-08 19:33:49 -04:00
let datatype_selector: Element<Message> = if state.read_only {
selected_datatype.map(text).into()
} else {
combo_box(
&state.datatype_state,
"Datatype",
selected_datatype.as_ref(),
move |selection| Message::DatatypeUpdated(key, Some(selection)),
)
.on_input(move |input| {
let value = if input.is_empty() { None } else { Some(input) };
Message::DatatypeUpdated(key, value)
2026-08-08 23:50:04 -04:00
})
.into()
2026-06-08 19:33:49 -04:00
};
2026-08-05 11:21:25 -04:00
let language = term.language().unwrap_or("en").to_owned();
2026-06-08 19:33:49 -04:00
let language_input = match term.datatype() {
Some(rdf::LANG_STRING) | Some(rdf::DIR_LANG_STRING) => Some(container(
2026-08-05 11:21:25 -04:00
text_input("Language", language).on_input(move |input| {
2026-06-08 19:33:49 -04:00
let value = if input.is_empty() { None } else { Some(input) };
Message::LanguageUpdated(key, value)
}),
)),
_ => None,
};
let direction_slider = term
.direction()
.map(|direction| match direction {
BaseDirection::Ltr => toggler(false),
BaseDirection::Rtl => toggler(true),
})
.map(|toggler| {
container(row![
text("LTR"),
toggler.on_toggle(move |new_state| {
let direction = if new_state {
BaseDirection::Rtl
} else {
BaseDirection::Ltr
};
Message::DirectionToggled(key, direction)
}),
text("RTL"),
])
});
let row = row![
button_area,
2026-07-24 19:25:07 -04:00
subject_input,
2026-07-07 22:06:39 -04:00
predicate_input,
2026-07-13 14:05:40 -04:00
predicate_label,
2026-06-30 19:25:15 -04:00
object_input,
2026-07-13 14:05:40 -04:00
value_label,
2026-06-08 19:33:49 -04:00
datatype_selector,
language_input,
direction_slider,
];
mouse_area(row)
.on_enter(Message::HoverRow(key))
.on_exit(Message::UnhoverRow(key))
.into()
}
fn view_new_entity_buttons(
&self,
2026-07-07 22:06:39 -04:00
entities: impl IntoIterator<Item = LabeledIri>,
2026-06-08 19:33:49 -04:00
) -> Element<'_, Message> {
2026-07-07 22:06:39 -04:00
let buttons = entities.into_iter().map(|entity| {
2026-08-08 23:50:04 -04:00
let abbreviation = self
.curie_helper
.abbreviate(None, entity.iri.as_str())
2026-07-13 14:05:40 -04:00
.unwrap_or_else(|| entity.iri.as_str().to_string());
let label = format!("{} ({})", entity.label, abbreviation);
2026-06-08 19:33:49 -04:00
button(text(label))
2026-07-07 22:06:39 -04:00
.on_press(Message::NewDocument(entity.iri))
2026-06-08 19:33:49 -04:00
.into()
});
grid(buttons)
.height(Sizing::EvenlyDistribute(Length::Shrink))
.into()
}
pub(crate) fn view(&self, window: window::Id) -> Element<'_, Message> {
if let Some(search_state) = &self.search_state
&& search_state.window_id == window
{
2026-08-08 23:50:04 -04:00
let search_input = text_input("Query", &search_state.query)
.on_input(Message::QueryUpdated)
.id("query");
2026-06-08 19:33:49 -04:00
2026-08-07 18:16:03 -04:00
let entities = Vec::new(); /*self.ontology
2026-08-08 23:50:04 -04:00
.searchable_classes(&*language::ENGLISH_OR_UNTAGGED)
.into_iter()
.collect::<Vec<_>>();*/
2026-06-17 20:50:26 -04:00
2026-08-08 23:50:04 -04:00
let type_selector = pick_list(
search_state.entity_selection.as_ref(),
entities,
ToString::to_string,
)
.on_select(|selection| Message::QueryTypeUpdated(selection));
2026-06-17 20:50:26 -04:00
2026-08-08 23:50:04 -04:00
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();
2026-06-17 20:50:26 -04:00
2026-08-08 23:50:04 -04:00
let abbreviated_iri = self
.curie_helper
.abbreviate(None, iri)
.unwrap_or_else(|| iri.to_string());
2026-07-13 14:05:40 -04:00
2026-08-08 23:50:04 -04:00
button(text(abbreviated_iri).wrapping(Wrapping::Word))
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
.style(button::text)
})];
2026-07-01 17:42:23 -04:00
2026-07-10 12:20:51 -04:00
let fields = if let Some(selection) = search_state.entity_selection.as_ref() {
2026-08-08 23:50:04 -04:00
self.ontology
.fields_for_class(&selection.iri, &*language::ENGLISH_OR_UNTAGGED)
2026-07-10 12:20:51 -04:00
.expect("Unable to load fields for class")
} else {
vec![]
};
2026-07-01 17:42:23 -04:00
for field in fields.into_iter() {
let field_name = field.name.clone();
columns.push(
table::column(text(field.label), move |document: &SearchDocument| {
2026-08-08 23:50:04 -04:00
let iri = document
.get_first(Schema::iri_field())
2026-07-01 17:42:23 -04:00
.and_then(|value| value.as_str())
.unwrap_or_default();
2026-08-08 23:50:04 -04:00
let value = document
.get_first(Schema::field(&field_name, Some(language::ENGLISH_PRIMARY)))
2026-07-01 17:42:23 -04:00
.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)
2026-08-08 23:50:04 -04:00
})
.width(Length::Fixed(256.0)),
2026-07-01 17:42:23 -04:00
);
2026-06-17 20:50:26 -04:00
}
let results_table = if columns.is_empty() {
None
} else {
Some(scrollable(table(columns, &search_state.results)))
};
2026-06-08 19:33:49 -04:00
2026-08-08 23:50:04 -04:00
return column![row![search_input, type_selector], results_table].into();
2026-06-08 19:33:49 -04:00
}
2026-07-14 21:49:04 -04:00
let back_button = button("\u{1f870}").on_press(Message::NavigateBack);
2026-07-14 21:31:15 -04:00
2026-07-14 21:49:04 -04:00
let forward_button = button("\u{1f872}").on_press(Message::NavigateForward);
2026-07-14 21:31:15 -04:00
2026-06-08 19:33:49 -04:00
let add_row_button = button("Add row").on_press(Message::AddRow(None));
2026-07-24 19:25:07 -04:00
let address_input = iri_input(&self.curie_helper, "URL", None, &self.url_input)
2026-06-30 19:25:15 -04:00
.on_input(Message::URLInputChanged)
.on_submit(Message::URLInputSubmitted)
2026-07-28 15:09:33 -04:00
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::URLInput))
2026-08-08 23:50:04 -04:00
.style(crate::theme::styles::text_input::required(
!self.url_input_valid,
));
2026-06-08 19:33:49 -04:00
let mut rows: Vec<Element<Message>> = vec![];
rows = self
.document
.dataset()
.iter_both()
2026-07-30 12:40:22 -04:00
.filter(|(_, _, state)| self.show_read_only || !state.read_only)
2026-06-08 19:33:49 -04:00
.map(|(key, quad, state)| self.view_row(key, quad, state))
.collect();
let body: Element<Message> = if self.show_new_document_buttons {
2026-08-05 18:33:30 -04:00
let subclasses = Vec::new(); // TODO self.ontology.subclasses_of(vocab::rda::ENTITY.into_owned());
2026-08-08 23:50:04 -04:00
let labeled_subclasses = subclasses
.iter()
.map(|iri| self.ontology.info(iri, &*language::ENGLISH_OR_UNTAGGED));
2026-07-10 23:14:50 -04:00
column![self.view_new_entity_buttons(labeled_subclasses)].into()
2026-06-08 19:33:49 -04:00
} else {
column(rows).into()
};
2026-08-07 19:28:08 -04:00
let inference_table = if self.show_inferred_properties {
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())
2026-08-08 23:50:04 -04:00
} else {
None
};
2026-08-07 19:28:08 -04:00
curie.map(text)
});
let predicate_column = table::column("Predicate", |triple: TripleRef| {
2026-08-08 23:50:04 -04:00
let predicate_info = self.ontology.info(
&triple.predicate.into_owned(),
&*language::ENGLISH_OR_UNTAGGED,
);
2026-08-07 19:28:08 -04:00
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())
2026-08-08 23:50:04 -04:00
} else {
predicate_info.label
};
2026-08-07 19:28:08 -04:00
text(label)
});
2026-08-08 23:50:04 -04:00
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
};
2026-08-07 19:28:08 -04:00
2026-06-08 19:33:49 -04:00
let save_button_base = button(text("\u{1f4be}"));
let save_button = if self.modified {
save_button_base.on_press(Message::SaveGraph(false))
} else {
save_button_base
};
2026-08-08 23:50:04 -04:00
let inferred_type_toggle =
toggler(self.show_inferred_types).on_toggle(Message::SetInferredTypes);
2026-08-07 19:28:08 -04:00
2026-08-08 23:50:04 -04:00
let inferred_property_toggle =
toggler(self.show_inferred_properties).on_toggle(Message::SetInferredProperties);
2026-08-07 19:28:08 -04:00
2026-08-08 23:50:04 -04:00
let read_only_toggle = toggler(self.show_read_only).on_toggle(Message::SetReadOnly);
2026-07-30 12:40:22 -04:00
let footer = row![
2026-08-07 19:28:08 -04:00
space::horizontal(),
text("Show Inferred Types"),
inferred_type_toggle,
space::horizontal(),
text("Show Inferred Properties"),
inferred_property_toggle,
2026-07-30 12:40:22 -04:00
space::horizontal(),
text("Show Read Only Triples"),
read_only_toggle
];
2026-07-14 21:31:15 -04:00
let content = navigation_area(column![
2026-06-08 19:33:49 -04:00
row![
2026-07-22 21:41:57 -04:00
add_row_button,
2026-07-14 21:31:15 -04:00
back_button,
forward_button,
2026-06-30 19:25:15 -04:00
address_input,
2026-06-08 19:33:49 -04:00
save_button,
],
2026-07-30 12:40:22 -04:00
footer,
2026-08-07 19:28:08 -04:00
scrollable(body),
scrollable(inference_table),
2026-07-14 21:31:15 -04:00
])
2026-08-08 23:50:04 -04:00
.on_back_click(Message::NavigateBack)
.on_forward_click(Message::NavigateForward);
2026-06-08 19:33:49 -04:00
if self.show_overwrite_confirmation {
let confirmation_modal = container(column![
text("The resource has changed since it was fetched. Overwrite?"),
row![
button(text("Yes"))
.style(button::danger)
.on_press(Message::ConfirmOverwrite),
space::horizontal(),
button(text("No")).on_press(Message::HideOverwriteConfirmationModal),
],
])
.width(Length::Shrink);
modal(content, confirmation_modal, Message::None)
} else {
content.into()
}
}
}
fn modal<'a, Message>(
base: impl Into<Element<'a, Message>>,
content: impl Into<Element<'a, Message>>,
on_blur: Message,
) -> Element<'a, Message>
where
Message: Clone + 'a,
{
stack![
base.into(),
opaque(
mouse_area(center(opaque(content)).style(|_theme| {
container::Style {
background: Some(
Color {
a: 0.8,
..Color::BLACK
}
.into(),
),
..container::Style::default()
}
}))
.on_press(on_blur)
)
]
.into()
2026-08-08 23:50:04 -04:00
}