2026-06-08 19:33:49 -04:00
|
|
|
use crate::rdf::ontology;
|
|
|
|
|
use crate::rdf::ontology::Ontology;
|
|
|
|
|
use crate::rdf::term_helper::{TermHelper, TermHelperMut};
|
2026-06-09 10:36:13 -04:00
|
|
|
use gl_search::{doc, Schema, SearchDocument, SearchIndex};
|
2026-06-08 19:33:49 -04:00
|
|
|
use http::StatusCode;
|
|
|
|
|
use iced::alignment::Horizontal;
|
|
|
|
|
use iced::widget::button::Style;
|
|
|
|
|
use iced::widget::grid::Sizing;
|
|
|
|
|
use iced::widget::{
|
|
|
|
|
button, center, column, combo_box, container, grid, mouse_area, opaque, row, scrollable, space,
|
|
|
|
|
stack, table, text, text_input, toggler,
|
|
|
|
|
};
|
|
|
|
|
use iced::window::Settings;
|
|
|
|
|
use iced::{Background, Color, Element, Length, Subscription, Task, color, window};
|
|
|
|
|
use ldp::middleware::BasicAuthMiddleware;
|
|
|
|
|
use ldp::model::{KeyedDataset, QuadKey};
|
|
|
|
|
use ldp::reqwest::{Client, Url};
|
|
|
|
|
use ldp::reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
|
|
|
|
use ldp::traverse::Traverse;
|
|
|
|
|
use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions};
|
|
|
|
|
use oxigraph::io::RdfFormat;
|
|
|
|
|
use oxigraph::model::vocab::rdf;
|
|
|
|
|
use oxigraph::model::{BaseDirection, Dataset, NamedNode, Quad, Term, TermRef};
|
|
|
|
|
use tracing::{debug, error};
|
|
|
|
|
|
2026-06-09 10:36:13 -04:00
|
|
|
const ANNOTATED_IRI_TYPE: u64 = 0;
|
|
|
|
|
|
2026-06-08 19:33:49 -04:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub(crate) enum Message {
|
|
|
|
|
None,
|
|
|
|
|
Traverse,
|
|
|
|
|
IndexRdfSource(RdfSource<Dataset>),
|
|
|
|
|
CommitIndex,
|
|
|
|
|
WindowClosed(window::Id),
|
|
|
|
|
URLInputChanged(String),
|
|
|
|
|
URLInputSubmitted,
|
|
|
|
|
FetchDocument(Url),
|
|
|
|
|
LoadDocument(RdfSource<KeyedDataset<RowState>>),
|
|
|
|
|
ShowNewDocumentButtons,
|
|
|
|
|
HideNewDocumentButtons,
|
|
|
|
|
ShowError(String),
|
|
|
|
|
AddRow(Option<QuadKey>),
|
|
|
|
|
DeleteRow(QuadKey),
|
|
|
|
|
DeleteAllRows,
|
|
|
|
|
OpenQueryWindow(SearchResultClickAction),
|
|
|
|
|
HoverRow(QuadKey),
|
|
|
|
|
UnhoverRow(QuadKey),
|
|
|
|
|
QueryUpdated(String),
|
|
|
|
|
SearchResultClicked(NamedNode),
|
|
|
|
|
DatatypeUpdated(QuadKey, Option<String>),
|
|
|
|
|
LanguageUpdated(QuadKey, Option<String>),
|
|
|
|
|
ValueUpdated(QuadKey, String),
|
|
|
|
|
DirectionToggled(QuadKey, BaseDirection),
|
|
|
|
|
SaveGraph(bool),
|
|
|
|
|
ShowOverwriteConfirmationModal,
|
|
|
|
|
HideOverwriteConfirmationModal,
|
|
|
|
|
ConfirmOverwrite,
|
|
|
|
|
NewDocument(NamedNode),
|
|
|
|
|
ResetState,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
|
query: String,
|
|
|
|
|
results: Vec<NamedNode>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) struct Publisher {
|
|
|
|
|
http_client: ClientWithMiddleware,
|
|
|
|
|
ontology: Ontology,
|
|
|
|
|
abbreviated_datatypes: Vec<String>,
|
|
|
|
|
window_id: window::Id,
|
|
|
|
|
url_input: String,
|
|
|
|
|
document: RdfSource<KeyedDataset<RowState>>,
|
|
|
|
|
hovered_row: Option<QuadKey>,
|
|
|
|
|
search_state: Option<SearchState>,
|
|
|
|
|
index: SearchIndex,
|
|
|
|
|
show_overwrite_confirmation: bool,
|
|
|
|
|
modified: bool,
|
|
|
|
|
show_new_document_buttons: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Publisher {
|
|
|
|
|
pub(crate) fn new() -> (Self, Task<Message>) {
|
|
|
|
|
let ontology = Ontology::builder()
|
|
|
|
|
.with_default_ontologies()
|
|
|
|
|
.build()
|
|
|
|
|
.expect("Failed to build ontology");
|
|
|
|
|
|
|
|
|
|
let mut index = SearchIndex::builder()
|
|
|
|
|
.with_path("/home/alex/Documents/Index")
|
|
|
|
|
.build()
|
|
|
|
|
.expect("Failed to build search index");
|
|
|
|
|
|
2026-06-09 10:36:13 -04:00
|
|
|
index.remove_all_of_type(ANNOTATED_IRI_TYPE);
|
2026-06-08 19:33:49 -04:00
|
|
|
ontology.for_each_annotated_iri(|iri, label, comment| {
|
2026-06-09 10:36:13 -04:00
|
|
|
let mut document = doc!(
|
|
|
|
|
Schema::type_field() => ANNOTATED_IRI_TYPE,
|
|
|
|
|
Schema::iri_field() => iri,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if let Some(label) = label {
|
|
|
|
|
document.add_text(Schema::field("label"), label.to_lowercase());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(comment) = comment {
|
|
|
|
|
document.add_text(Schema::field("comment"), comment.to_lowercase());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
index.add(document).expect("Unable to add annotated IRI to search index");
|
2026-06-08 19:33:49 -04:00
|
|
|
});
|
|
|
|
|
index.commit().expect("Unable to commit ontology to index");
|
|
|
|
|
|
|
|
|
|
let mut abbreviated_datatypes = ontology
|
|
|
|
|
.datatypes()
|
|
|
|
|
.map(|node| ontology.abbreviate(node))
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
let url_input = "http://fedora.quill.lan/rest/".to_string();
|
|
|
|
|
let document = RdfSource::new(Url::parse(&url_input).unwrap());
|
|
|
|
|
|
|
|
|
|
(
|
|
|
|
|
Self {
|
|
|
|
|
http_client,
|
|
|
|
|
ontology,
|
|
|
|
|
abbreviated_datatypes,
|
|
|
|
|
window_id: id,
|
|
|
|
|
url_input,
|
|
|
|
|
document,
|
|
|
|
|
hovered_row: None,
|
|
|
|
|
search_state: None,
|
|
|
|
|
index,
|
|
|
|
|
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();
|
|
|
|
|
debug!(?message);
|
|
|
|
|
|
|
|
|
|
match message {
|
|
|
|
|
Message::Traverse => {
|
|
|
|
|
let client = self.http_client.clone();
|
|
|
|
|
let root = Url::parse(&self.url_input).expect("Invalid URL");
|
|
|
|
|
let stream = Traverse::new(client, root, None);
|
|
|
|
|
task = Task::run(stream, |result| match result {
|
|
|
|
|
Ok(rdf_source) => Message::IndexRdfSource(rdf_source),
|
|
|
|
|
Err(err) => Message::ShowError(format!("Unable to fetch RDF Source: {err}")),
|
|
|
|
|
})
|
|
|
|
|
.chain(Task::done(Message::CommitIndex));
|
|
|
|
|
}
|
|
|
|
|
Message::IndexRdfSource(rdf_source) => {
|
|
|
|
|
let catalog_id = rdf_source
|
|
|
|
|
.classes()
|
|
|
|
|
.filter_map(|class| self.ontology.catalog_id(&class.into_owned()))
|
|
|
|
|
.next();
|
|
|
|
|
if let Some(catalog_id) = catalog_id {
|
|
|
|
|
let mut document = SearchDocument::new();
|
|
|
|
|
document.add_u64(Schema::type_field(), catalog_id);
|
|
|
|
|
document.add_text(Schema::iri_field(), rdf_source.origin());
|
|
|
|
|
|
|
|
|
|
for quad in rdf_source.dataset() {
|
|
|
|
|
if let Some(field_name) =
|
|
|
|
|
self.ontology.field_name(&quad.predicate.into_owned())
|
|
|
|
|
&& let TermRef::Literal(literal) = quad.object
|
|
|
|
|
{
|
|
|
|
|
let field = Schema::schema()
|
|
|
|
|
.get_field(&field_name)
|
|
|
|
|
.expect("Field not found in schema");
|
|
|
|
|
document.add_text(field, literal.value().to_lowercase());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.index
|
|
|
|
|
.add(document)
|
|
|
|
|
.expect("Unable to add document to search index");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::CommitIndex => {
|
|
|
|
|
if let Err(err) = self.index.commit() {
|
|
|
|
|
task = Task::done(Message::ShowError(format!("Unable to commit index: {err}")));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::WindowClosed(id) => {
|
|
|
|
|
if self.window_id == id {
|
|
|
|
|
task = iced::exit();
|
|
|
|
|
} else {
|
|
|
|
|
self.search_state = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::URLInputChanged(value) => {
|
|
|
|
|
self.url_input = value;
|
|
|
|
|
}
|
|
|
|
|
Message::URLInputSubmitted => {
|
|
|
|
|
let url = Url::parse(&self.url_input).expect("Invalid URL");
|
|
|
|
|
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) => {
|
|
|
|
|
let messages = document
|
|
|
|
|
.dataset()
|
|
|
|
|
.quads
|
|
|
|
|
.keys()
|
|
|
|
|
.map(|key| Message::AddRow(Some(key)));
|
|
|
|
|
task = Task::batch(messages.map(Task::done)).chain(Task::done(Message::ResetState));
|
|
|
|
|
|
|
|
|
|
self.document = document;
|
|
|
|
|
}
|
|
|
|
|
Message::ShowError(error) => {
|
|
|
|
|
error!(error);
|
|
|
|
|
}
|
|
|
|
|
Message::AddRow(None) => {
|
|
|
|
|
let quad = self.document.new_quad();
|
|
|
|
|
let key = self.document.dataset_mut().quads.insert(quad);
|
|
|
|
|
task = Task::done(Message::AddRow(Some(key)));
|
|
|
|
|
}
|
|
|
|
|
Message::AddRow(Some(key)) => {
|
|
|
|
|
let quad = self
|
|
|
|
|
.document
|
|
|
|
|
.dataset()
|
|
|
|
|
.quads
|
|
|
|
|
.get(key)
|
|
|
|
|
.expect("Failed to get quad from document");
|
|
|
|
|
let read_only = self.ontology.is_read_only(quad.as_ref());
|
|
|
|
|
let term = TermHelper::new(&quad.object);
|
|
|
|
|
let datatype = term
|
|
|
|
|
.datatype()
|
|
|
|
|
.map(|datatype| self.ontology.abbreviate(datatype));
|
|
|
|
|
let datatype_state = combo_box::State::with_selection(
|
|
|
|
|
self.abbreviated_datatypes.clone(),
|
|
|
|
|
datatype.as_ref(),
|
|
|
|
|
);
|
|
|
|
|
let state = RowState {
|
|
|
|
|
read_only,
|
|
|
|
|
datatype_state,
|
|
|
|
|
};
|
|
|
|
|
self.document
|
|
|
|
|
.dataset_mut()
|
|
|
|
|
.associated_data
|
|
|
|
|
.insert(key, state);
|
|
|
|
|
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) => {
|
|
|
|
|
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,
|
|
|
|
|
query: String::new(),
|
|
|
|
|
results: Vec::new(),
|
|
|
|
|
});
|
|
|
|
|
task = window_task.map(|_| Message::None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::QueryUpdated(new_query) => {
|
|
|
|
|
if let Some(search_state) = &mut self.search_state {
|
|
|
|
|
search_state.results = self
|
|
|
|
|
.index
|
2026-06-09 10:36:13 -04:00
|
|
|
.query(0, new_query.as_str(), Schema::all_fields())
|
2026-06-08 19:33:49 -04:00
|
|
|
.expect("Error encountered while querying index")
|
|
|
|
|
.iter()
|
|
|
|
|
.map(NamedNode::new_unchecked)
|
|
|
|
|
.collect();
|
|
|
|
|
search_state.query = new_query;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
.ontology
|
|
|
|
|
.expand(&maybe_prefixed_iri)
|
|
|
|
|
.unwrap_or(NamedNode::new_unchecked(&maybe_prefixed_iri));
|
|
|
|
|
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
|
|
|
|
|
let mut term = TermHelperMut::new(&mut quad.object);
|
|
|
|
|
term.set_datatype(Some(node));
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::ValueUpdated(key, value) => {
|
|
|
|
|
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)
|
|
|
|
|
.with_filter(self.ontology.exclude_read_only_predicate());
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
let quads = self
|
|
|
|
|
.ontology
|
|
|
|
|
.template_triples(class.as_ref(), subject.as_ref())
|
|
|
|
|
.map(|triples| {
|
|
|
|
|
triples
|
|
|
|
|
.map(|triple| self.document.quad_from_triple(triple))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if let Some(quads) = quads {
|
|
|
|
|
let new_keys = self.document.dataset_mut().extend(quads.into_iter());
|
|
|
|
|
let messages = new_keys.map(|triple| Message::AddRow(Some(triple)));
|
|
|
|
|
task = Task::done(Message::HideNewDocumentButtons)
|
|
|
|
|
.chain(Task::batch(messages.map(Task::done)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::ResetState => {
|
|
|
|
|
self.modified = false;
|
|
|
|
|
self.show_new_document_buttons = false;
|
|
|
|
|
self.show_overwrite_confirmation = false;
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
task
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn title(&self, _window: window::Id) -> String {
|
|
|
|
|
"Graph of Liberty Publisher".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn subscription(&self) -> Subscription<Message> {
|
|
|
|
|
window::close_events().map(Message::WindowClosed)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let property_label = self
|
|
|
|
|
.ontology
|
|
|
|
|
.label(triple.predicate.as_ref())
|
|
|
|
|
.unwrap_or(self.ontology.abbreviate(triple.predicate.as_ref()).as_str())
|
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
|
|
let property: Element<Message> = if state.read_only {
|
|
|
|
|
text(property_label).into()
|
|
|
|
|
} else {
|
|
|
|
|
button(text(property_label))
|
|
|
|
|
.on_press(Message::OpenQueryWindow(
|
|
|
|
|
SearchResultClickAction::Predicate(key),
|
|
|
|
|
))
|
|
|
|
|
.style(button::text)
|
|
|
|
|
.into()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let term = TermHelper::new(&triple.object);
|
|
|
|
|
|
|
|
|
|
let value_label = term.value_as_named_node().and_then(|node| {
|
|
|
|
|
self.ontology
|
|
|
|
|
.label(node)
|
|
|
|
|
.map(|label| container(text(label)))
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let value = term
|
|
|
|
|
.value_as_named_node()
|
|
|
|
|
.map(|node| self.ontology.abbreviate(node))
|
|
|
|
|
.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);
|
|
|
|
|
|
|
|
|
|
let value_input_base = text_input("Value", value.as_str()).align_x(value_alignment);
|
|
|
|
|
|
|
|
|
|
let value_input = if !state.read_only {
|
|
|
|
|
let is_named_node = term.is_named_node();
|
|
|
|
|
value_input_base.on_input(move |input| {
|
|
|
|
|
let expanded_input = if is_named_node {
|
|
|
|
|
self.ontology
|
|
|
|
|
.expand(input.as_str())
|
|
|
|
|
.map(|node| node.as_str().to_string())
|
|
|
|
|
.unwrap_or(input)
|
|
|
|
|
} else {
|
|
|
|
|
input
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Message::ValueUpdated(key, expanded_input)
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
value_input_base
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let search_launcher = if term.is_named_node() && !state.read_only {
|
|
|
|
|
Some(container(
|
|
|
|
|
button(text("\u{1f50e}"))
|
|
|
|
|
.on_press(Message::OpenQueryWindow(SearchResultClickAction::Object(
|
|
|
|
|
key,
|
|
|
|
|
)))
|
|
|
|
|
.style(button::text),
|
|
|
|
|
))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let selected_datatype = term.datatype().map(|node| self.ontology.abbreviate(node));
|
|
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
.into()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let language_input = match term.datatype() {
|
|
|
|
|
Some(rdf::LANG_STRING) | Some(rdf::DIR_LANG_STRING) => Some(container(
|
|
|
|
|
text_input("Language", term.language().unwrap_or("en")).on_input(move |input| {
|
|
|
|
|
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,
|
|
|
|
|
property,
|
|
|
|
|
value_label,
|
|
|
|
|
value_input,
|
|
|
|
|
search_launcher,
|
|
|
|
|
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,
|
|
|
|
|
entities: impl Iterator<Item = NamedNode>,
|
|
|
|
|
) -> Element<'_, Message> {
|
|
|
|
|
let buttons = entities.map(|entity| {
|
|
|
|
|
let label = self
|
|
|
|
|
.ontology
|
|
|
|
|
.label(entity.as_ref())
|
|
|
|
|
.map(|label| label.to_string())
|
|
|
|
|
.unwrap_or(entity.as_str().to_string());
|
|
|
|
|
button(text(label))
|
|
|
|
|
.on_press(Message::NewDocument(entity))
|
|
|
|
|
.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
|
|
|
|
|
{
|
|
|
|
|
let search_input =
|
|
|
|
|
text_input("Query", &search_state.query).on_input(Message::QueryUpdated);
|
|
|
|
|
|
|
|
|
|
let label_column = table::column("Label", |result: &NamedNode| {
|
|
|
|
|
let label = self.ontology.label(result.as_ref()).unwrap_or("Unknown");
|
|
|
|
|
|
|
|
|
|
button(text(label))
|
|
|
|
|
.on_press(Message::SearchResultClicked(result.clone()))
|
|
|
|
|
.style(button::text)
|
|
|
|
|
});
|
|
|
|
|
let iri_column = table::column("IRI", |result: &NamedNode| {
|
|
|
|
|
button(text(result.as_str()))
|
|
|
|
|
.on_press(Message::SearchResultClicked(result.clone()))
|
|
|
|
|
.style(button::text)
|
|
|
|
|
});
|
|
|
|
|
let results_table =
|
|
|
|
|
scrollable(table([label_column, iri_column], &search_state.results));
|
|
|
|
|
|
|
|
|
|
return column![search_input, results_table].into();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let add_row_button = button("Add row").on_press(Message::AddRow(None));
|
|
|
|
|
|
|
|
|
|
let index_button = button("Index").on_press(Message::Traverse);
|
|
|
|
|
|
|
|
|
|
let search_launcher = button(text("\u{1f50e}"))
|
|
|
|
|
.on_press(Message::OpenQueryWindow(SearchResultClickAction::URLInput))
|
|
|
|
|
.style(button::text);
|
|
|
|
|
|
|
|
|
|
let mut rows: Vec<Element<Message>> = vec![];
|
|
|
|
|
rows = self
|
|
|
|
|
.document
|
|
|
|
|
.dataset()
|
|
|
|
|
.iter_both()
|
|
|
|
|
.map(|(key, quad, state)| self.view_row(key, quad, state))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
let body: Element<Message> = if self.show_new_document_buttons {
|
|
|
|
|
column![
|
|
|
|
|
self.view_new_entity_buttons(self.ontology.subclass_of(ontology::GL_ENTITY)),
|
|
|
|
|
self.view_new_entity_buttons(self.ontology.subclass_of(ontology::RDA_ENTITY)),
|
|
|
|
|
]
|
|
|
|
|
.into()
|
|
|
|
|
} else {
|
|
|
|
|
column(rows).into()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let content = column![
|
|
|
|
|
row![
|
|
|
|
|
add_row_button,
|
|
|
|
|
index_button,
|
|
|
|
|
search_launcher,
|
|
|
|
|
text_input("URL", &self.url_input)
|
|
|
|
|
.on_input(Message::URLInputChanged)
|
|
|
|
|
.on_submit(Message::URLInputSubmitted),
|
|
|
|
|
save_button,
|
|
|
|
|
],
|
|
|
|
|
scrollable(body),
|
|
|
|
|
space::vertical(),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
}
|