.
This commit is contained in:
@@ -8,14 +8,14 @@ pub enum Category {
|
||||
impl Category {
|
||||
pub fn to_named_node(&self) -> NamedNodeRef<'_> {
|
||||
match self {
|
||||
Category::AudioBook => vocab::gl::AUDIO_BOOK,
|
||||
Category::AudioBook => vocab::glo::AUDIO_BOOK,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_named_node<'a>(node: impl Into<NamedNodeRef<'a>>) -> Option<Self> {
|
||||
let node = node.into();
|
||||
match node {
|
||||
vocab::gl::AUDIO_BOOK => Some(Category::AudioBook),
|
||||
vocab::glo::AUDIO_BOOK => Some(Category::AudioBook),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
+39
-1
@@ -1,7 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use oxigraph::model::{NamedNode, NamedNodeRef};
|
||||
use oxigraph::model::{Graph, Literal, NamedNode, NamedNodeRef, NamedOrBlankNode, Term, Triple};
|
||||
use oxilangtag::LanguageTag;
|
||||
use url::Url;
|
||||
use gl_search::Schema;
|
||||
use crate::language::LanguageCondition;
|
||||
use crate::vocab;
|
||||
@@ -45,6 +46,14 @@ impl Class {
|
||||
Class::Manifestation,
|
||||
];
|
||||
|
||||
pub const CREATABLE: &[Class] = &[
|
||||
Class::Work,
|
||||
Class::Person,
|
||||
Class::CorporateBody,
|
||||
Class::Expression,
|
||||
Class::Manifestation,
|
||||
];
|
||||
|
||||
pub fn to_named_node(&self) -> NamedNodeRef<'_> {
|
||||
match self {
|
||||
Class::RdfProperty => vocab::rdf::PROPERTY,
|
||||
@@ -105,4 +114,33 @@ impl Class {
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_url(&self) -> Option<Url> {
|
||||
match self {
|
||||
Class::Work => Some(Url::parse("http://fedora.quill.lan/rest/#Work").unwrap()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn template(&self, subject: NamedNode) -> Graph {
|
||||
let empty_english_string = Term::Literal(Literal::new_language_tagged_literal_unchecked("", "en"));
|
||||
let empty_node = Term::NamedNode(NamedNode::new_unchecked("#"));
|
||||
|
||||
match self {
|
||||
Class::RdfProperty => Graph::new(),
|
||||
Class::RdfsClass => Graph::new(),
|
||||
Class::SkosConcept => Graph::new(),
|
||||
Class::Work => Graph::from_iter([
|
||||
Triple::new(NamedOrBlankNode::NamedNode(subject), vocab::rdawd::TITLE_OF_WORK, empty_english_string)
|
||||
]),
|
||||
Class::Person => Graph::from_iter([
|
||||
Triple::new(NamedOrBlankNode::NamedNode(subject.clone()), vocab::rdaao::IDENTIFIER_FOR_PERSON, empty_node),
|
||||
Triple::new(NamedOrBlankNode::NamedNode(subject.clone()), vocab::rdaad::GIVEN_NAME, empty_english_string.clone()),
|
||||
Triple::new(NamedOrBlankNode::NamedNode(subject), vocab::rdaad::SURNAME, empty_english_string),
|
||||
]),
|
||||
Class::CorporateBody => Graph::new(),
|
||||
Class::Expression => Graph::new(),
|
||||
Class::Manifestation => Graph::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -1,7 +1,7 @@
|
||||
pub use oxigraph::model::vocab::rdf;
|
||||
pub use oxigraph::model::vocab::rdfs;
|
||||
|
||||
pub mod gl {
|
||||
pub mod glo {
|
||||
use oxigraph::model::NamedNodeRef;
|
||||
|
||||
pub const AUDIO_BOOK: NamedNodeRef =
|
||||
@@ -9,6 +9,9 @@ pub mod gl {
|
||||
|
||||
pub const READ_ONLY: NamedNodeRef =
|
||||
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/readOnly");
|
||||
|
||||
pub const NEXT_URL: NamedNodeRef =
|
||||
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/nextUrl");
|
||||
}
|
||||
|
||||
pub mod skos {
|
||||
@@ -57,6 +60,13 @@ pub mod rdaad {
|
||||
NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/a/datatype/P50032");
|
||||
}
|
||||
|
||||
pub mod rdaao {
|
||||
use oxigraph::model::NamedNodeRef;
|
||||
|
||||
pub const IDENTIFIER_FOR_PERSON: NamedNodeRef =
|
||||
NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/a/object/P50094");
|
||||
}
|
||||
|
||||
pub mod rdawd {
|
||||
use oxigraph::model::NamedNodeRef;
|
||||
|
||||
|
||||
@@ -95,6 +95,12 @@ xsd:unsignedShort rdf:type rdfs:Datatype ;
|
||||
rdfs:label "derived with"@en .
|
||||
|
||||
|
||||
### https://graphofliberty.org/2026/04/ont/nextUrl
|
||||
:nextUrl rdf:type owl:ObjectProperty ;
|
||||
rdfs:comment "The next URL to use when creating a new document."@en ;
|
||||
rdfs:label "Next URL"@en .
|
||||
|
||||
|
||||
#################################################################
|
||||
# Data properties
|
||||
#################################################################
|
||||
@@ -135,6 +141,11 @@ fedora:Container rdf:type owl:NamedIndividual ;
|
||||
:readOnly "true"^^xsd:boolean .
|
||||
|
||||
|
||||
### http://fedora.info/definitions/v4/repository#RepositoryRoot
|
||||
fedora:RepositoryRoot rdf:type owl:NamedIndividual ;
|
||||
:readOnly "true"^^xsd:boolean .
|
||||
|
||||
|
||||
### http://fedora.info/definitions/v4/repository#Resource
|
||||
fedora:Resource rdf:type owl:NamedIndividual ;
|
||||
:readOnly "true"^^xsd:boolean .
|
||||
@@ -160,6 +171,11 @@ fedora:hasParent rdf:type owl:NamedIndividual ;
|
||||
:readOnly "true"^^xsd:boolean .
|
||||
|
||||
|
||||
### http://fedora.info/definitions/v4/repository#hasTransactionProvider
|
||||
fedora:hasTransactionProvider rdf:type owl:NamedIndividual ;
|
||||
:readOnly "true"^^xsd:boolean .
|
||||
|
||||
|
||||
### http://fedora.info/definitions/v4/repository#lastModified
|
||||
fedora:lastModified rdf:type owl:NamedIndividual ;
|
||||
:readOnly "true"^^xsd:boolean .
|
||||
|
||||
+79
-79
@@ -45,12 +45,12 @@ pub(crate) enum Message {
|
||||
NavigateTo(Url),
|
||||
FetchDocument(Url),
|
||||
LoadDocument(RdfSource<KeyedDataset<RowState>>),
|
||||
ResourceNotFound,
|
||||
ShowNewDocumentButtons,
|
||||
HideNewDocumentButtons,
|
||||
ShowError(String),
|
||||
AddRow(Option<(QuadKey, RowState)>),
|
||||
DeleteRow(QuadKey),
|
||||
DeleteAllRows,
|
||||
OpenQueryWindow(SearchResultClickAction),
|
||||
HoverRow(QuadKey),
|
||||
UnhoverRow(QuadKey),
|
||||
@@ -68,7 +68,7 @@ pub(crate) enum Message {
|
||||
ShowOverwriteConfirmationModal,
|
||||
HideOverwriteConfirmationModal,
|
||||
ConfirmOverwrite,
|
||||
NewDocument(NamedNode),
|
||||
NewDocument(Class, Option<Url>),
|
||||
NavigateToPredicate(QuadKey),
|
||||
NavigateToObject(QuadKey),
|
||||
NavigateBack,
|
||||
@@ -123,6 +123,7 @@ pub(crate) struct Publisher {
|
||||
show_overwrite_confirmation: bool,
|
||||
modified: bool,
|
||||
show_new_document_buttons: bool,
|
||||
resource_not_found: bool,
|
||||
}
|
||||
|
||||
impl Publisher {
|
||||
@@ -180,6 +181,7 @@ impl Publisher {
|
||||
show_overwrite_confirmation: false,
|
||||
modified: false,
|
||||
show_new_document_buttons: false,
|
||||
resource_not_found: false,
|
||||
},
|
||||
task.map(|_| Message::ConnectToOntologyService("http://[::1]:3000".to_string())),
|
||||
)
|
||||
@@ -259,8 +261,7 @@ impl Publisher {
|
||||
}
|
||||
}
|
||||
Message::NavigateTo(url) => {
|
||||
task = Task::done(Message::DeleteAllRows)
|
||||
.chain(Task::done(Message::ResetState))
|
||||
task = Task::done(Message::ResetState)
|
||||
.chain(Task::done(Message::FetchDocument(url)));
|
||||
}
|
||||
Message::FetchDocument(url) => {
|
||||
@@ -280,7 +281,7 @@ impl Publisher {
|
||||
},
|
||||
Err(ldp::Error::Reqwest(err)) => {
|
||||
if err.status() == Some(StatusCode::NOT_FOUND) {
|
||||
Message::ShowNewDocumentButtons
|
||||
Message::ResourceNotFound
|
||||
} else {
|
||||
Message::ShowError(err.to_string())
|
||||
}
|
||||
@@ -289,6 +290,10 @@ impl Publisher {
|
||||
}
|
||||
});
|
||||
}
|
||||
Message::ResourceNotFound => {
|
||||
self.resource_not_found = true;
|
||||
task = Task::done(Message::ShowNewDocumentButtons);
|
||||
}
|
||||
Message::LoadDocument(document) => {
|
||||
let nodes_to_look_up = document.dataset().quads.iter().map(|(_, quad)| {
|
||||
let mut nodes = vec![ quad.predicate.clone() ];
|
||||
@@ -342,10 +347,6 @@ impl Publisher {
|
||||
self.document.dataset_mut().remove(key);
|
||||
self.modified = true;
|
||||
}
|
||||
Message::DeleteAllRows => {
|
||||
self.document.dataset_mut().clear();
|
||||
self.modified = true;
|
||||
}
|
||||
Message::OpenQueryWindow(action) => {
|
||||
let selected_entity_class = match action {
|
||||
SearchResultClickAction::Predicate(_) => Some(vocab::rdf::PROPERTY),
|
||||
@@ -538,21 +539,26 @@ impl Publisher {
|
||||
Message::HideNewDocumentButtons => {
|
||||
self.show_new_document_buttons = false;
|
||||
}
|
||||
Message::NewDocument(class) => {
|
||||
let url = Url::parse(self.url_input.as_str()).expect("Invalid URL");
|
||||
let mut document: RdfSource<KeyedDataset<RowState>> = RdfSource::new(url);
|
||||
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());
|
||||
let mut quad = document.new_quad();
|
||||
quad.predicate = vocab::rdf::TYPE.into_owned();
|
||||
quad.object = Term::NamedNode(class);
|
||||
quad.object = Term::NamedNode(class.to_named_node().into_owned());
|
||||
document.dataset_mut().quads.insert(quad);
|
||||
|
||||
task = Task::done(Message::HideNewDocumentButtons)
|
||||
.chain(Task::done(Message::URLInputChanged(url.to_string())))
|
||||
.chain(Task::done(Message::LoadDocument(document)));
|
||||
}
|
||||
Message::ResetState => {
|
||||
self.modified = false;
|
||||
self.show_new_document_buttons = false;
|
||||
self.show_overwrite_confirmation = false;
|
||||
self.resource_not_found = false;
|
||||
}
|
||||
Message::NavigateToPredicate(key) => {
|
||||
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
|
||||
@@ -811,72 +817,76 @@ impl Publisher {
|
||||
.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)
|
||||
.id("query");
|
||||
fn view_search_window<'a>(&'a self, search_state: &'a SearchState) -> Element<'a, Message> {
|
||||
let search_input = text_input("Query", &search_state.query)
|
||||
.on_input(Message::QueryUpdated)
|
||||
.id("query");
|
||||
|
||||
let type_selector = pick_list(
|
||||
search_state.entity_class_selection.as_ref(),
|
||||
Class::ALL,
|
||||
ToString::to_string,
|
||||
).on_select(|selection| Message::UpdateQueryClass(selection));
|
||||
let type_selector = pick_list(
|
||||
search_state.entity_class_selection.as_ref(),
|
||||
Class::ALL,
|
||||
ToString::to_string,
|
||||
).on_select(|selection| Message::UpdateQueryClass(selection));
|
||||
|
||||
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();
|
||||
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();
|
||||
|
||||
let abbreviated_iri = document.get(&Schema::curie_field())
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
let abbreviated_iri = document.get(&Schema::curie_field())
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
|
||||
button(text(abbreviated_iri).wrapping(Wrapping::Word))
|
||||
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
|
||||
.style(button::text)
|
||||
})];
|
||||
button(text(abbreviated_iri).wrapping(Wrapping::Word))
|
||||
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
|
||||
.style(button::text)
|
||||
})];
|
||||
|
||||
let fields = search_state.entity_class_selection
|
||||
.as_ref()
|
||||
.map(|class| {
|
||||
let fields = search_state.entity_class_selection
|
||||
.as_ref()
|
||||
.map(|class| {
|
||||
class.fields(language::ENGLISH_TAG.clone())
|
||||
}).unwrap_or_default();
|
||||
|
||||
for (field, label) in fields {
|
||||
columns.push(
|
||||
table::column(text(label), move |document: &HashMap<gl_search::Field, gl_search::OwnedValue>| {
|
||||
let iri = document.get(&Schema::iri_field())
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
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();
|
||||
|
||||
let value = document
|
||||
.get(&field)
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
let value = document
|
||||
.get(&field)
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
|
||||
button(text(value).wrapping(Wrapping::Word))
|
||||
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
|
||||
.style(button::text)
|
||||
})
|
||||
button(text(value).wrapping(Wrapping::Word))
|
||||
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
|
||||
.style(button::text)
|
||||
})
|
||||
.width(Length::Fixed(256.0)),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let results_table = if columns.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(scrollable(table(columns, &search_state.results)))
|
||||
};
|
||||
let results_table = if columns.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(scrollable(table(columns, &search_state.results)))
|
||||
};
|
||||
|
||||
return column![row![search_input, type_selector], results_table].into();
|
||||
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);
|
||||
}
|
||||
|
||||
let back_button = button("\u{1f870}").on_press(Message::NavigateBack);
|
||||
let forward_button = button("\u{1f872}").on_press(Message::NavigateForward);
|
||||
let add_row_button = button("Add row").on_press(Message::AddRow(None));
|
||||
let new_document_button = button("New Document").on_press(Message::ShowNewDocumentButtons);
|
||||
let add_row_button = button("Add Row").on_press(Message::AddRow(None));
|
||||
|
||||
let address_input = iri_input(&self.curie_helper, "URL", None, &self.url_input)
|
||||
.on_input(Message::URLInputChanged)
|
||||
@@ -895,25 +905,14 @@ impl Publisher {
|
||||
.collect();
|
||||
|
||||
let body: Element<Message> = if self.show_new_document_buttons {
|
||||
let buttons = Class::ALL.iter()
|
||||
let buttons = Class::CREATABLE.iter()
|
||||
.map(|class| {
|
||||
let node = class.to_named_node().into_owned();
|
||||
let abbreviation = self.curie_helper
|
||||
.abbreviate(None, node.as_str())
|
||||
.unwrap_or(node.to_string());
|
||||
let url = if self.resource_not_found {
|
||||
Url::parse(&self.url_input).ok()
|
||||
} else { None };
|
||||
|
||||
let description = self.resource_descriptions
|
||||
.get(&node);
|
||||
|
||||
let button_text = if let Some(description) = description &&
|
||||
let Some(label) = &description.label {
|
||||
format!("{label} ({abbreviation})")
|
||||
} else {
|
||||
abbreviation
|
||||
};
|
||||
|
||||
button(text(button_text))
|
||||
.on_press(Message::NewDocument(node))
|
||||
button(text(class.to_string()))
|
||||
.on_press(Message::NewDocument(class.clone(), url))
|
||||
.into()
|
||||
});
|
||||
|
||||
@@ -1004,6 +1003,7 @@ impl Publisher {
|
||||
|
||||
let content = navigation_area(column![
|
||||
row![
|
||||
new_document_button,
|
||||
add_row_button,
|
||||
back_button,
|
||||
forward_button,
|
||||
|
||||
+37
-1
@@ -1,5 +1,11 @@
|
||||
use iced::Task;
|
||||
use oxigraph::model::{Graph, NamedNode};
|
||||
use ldp::reqwest_middleware::ClientWithMiddleware;
|
||||
use ldp::ResourceRequestBuilder;
|
||||
use oxigraph::io::RdfFormat;
|
||||
use oxigraph::model::{Dataset, Graph, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, TermRef};
|
||||
use tracing::debug;
|
||||
use url::Url;
|
||||
use gl_graph::class::Class;
|
||||
use gl_graph::ontology::{ConnectedOntology, OntologyBuilder, ResourceSelector};
|
||||
use gl_graph::vocab;
|
||||
use crate::app::Message;
|
||||
@@ -48,3 +54,33 @@ pub(crate) fn run_inference(mut ontology: ConnectedOntology, graph: Graph) -> Ta
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn navigate_to_next_url(client: ClientWithMiddleware, class: Class) -> Task<Message> {
|
||||
if let Some(lookup_url) = class.next_url() {
|
||||
Task::future(async move {
|
||||
let request = ResourceRequestBuilder::with_client_and_url(client, lookup_url.clone())
|
||||
.accept_rdf_format(RdfFormat::Turtle)
|
||||
.follow_described_by(true)
|
||||
.build();
|
||||
|
||||
match request.send().await {
|
||||
Ok(resource) => match resource.into_rdf_source::<Dataset>().await {
|
||||
Ok(document) => {
|
||||
let node = NamedNodeRef::new_unchecked(lookup_url.as_str());
|
||||
let result = document.dataset()
|
||||
.quads_for_pattern(Some(NamedOrBlankNodeRef::NamedNode(node)), Some(vocab::glo::NEXT_URL), None, None)
|
||||
.next();
|
||||
if let Some(next_url_quad) = result {
|
||||
if let TermRef::NamedNode(next_url) = next_url_quad.object {
|
||||
let url = Url::parse(next_url.as_str()).unwrap();
|
||||
Message::NewDocument(class, Some(url))
|
||||
} else { Message::None }
|
||||
} else { Message::None }
|
||||
}
|
||||
_ => Message::None
|
||||
}
|
||||
_ => Message::None
|
||||
}
|
||||
})
|
||||
} else { Task::none() }
|
||||
}
|
||||
Reference in New Issue
Block a user