at main 266 lines 11 kB view raw
1use crate::Route; 2use crate::auth::AuthState; 3use crate::components::record_editor::EditableRecordContent; 4use crate::components::record_view::{ 5 CodeView, PrettyRecordView, RecordViewLayout, SchemaView, ViewMode, 6}; 7use crate::fetch::Fetcher; 8use dioxus::prelude::*; 9use jacquard::common::to_data; 10use jacquard::smol_str::ToSmolStr; 11use jacquard::{ 12 client::AgentSessionExt, 13 common::IntoStatic, 14 identity::lexicon_resolver::LexiconSchemaResolver, 15 types::{aturi::AtUri, ident::AtIdentifier, string::Nsid}, 16}; 17use jacquard_lexicon::lexicon::LexiconDoc; 18 19#[component] 20pub fn RecordIndex() -> Element { 21 let navigator = use_navigator(); 22 let mut uri_input = use_signal(|| String::new()); 23 let handle_uri_submit = move || { 24 let input_uri = uri_input.read().clone(); 25 if !input_uri.is_empty() { 26 if let Ok(parsed) = AtUri::new(&input_uri) { 27 let link = format!("{}/record/{}", crate::env::WEAVER_APP_HOST, parsed); 28 navigator.push(link); 29 } 30 } 31 }; 32 rsx! { 33 document::Stylesheet { href: asset!("/assets/styling/record-view.css") } 34 div { 35 class: "record-view-container", 36 div { class: "record-header", 37 h1 { "Record View" } 38 div { class: "uri-input-section", 39 input { 40 r#type: "text", 41 class: "uri-input", 42 placeholder: "at://did:plc:.../collection/rkey", 43 value: "{uri_input}", 44 oninput: move |evt| uri_input.set(evt.value()), 45 onkeydown: move |evt| { 46 if evt.key() == Key::Enter { 47 handle_uri_submit(); 48 } 49 }, 50 } 51 } 52 } 53 54 Outlet::<Route> {} 55 } 56 } 57} 58 59#[component] 60pub fn RecordPage(uri: ReadSignal<Vec<String>>) -> Element { 61 rsx! { 62 {std::iter::once(rsx! {RecordView {uri}})} 63 } 64} 65 66#[component] 67pub fn RecordView(uri: ReadSignal<Vec<String>>) -> Element { 68 let fetcher = use_context::<Fetcher>(); 69 info!("Uri:{:?}", uri().join("/")); 70 let at_uri = AtUri::new_owned(&*uri.read().join("/")); 71 if at_uri.is_err() { 72 return rsx! {}; 73 } 74 let uri = use_signal(move || AtUri::new_owned(&*uri.read().join("/")).unwrap()); 75 let mut view_mode = use_signal(|| ViewMode::Pretty); 76 let mut edit_mode = use_signal(|| false); 77 78 let client = fetcher.get_client(); 79 let record_resource = use_resource(move || { 80 let client = client.clone(); 81 async move { client.fetch_record_slingshot(&*uri.read()).await } 82 }); 83 84 // Fetch schema for the record 85 let schema_resource = use_resource(move || { 86 let fetcher = fetcher.clone(); 87 async move { 88 let record_read = record_resource.read(); 89 let record = record_read.as_ref()?.as_ref().ok()?; 90 91 let validator = jacquard_lexicon::validation::SchemaValidator::global(); 92 let main_type = record.value.type_discriminator(); 93 let mut main_schema = None; 94 let mut resolved = std::collections::HashSet::new(); 95 96 // Helper to recursively resolve a schema and its refs 97 fn resolve_schema_with_refs<'a>( 98 fetcher: &'a Fetcher, 99 type_str: &'a str, 100 validator: &'a jacquard_lexicon::validation::SchemaValidator, 101 resolved: &'a mut std::collections::HashSet<String>, 102 ) -> std::pin::Pin< 103 Box<dyn std::future::Future<Output = Option<LexiconDoc<'static>>> + 'a>, 104 > { 105 Box::pin(async move { 106 if resolved.contains(type_str) { 107 return None; 108 } 109 resolved.insert(type_str.to_string()); 110 111 let mut split = type_str.split('#'); 112 let nsid_str = split.next().unwrap_or_default(); 113 let nsid = Nsid::new(nsid_str).ok()?; 114 115 let schema = fetcher.resolve_lexicon_schema(&nsid).await.ok()?; 116 117 // Register by base NSID only (validator handles fragment lookup) 118 validator 119 .registry() 120 .insert(nsid_str.to_smolstr(), schema.doc.clone()); 121 122 // Find refs in the schema and resolve them 123 if let Ok(schema_data) = to_data(&schema.doc) { 124 for ref_val in schema_data.query("...ref").values() { 125 if let Some(ref_str) = ref_val.as_str() { 126 if ref_str.contains('.') { 127 resolve_schema_with_refs(fetcher, ref_str, validator, resolved) 128 .await; 129 } 130 } 131 } 132 for ref_val in schema_data.query("...refs").values() { 133 if let Some(ref_str) = ref_val.as_str() { 134 if ref_str.contains('.') { 135 resolve_schema_with_refs(fetcher, ref_str, validator, resolved) 136 .await; 137 } 138 } 139 } 140 } 141 142 Some(schema.doc) 143 }) 144 } 145 146 // Find and resolve all schemas (including main and nested) 147 for type_val in record.value.query("...$type").values() { 148 if let Some(type_str) = type_val.as_str() { 149 // Skip non-NSID types (like "blob") 150 if !type_str.contains('.') { 151 continue; 152 } 153 154 if let Some(schema) = 155 resolve_schema_with_refs(&fetcher, type_str, &validator, &mut resolved) 156 .await 157 { 158 // Keep the main record schema 159 if Some(type_str) == main_type { 160 main_schema = Some(schema); 161 } 162 } 163 } 164 } 165 166 main_schema 167 } 168 }); 169 170 let schema_signal = use_memo(move || schema_resource.read().clone().flatten()); 171 172 // Check ownership for edit access 173 let auth_state = use_context::<Signal<AuthState>>(); 174 let is_owner = use_memo(move || { 175 let auth = auth_state(); 176 if !auth.is_authenticated() { 177 return false; 178 } 179 180 // authority() returns &AtIdentifier which can be Did or Handle 181 match &*uri.read().authority() { 182 AtIdentifier::Did(record_did) => auth.did.as_ref() == Some(record_did), 183 AtIdentifier::Handle(_) => { 184 // Can't easily check ownership for handles without async resolution 185 false 186 } 187 } 188 }); 189 if let Some(Ok(record)) = &*record_resource.read() { 190 let record_value = record.value.clone().into_static(); 191 let record = record.clone(); 192 193 rsx! { 194 Fragment { key: "{uri()}", 195 RecordViewLayout { 196 uri: uri().clone(), 197 cid: record.cid.clone(), 198 schema: schema_signal, 199 record_value: record_value.clone(), 200 if edit_mode() { 201 202 EditableRecordContent { 203 record_value: record_value, 204 uri: uri, 205 view_mode: view_mode, 206 edit_mode: edit_mode, 207 record_resource: record_resource, 208 schema: schema_signal, 209 } 210 } else { 211 div { 212 class: "tab-bar", 213 button { 214 class: if view_mode() == ViewMode::Pretty { "tab-button active" } else { "tab-button" }, 215 onclick: move |_| view_mode.set(ViewMode::Pretty), 216 "View" 217 } 218 button { 219 class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" }, 220 onclick: move |_| view_mode.set(ViewMode::Json), 221 "JSON" 222 } 223 button { 224 class: if view_mode() == ViewMode::Schema { "tab-button active" } else { "tab-button" }, 225 onclick: move |_| view_mode.set(ViewMode::Schema), 226 "Schema" 227 } 228 if is_owner() { 229 button { 230 class: "tab-button edit-button", 231 onclick: move |_| edit_mode.set(true), 232 "Edit" 233 } 234 } 235 } 236 div { 237 class: "tab-content", 238 match view_mode() { 239 ViewMode::Pretty => rsx! { 240 PrettyRecordView { record: record_value, uri: uri().clone(), schema: schema_signal } 241 }, 242 ViewMode::Json => { 243 let json = use_memo(use_reactive!(|record| serde_json::to_string_pretty( 244 &record.value 245 ) 246 .unwrap_or_default())); 247 rsx! { 248 CodeView { 249 code: json, 250 lang: Some("json".to_string()), 251 } 252 } 253 }, 254 ViewMode::Schema => rsx! { 255 SchemaView { schema: schema_signal } 256 }, 257 } 258 } 259 } 260 } 261 } 262 } 263 } else { 264 rsx! {} 265 } 266}