diff --git a/publish/src/app.rs b/publish/src/app.rs index 8f4f544..1be84cd 100644 --- a/publish/src/app.rs +++ b/publish/src/app.rs @@ -17,8 +17,8 @@ use ldp::traverse::Traverse; use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions}; use oxigraph::io::RdfFormat; use oxigraph::model::vocab::{rdf, rdfs}; -use oxigraph::model::{BaseDirection, Dataset, NamedNode, NamedNodeRef, Quad, Term}; -use tracing::{debug, debug_span, error, trace}; +use oxigraph::model::{BaseDirection, BlankNode, Dataset, NamedNode, NamedNodeRef, NamedOrBlankNode, Quad, Term, Triple}; +use tracing::{debug, debug_span, error, info, trace}; use crate::rdf::language; use crate::rdf::vocab::rda; use crate::widget::iri_input::iri_input; @@ -49,7 +49,8 @@ pub(crate) enum Message { SearchResultClicked(NamedNode), DatatypeUpdated(QuadKey, Option), LanguageUpdated(QuadKey, Option), - ValueUpdated(QuadKey, String), + PredicateUpdated(QuadKey, String), + ObjectUpdated(QuadKey, String), DirectionToggled(QuadKey, BaseDirection), SaveGraph(bool), ShowOverwriteConfirmationModal, @@ -315,7 +316,7 @@ impl Publisher { if let Some(search_state) = &mut self.search_state { search_state.action = action; } else { - if let Some(entity) = self.ontology.searchable_classes(&*language::ENGLISH_OR_UNTAGGED).next() { + if let Some(entity) = self.ontology.searchable_classes(&*language::ENGLISH_OR_UNTAGGED).into_iter().next() { let (id, window_task) = window::open(Settings::default()); self.search_state = Some(SearchState { window_id: id, @@ -434,7 +435,13 @@ impl Publisher { self.modified = true; } } - Message::ValueUpdated(key, value) => { + 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) => { if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) { let mut term = TermHelperMut::new(&mut quad.object); term.set_value(value); @@ -488,21 +495,30 @@ impl Publisher { let subject = NamedNode::new_unchecked(url.as_str()); self.document = RdfSource::new(url); - let quads = self + let template_quads = self .ontology .template_triples(class.as_ref(), subject.as_ref()) .map(|triples| { triples .map(|triple| self.document.quad_from_triple(triple)) .collect::>() - }); + }) + .unwrap_or_else(|| { + let triple = Triple::new( + NamedOrBlankNode::BlankNode(BlankNode::default()), + rdf::TYPE, + Term::NamedNode(class) + ); + vec![ + self.document.quad_from_triple(triple) + ] + }); - 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))); - } + task = Task::done(Message::HideNewDocumentButtons); + + let new_keys = self.document.dataset_mut().extend(template_quads.into_iter()); + let messages = new_keys.map(|triple| Message::AddRow(Some(triple))); + task = task.chain(Task::batch(messages.map(Task::done))); } Message::ResetState => { self.modified = false; @@ -542,7 +558,7 @@ impl Publisher { }; let predicate_input = iri_input(self.ontology.prefixes(), "Predicate", triple.predicate.as_str()) - .on_input(move |value| Message::ValueUpdated(key, value)) + .on_input(move |value| Message::PredicateUpdated(key, value)) .on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Predicate(key))); let term = TermHelper::new(&triple.object); @@ -567,7 +583,7 @@ impl Publisher { let object_input = iri_input(self.ontology.prefixes(), "Object", value.as_str()) .align_x(value_alignment) - .on_input(move |value| Message::ValueUpdated(key, value)) + .on_input(move |value| Message::ObjectUpdated(key, value)) .on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Object(key))); let selected_datatype = term.datatype().map(|node| self.ontology.abbreviate(node)); @@ -656,10 +672,10 @@ impl Publisher { let search_input = text_input("Query", &search_state.query).on_input(Message::QueryUpdated); - let mut entities = self.ontology + let entities = self.ontology .searchable_classes(&*language::ENGLISH_OR_UNTAGGED) + .into_iter() .collect::>(); - entities.sort_by(|a, b| Ord::cmp(&a.label, &b.label)); let type_selector = pick_list(Some(&search_state.type_), entities, ToString::to_string) .on_select(|selection| Message::QueryTypeUpdated(selection)); diff --git a/publish/src/rdf/ontology.rs b/publish/src/rdf/ontology.rs index fae5b01..0729282 100644 --- a/publish/src/rdf/ontology.rs +++ b/publish/src/rdf/ontology.rs @@ -97,7 +97,7 @@ pub struct IndexEntry { pub fields: HashMap, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq)] pub struct LabeledIri { pub iri: NamedNode, pub label: String, @@ -109,6 +109,18 @@ impl PartialEq for LabeledIri { } } +impl PartialOrd for LabeledIri { + fn partial_cmp(&self, other: &Self) -> Option { + self.label.partial_cmp(&other.label) + } +} + +impl Ord for LabeledIri { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.label.cmp(&other.label) + } +} + impl Display for LabeledIri { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.label) @@ -255,8 +267,8 @@ WHERE {{ } } - pub fn searchable_classes(&self, language: &LanguageCondition) -> impl Iterator { - self.store.quads_for_pattern(None, Some(rdf::TYPE), Some(gl::SEARCHABLE.into()), None) + pub fn searchable_classes(&self, language: &LanguageCondition) -> BTreeSet { + self.store.quads_for_pattern(None, Some(rdf::TYPE), Some(gl::SEARCHABLE_CLASS.into()), None) .filter_map(Result::ok) .filter_map(|quad| { let label = self.store.quads_for_pattern(Some(quad.subject.as_ref().into()), Some(rdfs::LABEL), None, None) @@ -271,7 +283,7 @@ WHERE {{ label, }) } else { None } - }) + }).collect() } pub fn subclasses_of(&self, class: &NamedNode) -> BTreeSet { diff --git a/publish/src/rdf/vocab.rs b/publish/src/rdf/vocab.rs index c0fc2e8..3703ea3 100644 --- a/publish/src/rdf/vocab.rs +++ b/publish/src/rdf/vocab.rs @@ -10,8 +10,8 @@ pub mod gl { pub const CATALOG_ID: NamedNodeRef = NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/catalogId"); - pub const SEARCHABLE: NamedNodeRef = - NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/Searchable"); + pub const SEARCHABLE_CLASS: NamedNodeRef = + NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/SearchableClass"); pub const ENTITY: NamedNodeRef = NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/Entity");