2026-08-20 20:43:18 -04:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2026-08-23 17:01:13 -04:00
|
|
|
use std::ops::Sub;
|
2026-08-05 11:21:25 -04:00
|
|
|
use crate::navigator::Navigator;
|
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-24 21:44:23 -04:00
|
|
|
use gl_graph::{language, CurieHelper, vocab};
|
2026-08-12 14:11:47 -04:00
|
|
|
use gl_search::{Schema, SearchIndex};
|
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;
|
2026-08-24 21:44:23 -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-08-20 20:43:18 -04:00
|
|
|
use iced::advanced::text::Wrapping;
|
2026-08-24 21:44:23 -04:00
|
|
|
use iced::widget::grid::Sizing;
|
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-08-24 21:44:23 -04:00
|
|
|
use oxigraph::model::{BaseDirection, Graph, NamedNode, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term, TermRef, Triple, TripleRef};
|
|
|
|
|
use tracing::{error, trace};
|
2026-08-20 20:43:18 -04:00
|
|
|
use gl_graph::class::Class;
|
2026-08-22 23:31:14 -04:00
|
|
|
use gl_graph::ontology::{ConnectedOntology, OntologyBuilder, ReadOnlyEntity, ResourceDescription};
|
2026-08-20 20:43:18 -04:00
|
|
|
use gl_search::tantivy::schema::Value;
|
2026-08-24 21:44:23 -04:00
|
|
|
use crate::rdf::conversion::term_to_named_node;
|
2026-08-20 20:43:18 -04:00
|
|
|
use crate::tasks;
|
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,
|
2026-08-20 20:43:18 -04:00
|
|
|
ConnectToOntologyService(String),
|
2026-08-22 23:31:14 -04:00
|
|
|
ConnectedToOntologyService(ConnectedOntology),
|
2026-08-20 20:43:18 -04:00
|
|
|
CacheDatatypes(HashMap<NamedNode, ResourceDescription>),
|
|
|
|
|
CacheReadOnlyEntities(HashSet<ReadOnlyEntity>),
|
|
|
|
|
PopulateCaches,
|
2026-08-26 20:43:01 -04:00
|
|
|
LookupResourceDescriptions(HashSet<NamedNode>),
|
2026-08-23 17:01:13 -04:00
|
|
|
CacheResourceDescriptions(HashMap<NamedNode, ResourceDescription>),
|
2026-06-08 19:33:49 -04:00
|
|
|
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>>),
|
2026-08-26 19:44:18 -04:00
|
|
|
ResourceNotFound,
|
2026-06-08 19:33:49 -04:00
|
|
|
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),
|
|
|
|
|
OpenQueryWindow(SearchResultClickAction),
|
|
|
|
|
HoverRow(QuadKey),
|
|
|
|
|
UnhoverRow(QuadKey),
|
|
|
|
|
QueryUpdated(String),
|
2026-08-20 20:43:18 -04:00
|
|
|
SetSearchResults(Vec<HashMap<gl_search::Field, gl_search::OwnedValue>>),
|
|
|
|
|
UpdateQueryClass(Class),
|
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,
|
2026-08-26 19:44:18 -04:00
|
|
|
NewDocument(Class, Option<Url>),
|
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-08-20 20:43:18 -04:00
|
|
|
SetShowReadOnly(bool),
|
|
|
|
|
SetShowInferredTriples(bool),
|
2026-08-07 19:28:08 -04:00
|
|
|
RunInference,
|
2026-08-20 20:43:18 -04:00
|
|
|
SetInferredTriples(Graph),
|
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-08-20 20:43:18 -04:00
|
|
|
entity_class_selection: Option<Class>,
|
2026-06-08 19:33:49 -04:00
|
|
|
query: String,
|
2026-08-20 20:43:18 -04:00
|
|
|
results: Vec<HashMap<gl_search::Field, gl_search::OwnedValue>>,
|
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-08-22 23:31:14 -04:00
|
|
|
ontology: Option<ConnectedOntology>,
|
2026-08-20 20:43:18 -04:00
|
|
|
read_only_entities: HashSet<ReadOnlyEntity>,
|
2026-06-08 19:33:49 -04:00
|
|
|
abbreviated_datatypes: Vec<String>,
|
2026-08-20 20:43:18 -04:00
|
|
|
resource_descriptions: HashMap<NamedNode, ResourceDescription>,
|
2026-06-08 19:33:49 -04:00
|
|
|
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-20 20:43:18 -04:00
|
|
|
show_inferred_triples: bool,
|
2026-06-08 19:33:49 -04:00
|
|
|
hovered_row: Option<QuadKey>,
|
|
|
|
|
search_state: Option<SearchState>,
|
|
|
|
|
index: SearchIndex,
|
|
|
|
|
show_overwrite_confirmation: bool,
|
|
|
|
|
modified: bool,
|
|
|
|
|
show_new_document_buttons: bool,
|
2026-08-26 19:44:18 -04:00
|
|
|
resource_not_found: bool,
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Publisher {
|
2026-08-20 20:43:18 -04:00
|
|
|
fn is_read_only<'a>(&'a self, triple: impl Into<TripleRef<'a>>) -> bool {
|
|
|
|
|
let triple = triple.into();
|
2026-08-24 21:44:23 -04:00
|
|
|
if triple.predicate == vocab::rdf::TYPE &&
|
2026-08-20 20:43:18 -04:00
|
|
|
let TermRef::NamedNode(node) = triple.object {
|
|
|
|
|
self.read_only_entities.contains(&ReadOnlyEntity::Class(node.into_owned()))
|
|
|
|
|
} else {
|
|
|
|
|
self.read_only_entities.contains(&ReadOnlyEntity::Property(triple.predicate.into_owned()))
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-13 14:05:40 -04:00
|
|
|
|
2026-08-20 20:43:18 -04:00
|
|
|
pub(crate) fn new() -> (Self, Task<Message>) {
|
|
|
|
|
let curie_helper = CurieHelper::new(gl_graph::PREFIXES.clone());
|
2026-06-08 19:33:49 -04:00
|
|
|
|
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 (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-08-20 20:43:18 -04:00
|
|
|
ontology: None,
|
|
|
|
|
read_only_entities: HashSet::new(),
|
|
|
|
|
abbreviated_datatypes: Vec::new(),
|
|
|
|
|
resource_descriptions: HashMap::new(),
|
2026-06-08 19:33:49 -04:00
|
|
|
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-20 20:43:18 -04:00
|
|
|
show_inferred_triples: false,
|
2026-06-08 19:33:49 -04:00
|
|
|
hovered_row: None,
|
|
|
|
|
search_state: None,
|
|
|
|
|
index,
|
|
|
|
|
show_overwrite_confirmation: false,
|
|
|
|
|
modified: false,
|
|
|
|
|
show_new_document_buttons: false,
|
2026-08-26 19:44:18 -04:00
|
|
|
resource_not_found: false,
|
2026-06-08 19:33:49 -04:00
|
|
|
},
|
2026-08-20 20:43:18 -04:00
|
|
|
task.map(|_| Message::ConnectToOntologyService("http://[::1]:3000".to_string())),
|
2026-06-08 19:33:49 -04:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-08-20 20:43:18 -04:00
|
|
|
Message::ConnectToOntologyService(endpoint) => {
|
2026-08-22 23:31:14 -04:00
|
|
|
let builder = OntologyBuilder::from_string(&endpoint, language::ENGLISH_OR_UNTAGGED.clone())
|
|
|
|
|
.expect("Unable to parse endpoint of ontology service");
|
|
|
|
|
|
|
|
|
|
task = tasks::connect_to_ontology_service(builder)
|
2026-08-20 20:43:18 -04:00
|
|
|
.chain(Task::done(Message::PopulateCaches));
|
|
|
|
|
}
|
|
|
|
|
Message::ConnectedToOntologyService(ontology) => {
|
|
|
|
|
self.ontology = Some(ontology);
|
|
|
|
|
}
|
|
|
|
|
Message::PopulateCaches => {
|
|
|
|
|
if let Some(ontology) = &mut self.ontology {
|
2026-08-24 21:44:23 -04:00
|
|
|
let classes = Class::ALL.iter().map(|class| {
|
|
|
|
|
class.to_named_node().into_owned()
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-20 20:43:18 -04:00
|
|
|
task = Task::batch([
|
|
|
|
|
tasks::list_datatypes(ontology.clone()),
|
|
|
|
|
tasks::list_read_only_entities(ontology.clone()),
|
2026-08-24 21:44:23 -04:00
|
|
|
tasks::lookup_resource_descriptions(ontology.clone(), classes),
|
2026-08-20 20:43:18 -04:00
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::CacheDatatypes(datatypes) => {
|
|
|
|
|
self.abbreviated_datatypes = datatypes.keys()
|
|
|
|
|
.map(|node| {
|
|
|
|
|
self.curie_helper.abbreviate(None, node.as_str())
|
|
|
|
|
.unwrap_or(node.as_str().to_string())
|
|
|
|
|
}).collect();
|
|
|
|
|
}
|
|
|
|
|
Message::CacheReadOnlyEntities(entities) => {
|
|
|
|
|
self.read_only_entities = entities;
|
|
|
|
|
}
|
2026-08-23 17:01:13 -04:00
|
|
|
Message::LookupResourceDescriptions(nodes) => {
|
|
|
|
|
let provided_nodes: HashSet<NamedNode> = HashSet::from_iter(nodes);
|
|
|
|
|
let nodes_with_known_descriptions = HashSet::from_iter(self.resource_descriptions.keys().cloned());
|
|
|
|
|
let nodes_to_look_up = provided_nodes.sub(&nodes_with_known_descriptions);
|
|
|
|
|
|
|
|
|
|
if let Some(ontology) = &mut self.ontology {
|
|
|
|
|
task = tasks::lookup_resource_descriptions(ontology.clone(), nodes_to_look_up)
|
2026-08-20 20:43:18 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-08-23 17:01:13 -04:00
|
|
|
Message::CacheResourceDescriptions(descriptions) => {
|
|
|
|
|
self.resource_descriptions.extend(descriptions);
|
2026-08-20 20:43:18 -04:00
|
|
|
}
|
2026-06-08 19:33:49 -04:00
|
|
|
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-08-26 19:44:18 -04:00
|
|
|
task = Task::done(Message::ResetState)
|
2026-06-08 19:33:49 -04:00
|
|
|
.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) {
|
2026-08-26 19:44:18 -04:00
|
|
|
Message::ResourceNotFound
|
2026-06-08 19:33:49 -04:00
|
|
|
} else {
|
|
|
|
|
Message::ShowError(err.to_string())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => Message::ShowError(format!("Request failed: {err}")),
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-08-26 19:44:18 -04:00
|
|
|
Message::ResourceNotFound => {
|
|
|
|
|
self.resource_not_found = true;
|
|
|
|
|
task = Task::done(Message::ShowNewDocumentButtons);
|
|
|
|
|
}
|
2026-06-08 19:33:49 -04:00
|
|
|
Message::LoadDocument(document) => {
|
2026-08-24 21:44:23 -04:00
|
|
|
let nodes_to_look_up = document.dataset().quads.iter().map(|(_, quad)| {
|
|
|
|
|
let mut nodes = vec![ quad.predicate.clone() ];
|
|
|
|
|
if let Some(object) = term_to_named_node(&quad.object) {
|
|
|
|
|
nodes.push(object.clone());
|
|
|
|
|
}
|
|
|
|
|
nodes
|
|
|
|
|
}).flatten().collect();
|
|
|
|
|
|
2026-08-20 20:43:18 -04:00
|
|
|
let add_row_tasks = document.dataset().quads.iter().map(|(key, quad)| {
|
2026-08-08 23:50:04 -04:00
|
|
|
let datatype_state = combo_box::State::new(self.abbreviated_datatypes.clone());
|
|
|
|
|
let state = RowState {
|
2026-08-20 20:43:18 -04:00
|
|
|
read_only: self.is_read_only(quad.as_ref()),
|
2026-08-08 23:50:04 -04:00
|
|
|
datatype_state,
|
|
|
|
|
};
|
2026-08-20 20:43:18 -04:00
|
|
|
Task::done(Message::AddRow(Some((key, state))))
|
2026-08-08 23:50:04 -04:00
|
|
|
});
|
2026-08-24 21:44:23 -04:00
|
|
|
task = Task::done(Message::LookupResourceDescriptions(nodes_to_look_up))
|
|
|
|
|
.chain(Task::batch(add_row_tasks))
|
2026-08-07 19:28:08 -04:00
|
|
|
.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::OpenQueryWindow(action) => {
|
2026-07-10 12:20:51 -04:00
|
|
|
let selected_entity_class = match action {
|
2026-08-24 21:44:23 -04:00
|
|
|
SearchResultClickAction::Predicate(_) => Some(vocab::rdf::PROPERTY),
|
2026-07-10 12:20:51 -04:00
|
|
|
SearchResultClickAction::Object(key) => {
|
2026-08-08 23:50:04 -04:00
|
|
|
self.document.dataset().quads.get(key).and_then(|quad| {
|
2026-08-24 21:44:23 -04:00
|
|
|
if quad.predicate == vocab::rdf::TYPE {
|
|
|
|
|
Some(vocab::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,
|
2026-08-20 20:43:18 -04:00
|
|
|
entity_class_selection: None,
|
2026-07-12 13:52:13 -04:00
|
|
|
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
|
|
|
|
2026-08-20 20:43:18 -04:00
|
|
|
if let Some(selected_entity_class) = selected_entity_class &&
|
|
|
|
|
let Some(class) = Class::try_from_named_node(selected_entity_class) {
|
|
|
|
|
task = task.chain(Task::done(Message::UpdateQueryClass(class)))
|
2026-06-17 20:50:26 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-08-20 20:43:18 -04:00
|
|
|
Message::UpdateQueryClass(type_) => {
|
2026-06-17 20:50:26 -04:00
|
|
|
if let Some(search_state) = &mut self.search_state {
|
2026-08-20 20:43:18 -04:00
|
|
|
search_state.entity_class_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-20 20:43:18 -04:00
|
|
|
let category_id = search_state.entity_class_selection
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|selection| selection.to_owned() as u64);
|
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");
|
2026-08-20 20:43:18 -04:00
|
|
|
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 {
|
2026-08-26 20:43:01 -04:00
|
|
|
task = task.chain(Task::done(Message::LookupResourceDescriptions(HashSet::from_iter([node.clone()]))));
|
2026-08-25 17:07:40 -04:00
|
|
|
|
2026-06-08 19:33:49 -04:00
|
|
|
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();
|
2026-08-20 20:43:18 -04:00
|
|
|
let read_only_entities = self.read_only_entities.clone();
|
2026-06-08 19:33:49 -04:00
|
|
|
let options = SerializationOptions::from_format(RdfFormat::Turtle)
|
2026-08-20 20:43:18 -04:00
|
|
|
.with_filter(move |triple| {
|
2026-08-24 21:44:23 -04:00
|
|
|
if triple.predicate == vocab::rdf::TYPE &&
|
2026-08-20 20:43:18 -04:00
|
|
|
let TermRef::NamedNode(node) = triple.object {
|
2026-08-24 21:44:23 -04:00
|
|
|
!read_only_entities.contains(&ReadOnlyEntity::Class(node.into_owned()))
|
2026-08-20 20:43:18 -04:00
|
|
|
} else {
|
2026-08-24 21:44:23 -04:00
|
|
|
!read_only_entities.contains(&ReadOnlyEntity::Property(triple.predicate.into_owned()))
|
2026-08-20 20:43:18 -04:00
|
|
|
}
|
|
|
|
|
});
|
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;
|
|
|
|
|
}
|
2026-08-26 19:44:18 -04:00
|
|
|
Message::NewDocument(class, None) => {
|
|
|
|
|
let client = self.http_client.clone();
|
|
|
|
|
task = tasks::navigate_to_next_url(client, class);
|
|
|
|
|
}
|
|
|
|
|
Message::NewDocument(class, Some(url)) => {
|
|
|
|
|
let mut document: RdfSource<KeyedDataset<RowState>> = RdfSource::new(url.clone());
|
2026-08-26 20:43:01 -04:00
|
|
|
let mut nodes_to_look_up = HashSet::new();
|
|
|
|
|
let document_node = NamedNode::new_unchecked(url.as_str());
|
|
|
|
|
for triple in &class.template(document_node) {
|
|
|
|
|
let quad = document.quad_from_triple(triple.into_owned());
|
|
|
|
|
document.dataset_mut().quads.insert(quad);
|
2026-06-08 19:33:49 -04:00
|
|
|
|
2026-08-26 20:43:01 -04:00
|
|
|
if triple.predicate == vocab::rdf::TYPE &&
|
|
|
|
|
let TermRef::NamedNode(node) = triple.object {
|
|
|
|
|
nodes_to_look_up.insert(node.into_owned());
|
|
|
|
|
} else {
|
|
|
|
|
nodes_to_look_up.insert(triple.predicate.into_owned());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
task = Task::done(Message::LookupResourceDescriptions(nodes_to_look_up))
|
|
|
|
|
.chain(Task::done(Message::HideNewDocumentButtons))
|
2026-08-26 19:44:18 -04:00
|
|
|
.chain(Task::done(Message::URLInputChanged(url.to_string())))
|
2026-08-24 21:44:23 -04:00
|
|
|
.chain(Task::done(Message::LoadDocument(document)));
|
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-08-26 19:44:18 -04:00
|
|
|
self.resource_not_found = false;
|
2026-06-08 19:33:49 -04:00
|
|
|
}
|
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-08-20 20:43:18 -04:00
|
|
|
Message::SetShowReadOnly(value) => {
|
2026-07-30 12:40:22 -04:00
|
|
|
self.show_read_only = value;
|
|
|
|
|
}
|
2026-08-20 20:43:18 -04:00
|
|
|
Message::SetShowInferredTriples(value) => {
|
|
|
|
|
self.show_inferred_triples = value;
|
2026-08-07 19:28:08 -04:00
|
|
|
}
|
|
|
|
|
Message::RunInference => {
|
2026-08-20 20:43:18 -04:00
|
|
|
if let Some(ontology) = &mut self.ontology {
|
|
|
|
|
let graph = self.document
|
|
|
|
|
.dataset()
|
|
|
|
|
.quads
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(_, quad)| Triple::from(quad.clone()))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
task = tasks::run_inference(ontology.clone(), graph);
|
|
|
|
|
}
|
2026-08-07 19:28:08 -04:00
|
|
|
}
|
2026-08-20 20:43:18 -04:00
|
|
|
Message::SetInferredTriples(graph) => {
|
2026-08-23 17:01:13 -04:00
|
|
|
if let Some(ontology) = &mut self.ontology {
|
|
|
|
|
let predicates = graph.iter()
|
|
|
|
|
.map(|triple| triple.predicate.into_owned());
|
|
|
|
|
|
|
|
|
|
let objects = graph.iter()
|
|
|
|
|
.filter_map(|triple| match triple.object {
|
|
|
|
|
TermRef::NamedNode(node) => Some(node.into_owned()),
|
|
|
|
|
_ => None,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let mut nodes_to_look_up: HashSet<NamedNode> = HashSet::from_iter(predicates);
|
|
|
|
|
nodes_to_look_up.extend(objects);
|
|
|
|
|
task = tasks::lookup_resource_descriptions(ontology.clone(), nodes_to_look_up);
|
|
|
|
|
}
|
2026-08-20 20:43:18 -04:00
|
|
|
|
|
|
|
|
self.inferred_triples = graph;
|
2026-08-07 19:28:08 -04:00
|
|
|
}
|
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,
|
2026-08-20 20:43:18 -04:00
|
|
|
quad: &'a Quad,
|
2026-06-08 19:33:49 -04:00
|
|
|
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-08-27 13:00:36 -04:00
|
|
|
let base = self.document.origin();
|
2026-07-24 19:25:07 -04:00
|
|
|
|
2026-08-20 20:43:18 -04:00
|
|
|
let subject = match &quad.subject {
|
2026-07-24 19:25:07 -04:00
|
|
|
NamedOrBlankNode::NamedNode(subject) => subject.as_str(),
|
|
|
|
|
_ => "",
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-27 13:00:36 -04:00
|
|
|
let subject_input_base = iri_input(&self.curie_helper, "Subject", Some(base), subject);
|
2026-08-20 20:43:18 -04:00
|
|
|
let subject_input = if state.read_only {
|
2026-07-24 19:25:07 -04:00
|
|
|
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",
|
2026-08-27 13:00:36 -04:00
|
|
|
Some(base),
|
2026-08-20 20:43:18 -04:00
|
|
|
quad.predicate.as_str(),
|
2026-08-08 23:50:04 -04:00
|
|
|
);
|
2026-08-20 20:43:18 -04:00
|
|
|
let predicate_input = if state.read_only {
|
2026-07-10 23:14:50 -04:00
|
|
|
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-20 20:43:18 -04:00
|
|
|
let predicate_label = self.resource_descriptions
|
|
|
|
|
.get(&quad.predicate)
|
|
|
|
|
.and_then(|description| description.label.clone())
|
|
|
|
|
.map(|label| container(text(label)));
|
2026-07-13 14:05:40 -04:00
|
|
|
|
2026-08-20 20:43:18 -04:00
|
|
|
let term = TermHelper::new(&quad.object);
|
2026-06-08 19:33:49 -04:00
|
|
|
|
2026-08-20 20:43:18 -04:00
|
|
|
let value_label = term.value_as_named_node()
|
|
|
|
|
.and_then(|node| self.resource_descriptions.get(&node.into_owned()))
|
|
|
|
|
.and_then(|description| description.label.clone())
|
|
|
|
|
.map(|label| container(text(label)));
|
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-08-20 20:43:18 -04:00
|
|
|
if state.read_only {
|
2026-07-25 23:08:19 -04:00
|
|
|
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-08-27 13:00:36 -04:00
|
|
|
let base = iri_input(&self.curie_helper, "Object", Some(base), value.as_str())
|
2026-07-25 23:08:19 -04:00
|
|
|
.on_shift_click(Message::NavigateToObject(key));
|
2026-08-20 20:43:18 -04:00
|
|
|
if state.read_only {
|
2026-07-25 23:08:19 -04:00
|
|
|
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() {
|
2026-08-24 21:44:23 -04:00
|
|
|
Some(vocab::rdf::LANG_STRING) | Some(vocab::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()
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 19:44:18 -04:00
|
|
|
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");
|
2026-06-08 19:33:49 -04:00
|
|
|
|
2026-08-26 19:44:18 -04:00
|
|
|
let type_selector = pick_list(
|
|
|
|
|
search_state.entity_class_selection.as_ref(),
|
|
|
|
|
Class::ALL,
|
|
|
|
|
ToString::to_string,
|
|
|
|
|
).on_select(|selection| Message::UpdateQueryClass(selection));
|
2026-06-17 20:50:26 -04:00
|
|
|
|
2026-08-26 19:44:18 -04:00
|
|
|
let mut columns = vec![table::column(text("CURIE"), |document: &HashMap<gl_search::Field, gl_search::OwnedValue>| {
|
|
|
|
|
let iri = document.get(&Schema::iri_field())
|
|
|
|
|
.and_then(|value| value.as_str())
|
|
|
|
|
.unwrap_or_default();
|
2026-06-17 20:50:26 -04:00
|
|
|
|
2026-08-26 19:44:18 -04:00
|
|
|
let abbreviated_iri = document.get(&Schema::curie_field())
|
|
|
|
|
.and_then(|value| value.as_str())
|
|
|
|
|
.unwrap_or_default();
|
2026-07-13 14:05:40 -04:00
|
|
|
|
2026-08-26 19:44:18 -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-08-26 19:44:18 -04:00
|
|
|
let fields = search_state.entity_class_selection
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|class| {
|
2026-08-24 21:44:23 -04:00
|
|
|
class.fields(language::ENGLISH_TAG.clone())
|
|
|
|
|
}).unwrap_or_default();
|
|
|
|
|
|
2026-08-26 19:44:18 -04:00
|
|
|
for (field, label) in fields {
|
|
|
|
|
columns.push(
|
|
|
|
|
table::column(text(label), move |document: &HashMap<gl_search::Field, gl_search::OwnedValue>| {
|
|
|
|
|
let iri = document.get(&Schema::iri_field())
|
|
|
|
|
.and_then(|value| value.as_str())
|
|
|
|
|
.unwrap_or_default();
|
2026-07-01 17:42:23 -04:00
|
|
|
|
2026-08-26 19:44:18 -04:00
|
|
|
let value = document
|
|
|
|
|
.get(&field)
|
|
|
|
|
.and_then(|value| value.as_str())
|
|
|
|
|
.unwrap_or_default();
|
2026-07-01 17:42:23 -04:00
|
|
|
|
2026-08-26 19:44:18 -04:00
|
|
|
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-08-26 19:44:18 -04:00
|
|
|
);
|
|
|
|
|
}
|
2026-06-17 20:50:26 -04:00
|
|
|
|
2026-08-26 19:44:18 -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-26 19:44:18 -04:00
|
|
|
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 {
|
|
|
|
|
return self.view_search_window(search_state);
|
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);
|
|
|
|
|
let forward_button = button("\u{1f872}").on_press(Message::NavigateForward);
|
2026-08-26 19:44:18 -04:00
|
|
|
let new_document_button = button("New Document").on_press(Message::ShowNewDocumentButtons);
|
|
|
|
|
let add_row_button = button("Add Row").on_press(Message::AddRow(None));
|
2026-06-08 19:33:49 -04:00
|
|
|
|
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![];
|
2026-08-20 20:43:18 -04:00
|
|
|
rows = self.document
|
2026-06-08 19:33:49 -04:00
|
|
|
.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();
|
|
|
|
|
|
2026-08-24 21:44:23 -04:00
|
|
|
let body: Element<Message> = if self.show_new_document_buttons {
|
2026-08-26 19:44:18 -04:00
|
|
|
let buttons = Class::CREATABLE.iter()
|
2026-08-24 21:44:23 -04:00
|
|
|
.map(|class| {
|
2026-08-26 19:44:18 -04:00
|
|
|
let url = if self.resource_not_found {
|
|
|
|
|
Url::parse(&self.url_input).ok()
|
|
|
|
|
} else { None };
|
2026-08-24 21:44:23 -04:00
|
|
|
|
2026-08-26 19:44:18 -04:00
|
|
|
button(text(class.to_string()))
|
|
|
|
|
.on_press(Message::NewDocument(class.clone(), url))
|
2026-08-24 21:44:23 -04:00
|
|
|
.into()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
column![
|
|
|
|
|
grid(buttons).height(Sizing::EvenlyDistribute(Length::Shrink))
|
|
|
|
|
].into()
|
|
|
|
|
} else {
|
|
|
|
|
column(rows).into()
|
|
|
|
|
};
|
2026-06-08 19:33:49 -04:00
|
|
|
|
2026-08-20 20:43:18 -04:00
|
|
|
let inference_table = if self.show_inferred_triples {
|
2026-08-07 19:28:08 -04:00
|
|
|
let subject_column = table::column("Subject", |triple: TripleRef| {
|
|
|
|
|
let curie = if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject {
|
2026-08-20 20:43:18 -04:00
|
|
|
Some(self.curie_helper
|
2026-08-27 13:00:36 -04:00
|
|
|
.abbreviate(Some(self.document.origin()), subject.as_str())
|
2026-08-20 20:43:18 -04:00
|
|
|
.unwrap_or_else(|| subject.as_str().to_string()))
|
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-20 20:43:18 -04:00
|
|
|
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})"))
|
2026-08-08 23:50:04 -04:00
|
|
|
} else {
|
2026-08-20 20:43:18 -04:00
|
|
|
text(curie)
|
|
|
|
|
}
|
2026-08-07 19:28:08 -04:00
|
|
|
});
|
|
|
|
|
|
2026-08-08 23:50:04 -04:00
|
|
|
let object_column = table::column("Object", |triple: TripleRef| {
|
2026-08-23 17:01:13 -04:00
|
|
|
match triple.object {
|
2026-08-23 14:20:48 -04:00
|
|
|
TermRef::NamedNode(node) => {
|
2026-08-23 17:01:13 -04:00
|
|
|
let curie = self.curie_helper
|
2026-08-27 13:00:36 -04:00
|
|
|
.abbreviate(Some(self.document.origin()), node.as_str())
|
2026-08-23 17:01:13 -04:00
|
|
|
.unwrap_or_else(|| node.as_str().to_string());
|
2026-08-23 14:20:48 -04:00
|
|
|
|
2026-08-23 17:01:13 -04:00
|
|
|
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()),
|
|
|
|
|
}
|
2026-08-08 23:50:04 -04:00
|
|
|
});
|
|
|
|
|
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-20 20:43:18 -04:00
|
|
|
let inferred_triples_toggle =
|
|
|
|
|
toggler(self.show_inferred_triples).on_toggle(Message::SetShowInferredTriples);
|
2026-08-07 19:28:08 -04:00
|
|
|
|
2026-08-20 20:43:18 -04:00
|
|
|
let read_only_toggle = toggler(self.show_read_only).on_toggle(Message::SetShowReadOnly);
|
2026-07-30 12:40:22 -04:00
|
|
|
|
|
|
|
|
let footer = row![
|
2026-08-07 19:28:08 -04:00
|
|
|
space::horizontal(),
|
2026-08-20 20:43:18 -04:00
|
|
|
text("Show Inferred Triples"),
|
|
|
|
|
inferred_triples_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-08-26 19:44:18 -04:00
|
|
|
new_document_button,
|
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
|
|
|
}
|