tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
prett editor WIP
Orual
2 months ago
0b325539
3e94583b
+1071
-254
2 changed files
expand all
collapse all
unified
split
crates
weaver-app
assets
styling
record-view.css
src
views
record.rs
+131
crates/weaver-app/assets/styling/record-view.css
···
461
padding-left: 0.5rem;
462
margin: 0.25rem 0;
463
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
461
padding-left: 0.5rem;
462
margin: 0.25rem 0;
463
}
464
+
465
+
/* Pretty Editor Input Fields */
466
+
.record-field input[type="text"],
467
+
.record-field input[type="number"] {
468
+
font-family: var(--font-mono);
469
+
font-size: 0.9rem;
470
+
color: var(--color-text);
471
+
background: transparent;
472
+
border: none;
473
+
border-bottom: 1px solid transparent;
474
+
padding: 0.2rem 0 0.1rem 0;
475
+
margin-top: 0.1rem;
476
+
outline: none;
477
+
width: 100%;
478
+
transition: border-color 0.2s;
479
+
}
480
+
481
+
.record-field input[type="text"]:focus,
482
+
.record-field input[type="number"]:focus {
483
+
border-bottom-color: var(--color-primary);
484
+
}
485
+
486
+
.record-field input[type="text"].invalid,
487
+
.record-field input[type="number"].invalid {
488
+
border-bottom-color: var(--color-error, #ff6b6b);
489
+
}
490
+
491
+
.record-field input[type="checkbox"] {
492
+
width: 1.1rem;
493
+
height: 1.1rem;
494
+
margin-top: 0.3rem;
495
+
margin-bottom: 0.3rem;
496
+
cursor: pointer;
497
+
accent-color: var(--color-primary);
498
+
}
499
+
500
+
.field-error {
501
+
font-family: var(--font-mono);
502
+
font-size: 0.75rem;
503
+
color: var(--color-error, #ff6b6b);
504
+
padding-top: 0.2rem;
505
+
padding-bottom: 0.3rem;
506
+
}
507
+
508
+
.add-field-widget {
509
+
display: flex;
510
+
gap: 0.5rem;
511
+
align-items: center;
512
+
padding: 1rem 0 0rem 0rem;
513
+
margin-bottom: 1rem;
514
+
border-left: 1px solid var(--color-border);
515
+
}
516
+
517
+
.add-field-widget input[type="text"] {
518
+
font-family: var(--font-mono);
519
+
font-size: 0.85rem;
520
+
color: var(--color-text);
521
+
background: var(--color-background-alt, rgba(0, 0, 0, 0.2));
522
+
border: 1px solid var(--color-border);
523
+
padding: 0.3rem 0.5rem;
524
+
margin-left: -1px;
525
+
outline: none;
526
+
}
527
+
528
+
.add-field-widget input[type="text"]:focus {
529
+
border-color: var(--color-primary);
530
+
}
531
+
532
+
.add-field-widget button {
533
+
font-family: var(--font-mono);
534
+
font-size: 0.75rem;
535
+
text-transform: uppercase;
536
+
letter-spacing: 0.05em;
537
+
padding: 0.3rem 0.75rem;
538
+
margin-left: -1px;
539
+
background: transparent;
540
+
border: 1px solid var(--color-border);
541
+
color: var(--color-primary);
542
+
cursor: pointer;
543
+
transition: all 0.2s;
544
+
}
545
+
546
+
.add-field-form button {
547
+
font-family: var(--font-mono);
548
+
font-size: 0.75rem;
549
+
text-transform: uppercase;
550
+
letter-spacing: 0.05em;
551
+
padding: 0.36rem 0.75rem;
552
+
margin-left: -1px;
553
+
background: transparent;
554
+
border: 1px solid var(--color-primary);
555
+
color: var(--color-primary);
556
+
cursor: pointer;
557
+
transition: all 0.2s;
558
+
}
559
+
560
+
.add-field-form button:hover {
561
+
background-color: var(--color-primary);
562
+
color: var(--color-surface);
563
+
}
564
+
565
+
.add-field-widget button:hover {
566
+
border: 1px solid var(--color-primary);
567
+
}
568
+
569
+
.add-field-widget button:disabled {
570
+
opacity: 0.4;
571
+
cursor: not-allowed;
572
+
}
573
+
574
+
.field-header {
575
+
display: flex;
576
+
align-items: baseline;
577
+
gap: 0.5rem;
578
+
}
579
+
580
+
.field-remove-button {
581
+
font-family: var(--font-mono);
582
+
font-size: 0.7rem;
583
+
color: var(--color-subtle);
584
+
background: transparent;
585
+
border: none;
586
+
padding: 0.2rem 0.4rem;
587
+
cursor: pointer;
588
+
margin-left: auto;
589
+
transition: color 0.2s;
590
+
}
591
+
592
+
.field-remove-button:hover {
593
+
color: var(--color-error, #ff6b6b);
594
+
}
+940
-254
crates/weaver-app/src/views/record.rs
···
5
use dioxus::prelude::*;
6
use dioxus_logger::tracing::*;
7
use humansize::format_size;
0
0
8
use jacquard::prelude::*;
9
use jacquard::smol_str::ToSmolStr;
10
use jacquard::{
···
12
common::{Data, IntoStatic},
13
identity::lexicon_resolver::LexiconSchemaResolver,
14
smol_str::SmolStr,
15
-
types::{aturi::AtUri, ident::AtIdentifier, string::Nsid},
16
};
17
use weaver_api::com_atproto::repo::{
18
create_record::CreateRecord, delete_record::DeleteRecord, put_record::PutRecord,
···
45
let navigator = use_navigator();
46
47
let client = fetcher.get_client();
48
-
let record = use_resource(move || {
49
let client = client.clone();
50
async move { client.fetch_record_slingshot(&uri()).await }
51
});
···
67
}
68
}
69
});
70
-
if let Some(Ok(record)) = &*record.read_unchecked() {
71
-
let record_value = record.value.clone().into_static();
72
-
let mut edit_data = use_signal(|| record_value.clone());
73
-
let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string()));
74
-
let json = serde_json::to_string_pretty(&record_value).unwrap();
75
rsx! {
76
-
document::Stylesheet { href: asset!("/assets/styling/record-view.css") }
77
-
div {
78
-
class: "record-view-container",
79
-
div {
80
-
class: "record-header",
81
-
h1 { "Record" }
0
0
0
0
0
0
82
div {
83
-
class: "record-metadata",
84
-
div { class: "metadata-row",
85
-
span { class: "metadata-label", "URI" }
86
-
span { class: "metadata-value",
87
-
HighlightedUri { uri: uri().clone() }
88
-
}
89
}
90
-
if let Some(cid) = &record.cid {
91
-
div { class: "metadata-row",
92
-
span { class: "metadata-label", "CID" }
93
-
code { class: "metadata-value", "{cid}" }
94
-
}
95
}
96
-
}
97
-
}
98
-
div {
99
-
class: "tab-bar",
100
-
button {
101
-
class: if view_mode() == ViewMode::Pretty { "tab-button active" } else { "tab-button" },
102
-
onclick: move |_| view_mode.set(ViewMode::Pretty),
103
-
"View"
104
-
}
105
-
button {
106
-
class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" },
107
-
onclick: move |_| view_mode.set(ViewMode::Json),
108
-
"JSON"
109
-
}
110
-
if is_owner() && !edit_mode() {
111
-
{
112
-
let record_value_clone = record_value.clone();
113
-
rsx! {
114
-
button {
115
-
class: "tab-button edit-button",
116
-
onclick: move |_| {
117
-
edit_data.set(record_value_clone.clone());
118
-
edit_mode.set(true);
119
-
},
120
-
"Edit"
121
-
}
122
}
123
}
124
}
125
-
if edit_mode() {
126
-
{
127
-
let record_value_clone = record_value.clone();
128
-
let update_fetcher = fetcher.clone();
129
-
let create_fetcher = fetcher.clone();
130
-
let replace_fetcher = fetcher.clone();
131
-
rsx! {
132
-
ActionButtons {
133
-
on_update: move |_| {
134
-
let fetcher = update_fetcher.clone();
135
-
let uri = uri();
136
-
let data = edit_data();
137
-
spawn(async move {
138
-
if let Some((did, _)) = fetcher.session_info().await {
139
-
if let (Some(collection_str), Some(rkey)) = (uri.collection(), uri.rkey()) {
140
-
let collection = Nsid::new(collection_str.as_str()).ok();
141
-
if let Some(collection) = collection {
142
-
let request = PutRecord::new()
143
-
.repo(AtIdentifier::Did(did))
144
-
.collection(collection)
145
-
.rkey(rkey.clone())
146
-
.record(data)
147
-
.build();
148
-
149
-
match fetcher.send(request).await {
150
-
Ok(_) => {
151
-
dioxus_logger::tracing::info!("Record updated successfully");
152
-
edit_mode.set(false);
153
-
}
154
-
Err(e) => {
155
-
dioxus_logger::tracing::error!("Failed to update record: {:?}", e);
156
-
}
157
-
}
158
-
}
159
-
}
160
-
}
161
-
});
162
-
},
163
-
on_save_new: move |_| {
164
-
let fetcher = create_fetcher.clone();
165
-
let data = edit_data();
166
-
let nav = navigator.clone();
167
-
spawn(async move {
168
-
if let Some((did, _)) = fetcher.session_info().await {
169
-
if let Some(collection_str) = data.type_discriminator() {
170
-
let collection = Nsid::new(collection_str).ok();
171
-
if let Some(collection) = collection {
172
-
let request = CreateRecord::new()
173
-
.repo(AtIdentifier::Did(did))
174
-
.collection(collection)
175
-
.record(data.clone())
176
-
.build();
177
-
178
-
match fetcher.send(request).await {
179
-
Ok(response) => {
180
-
if let Ok(output) = response.into_output() {
181
-
dioxus_logger::tracing::info!("Record created: {}", output.uri);
182
-
nav.push(Route::RecordView { uri: output.uri.to_smolstr() });
183
-
}
184
-
}
185
-
Err(e) => {
186
-
dioxus_logger::tracing::error!("Failed to create record: {:?}", e);
187
-
}
188
-
}
189
-
}
190
-
}
191
-
}
192
-
});
193
-
},
194
-
on_replace: move |_| {
195
-
let fetcher = replace_fetcher.clone();
196
-
let uri = uri();
197
-
let data = edit_data();
198
-
let nav = navigator.clone();
199
-
spawn(async move {
200
-
if let Some((did, _)) = fetcher.session_info().await {
201
-
if let Some(new_collection_str) = data.type_discriminator() {
202
-
let new_collection = Nsid::new(new_collection_str).ok();
203
-
if let Some(new_collection) = new_collection {
204
-
// Create new record
205
-
let create_req = CreateRecord::new()
206
-
.repo(AtIdentifier::Did(did.clone()))
207
-
.collection(new_collection)
208
-
.record(data.clone())
209
-
.build();
210
-
211
-
match fetcher.send(create_req).await {
212
-
Ok(response) => {
213
-
if let Ok(create_output) = response.into_output() {
214
-
// Delete old record
215
-
if let (Some(old_collection_str), Some(old_rkey)) = (uri.collection(), uri.rkey()) {
216
-
let old_collection = Nsid::new(old_collection_str.as_str()).ok();
217
-
if let Some(old_collection) = old_collection {
218
-
let delete_req = DeleteRecord::new()
219
-
.repo(AtIdentifier::Did(did))
220
-
.collection(old_collection)
221
-
.rkey(old_rkey.clone())
222
-
.build();
223
-
224
-
if let Err(e) = fetcher.send(delete_req).await {
225
-
warn!("Created new record but failed to delete old: {:?}", e);
226
-
}
227
-
}
228
-
}
229
-
230
-
info!("Record replaced: {}", create_output.uri);
231
-
nav.push(Route::RecordView { uri: create_output.uri.to_smolstr() });
232
-
}
233
-
}
234
-
Err(e) => {
235
-
error!("Failed to replace record: {:?}", e);
236
-
}
237
-
}
238
-
}
239
-
}
240
-
}
241
-
});
242
-
},
243
-
on_delete: move |_| {
244
-
let fetcher = fetcher.clone();
245
-
let uri = uri();
246
-
let nav = navigator.clone();
247
-
spawn(async move {
248
-
if let Some((did, _)) = fetcher.session_info().await {
249
-
if let (Some(collection_str), Some(rkey)) = (uri.collection(), uri.rkey()) {
250
-
let collection = Nsid::new(collection_str.as_str()).ok();
251
-
if let Some(collection) = collection {
252
-
let request = DeleteRecord::new()
253
-
.repo(AtIdentifier::Did(did))
254
-
.collection(collection)
255
-
.rkey(rkey.clone())
256
-
.build();
257
-
258
-
match fetcher.send(request).await {
259
-
Ok(_) => {
260
-
info!("Record deleted");
261
-
nav.push(Route::Home {});
262
-
}
263
-
Err(e) => {
264
-
error!("Failed to delete record: {:?}", e);
265
-
}
266
-
}
267
-
}
268
-
}
269
-
}
270
-
});
271
-
},
272
-
on_cancel: move |_| {
273
-
edit_data.set(record_value_clone.clone());
274
-
edit_mode.set(false);
275
-
},
276
}
277
-
}
278
}
279
-
}
280
-
}
281
-
div {
282
-
class: "tab-content",
283
-
match (view_mode(), edit_mode()) {
284
-
(ViewMode::Pretty, false) => rsx! {
285
-
PrettyRecordView { record: record_value.clone(), uri: uri().clone() }
286
-
},
287
-
(ViewMode::Json, false) => rsx! {
288
-
CodeView {
289
-
code: use_signal(|| json.clone()),
290
-
lang: Some("json".to_string()),
291
-
}
292
-
},
293
-
(ViewMode::Pretty, true) => rsx! {
294
-
div { "Pretty editor not yet implemented" }
295
-
},
296
-
(ViewMode::Json, true) => rsx! {
297
-
JsonEditor {
298
-
data: edit_data,
299
-
nsid: nsid,
300
-
}
301
-
},
302
}
303
}
304
}
···
444
span { class: "field-value", "{cid}" }
445
}
446
},
447
-
Data::Array(arr) => rsx! {
448
-
div { class: "record-section",
449
-
div { class: "section-label", "{path}" span { class: "array-len", "[{arr.len()}] " } }
0
0
0
0
0
0
0
0
450
451
-
div { class: "section-content",
452
-
for (idx, item) in arr.iter().enumerate() {
453
-
DataView {
454
-
data: item.clone(),
455
-
path: format!("{}[{}]", path, idx),
456
-
did: did.clone()
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
457
}
458
}
459
}
460
}
461
-
},
462
-
Data::Object(obj) => rsx! {
463
-
for (key, value) in obj.iter() {
464
-
{
465
-
let new_path = if path.is_empty() {
466
-
key.to_string()
467
-
} else {
468
-
format!("{}.{}", path, key)
469
-
};
470
-
let did_clone = did.clone();
471
472
-
match value {
473
-
Data::Object(_) | Data::Array(_) => rsx! {
474
-
div { class: "record-section",
475
-
div { class: "section-label", "{key}" }
476
-
div { class: "section-content",
477
-
DataView { data: value.clone(), path: new_path, did: did_clone }
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
478
}
479
}
480
-
},
481
-
_ => rsx! {
482
-
DataView { data: value.clone(), path: new_path, did: did_clone }
483
}
484
}
485
}
486
}
487
-
},
488
Data::Blob(blob) => {
489
let is_image = blob.mime_type.starts_with("image/");
490
let format = blob.mime_type.strip_prefix("image/").unwrap_or("jpeg");
···
872
}
873
}
874
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
875
/// Render some text as markdown.
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
876
#[component]
877
pub fn CodeView(props: CodeViewProps) -> Element {
878
let code = &*props.code.read();
···
5
use dioxus::prelude::*;
6
use dioxus_logger::tracing::*;
7
use humansize::format_size;
8
+
use jacquard::api::com_atproto::repo::get_record::GetRecordOutput;
9
+
use jacquard::client::AgentError;
10
use jacquard::prelude::*;
11
use jacquard::smol_str::ToSmolStr;
12
use jacquard::{
···
14
common::{Data, IntoStatic},
15
identity::lexicon_resolver::LexiconSchemaResolver,
16
smol_str::SmolStr,
17
+
types::{aturi::AtUri, cid::Cid, ident::AtIdentifier, string::Nsid},
18
};
19
use weaver_api::com_atproto::repo::{
20
create_record::CreateRecord, delete_record::DeleteRecord, put_record::PutRecord,
···
47
let navigator = use_navigator();
48
49
let client = fetcher.get_client();
50
+
let record_resource = use_resource(move || {
51
let client = client.clone();
52
async move { client.fetch_record_slingshot(&uri()).await }
53
});
···
69
}
70
}
71
});
72
+
if let Some(Ok(record)) = &*record_resource.read_unchecked() {
73
+
let mut record_value = use_signal(|| record.value.clone().into_static());
74
+
let json =
75
+
use_memo(move || serde_json::to_string_pretty(&record_value()).unwrap_or_default());
76
+
77
rsx! {
78
+
RecordViewLayout {
79
+
uri: uri().clone(),
80
+
cid: record.cid.clone(),
81
+
if edit_mode() {
82
+
EditableRecordContent {
83
+
record_value: record_value,
84
+
uri: uri,
85
+
view_mode: view_mode,
86
+
edit_mode: edit_mode,
87
+
record_resource: record_resource,
88
+
}
89
+
} else {
90
div {
91
+
class: "tab-bar",
92
+
button {
93
+
class: if view_mode() == ViewMode::Pretty { "tab-button active" } else { "tab-button" },
94
+
onclick: move |_| view_mode.set(ViewMode::Pretty),
95
+
"View"
0
96
}
97
+
button {
98
+
class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" },
99
+
onclick: move |_| view_mode.set(ViewMode::Json),
100
+
"JSON"
0
101
}
102
+
if is_owner() {
103
+
button {
104
+
class: "tab-button edit-button",
105
+
onclick: move |_| edit_mode.set(true),
106
+
"Edit"
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
107
}
108
}
109
}
110
+
div {
111
+
class: "tab-content",
112
+
match view_mode() {
113
+
ViewMode::Pretty => rsx! {
114
+
PrettyRecordView { record: record_value(), uri: uri().clone() }
115
+
},
116
+
ViewMode::Json => rsx! {
117
+
CodeView {
118
+
code: use_signal(|| json()),
119
+
lang: Some("json".to_string()),
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
120
}
121
+
},
122
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
123
}
124
}
125
}
···
265
span { class: "field-value", "{cid}" }
266
}
267
},
268
+
Data::Array(arr) => {
269
+
let label = path.split('.').last().unwrap_or(&path);
270
+
rsx! {
271
+
div { class: "record-section",
272
+
div { class: "section-label", "{label}" span { class: "array-len", "[{arr.len()}] " } }
273
+
274
+
div { class: "section-content",
275
+
for (idx, item) in arr.iter().enumerate() {
276
+
{
277
+
let item_path = format!("{}[{}]", path, idx);
278
+
let is_object = matches!(item, Data::Object(_));
279
280
+
if is_object {
281
+
rsx! {
282
+
div { class: "record-section",
283
+
div { class: "section-label", "{item_path}" }
284
+
div { class: "section-content",
285
+
DataView {
286
+
data: item.clone(),
287
+
path: item_path.clone(),
288
+
did: did.clone()
289
+
}
290
+
}
291
+
}
292
+
}
293
+
} else {
294
+
rsx! {
295
+
DataView {
296
+
data: item.clone(),
297
+
path: item_path,
298
+
did: did.clone()
299
+
}
300
+
}
301
+
}
302
+
}
303
}
304
}
305
}
306
}
307
+
}
308
+
Data::Object(obj) => {
309
+
let is_root = path.is_empty();
310
+
let is_array_item = path.contains('[');
0
0
0
0
0
0
311
312
+
if is_root || is_array_item {
313
+
// Root object or array item: just render children (array items already wrapped)
314
+
rsx! {
315
+
for (key, value) in obj.iter() {
316
+
{
317
+
let new_path = if is_root {
318
+
key.to_string()
319
+
} else {
320
+
format!("{}.{}", path, key)
321
+
};
322
+
let did_clone = did.clone();
323
+
rsx! {
324
+
DataView { data: value.clone(), path: new_path, did: did_clone }
325
+
}
326
+
}
327
+
}
328
+
}
329
+
} else {
330
+
// Nested object (not array item): wrap in section
331
+
let label = path.split('.').last().unwrap_or(&path);
332
+
rsx! {
333
+
div { class: "record-section",
334
+
div { class: "section-label", "{label}" }
335
+
div { class: "section-content",
336
+
for (key, value) in obj.iter() {
337
+
{
338
+
let new_path = format!("{}.{}", path, key);
339
+
let did_clone = did.clone();
340
+
rsx! {
341
+
DataView { data: value.clone(), path: new_path, did: did_clone }
342
+
}
343
}
344
}
0
0
0
345
}
346
}
347
}
348
}
349
+
}
350
Data::Blob(blob) => {
351
let is_image = blob.mime_type.starts_with("image/");
352
let format = blob.mime_type.strip_prefix("image/").unwrap_or("jpeg");
···
734
}
735
}
736
737
+
// ============================================================================
738
+
// Pretty Editor: Helper Functions
739
+
// ============================================================================
740
+
741
+
/// Infer Data type from text input
742
+
fn infer_data_from_text(text: &str) -> Result<Data<'static>, String> {
743
+
let trimmed = text.trim();
744
+
745
+
if trimmed == "true" || trimmed == "false" {
746
+
Ok(Data::Boolean(trimmed == "true"))
747
+
} else if trimmed == "{}" {
748
+
use jacquard::types::value::Object;
749
+
use std::collections::BTreeMap;
750
+
Ok(Data::Object(Object(BTreeMap::new())))
751
+
} else if trimmed == "[]" {
752
+
use jacquard::types::value::Array;
753
+
Ok(Data::Array(Array(Vec::new())))
754
+
} else if trimmed == "null" {
755
+
Ok(Data::Null)
756
+
} else if let Ok(num) = trimmed.parse::<i64>() {
757
+
Ok(Data::Integer(num))
758
+
} else {
759
+
// Smart string parsing
760
+
use jacquard::types::value::parsing;
761
+
Ok(Data::String(parsing::parse_string(trimmed).into_static()))
762
+
}
763
+
}
764
+
765
+
/// Parse text as specific AtprotoStr type, preserving type information
766
+
fn try_parse_as_type(
767
+
text: &str,
768
+
string_type: jacquard::types::LexiconStringType,
769
+
) -> Result<jacquard::types::string::AtprotoStr<'static>, String> {
770
+
use jacquard::types::LexiconStringType;
771
+
use jacquard::types::string::*;
772
+
use std::str::FromStr;
773
+
774
+
match string_type {
775
+
LexiconStringType::Datetime => Datetime::from_str(text)
776
+
.map(AtprotoStr::Datetime)
777
+
.map_err(|e| format!("Invalid datetime: {}", e)),
778
+
LexiconStringType::Did => Did::new(text)
779
+
.map(|v| AtprotoStr::Did(v.into_static()))
780
+
.map_err(|e| format!("Invalid DID: {}", e)),
781
+
LexiconStringType::Handle => Handle::new(text)
782
+
.map(|v| AtprotoStr::Handle(v.into_static()))
783
+
.map_err(|e| format!("Invalid handle: {}", e)),
784
+
LexiconStringType::AtUri => AtUri::new(text)
785
+
.map(|v| AtprotoStr::AtUri(v.into_static()))
786
+
.map_err(|e| format!("Invalid AT-URI: {}", e)),
787
+
LexiconStringType::AtIdentifier => AtIdentifier::new(text)
788
+
.map(|v| AtprotoStr::AtIdentifier(v.into_static()))
789
+
.map_err(|e| format!("Invalid identifier: {}", e)),
790
+
LexiconStringType::Nsid => Nsid::new(text)
791
+
.map(|v| AtprotoStr::Nsid(v.into_static()))
792
+
.map_err(|e| format!("Invalid NSID: {}", e)),
793
+
LexiconStringType::Tid => Tid::new(text)
794
+
.map(|v| AtprotoStr::Tid(v.into_static()))
795
+
.map_err(|e| format!("Invalid TID: {}", e)),
796
+
LexiconStringType::RecordKey => Rkey::new(text)
797
+
.map(|rk| AtprotoStr::RecordKey(RecordKey::from(rk)))
798
+
.map_err(|e| format!("Invalid record key: {}", e)),
799
+
LexiconStringType::Cid => Cid::new(text.as_bytes())
800
+
.map(|v| AtprotoStr::Cid(v.into_static()))
801
+
.map_err(|_| "Invalid CID".to_string()),
802
+
LexiconStringType::Language => Language::new(text)
803
+
.map(AtprotoStr::Language)
804
+
.map_err(|e| format!("Invalid language: {}", e)),
805
+
LexiconStringType::Uri(_) => Uri::new(text)
806
+
.map(|u| AtprotoStr::Uri(u.into_static()))
807
+
.map_err(|e| format!("Invalid URI: {}", e)),
808
+
LexiconStringType::String => {
809
+
// Plain strings: use smart inference
810
+
use jacquard::types::value::parsing;
811
+
Ok(parsing::parse_string(text).into_static())
812
+
}
813
+
}
814
+
}
815
+
816
+
// ============================================================================
817
+
// Pretty Editor: Component Hierarchy
818
+
// ============================================================================
819
+
820
+
/// Main dispatcher - routes to specific field editors based on Data type
821
+
#[component]
822
+
fn EditableDataView(
823
+
root: Signal<Data<'static>>,
824
+
path: String,
825
+
did: String,
826
+
#[props(default)] remove_button: Option<Element>,
827
+
) -> Element {
828
+
let path_for_memo = path.clone();
829
+
let root_read = root.read();
830
+
831
+
match root_read
832
+
.get_at_path(&path_for_memo)
833
+
.map(|d| d.clone().into_static())
834
+
{
835
+
Some(Data::Object(_)) => rsx! { EditableObjectField { root, path: path.clone(), did } },
836
+
Some(Data::String(_)) => {
837
+
rsx! { EditableStringField { root, path: path.clone(), remove_button } }
838
+
}
839
+
Some(Data::Integer(_)) => {
840
+
rsx! { EditableIntegerField { root, path: path.clone(), remove_button } }
841
+
}
842
+
Some(Data::Boolean(_)) => {
843
+
rsx! { EditableBooleanField { root, path: path.clone(), remove_button } }
844
+
}
845
+
Some(Data::Null) => rsx! { EditableNullField { root, path: path.clone(), remove_button } },
846
+
847
+
// Not yet implemented - fall back to read-only DataView
848
+
Some(data) => {
849
+
rsx! { DataView { data: data.clone(), path: path.clone(), did } }
850
+
}
851
+
852
+
None => rsx! { div { class: "field-error", "❌ Path not found: {path}" } },
853
+
}
854
+
}
855
+
856
+
// ============================================================================
857
+
// Primitive Field Editors
858
+
// ============================================================================
859
+
860
+
/// String field with type preservation
861
+
#[component]
862
+
fn EditableStringField(
863
+
root: Signal<Data<'static>>,
864
+
path: String,
865
+
#[props(default)] remove_button: Option<Element>,
866
+
) -> Element {
867
+
use jacquard::types::LexiconStringType;
868
+
869
+
let path_for_text = path.clone();
870
+
let path_for_type = path.clone();
871
+
872
+
// Get current string value
873
+
let current_text = use_memo(move || {
874
+
root.read()
875
+
.get_at_path(&path_for_text)
876
+
.and_then(|d| d.as_str())
877
+
.map(|s| s.to_string())
878
+
.unwrap_or_default()
879
+
});
880
+
881
+
// Get string type (Copy, cheap to store)
882
+
let string_type = use_memo(move || {
883
+
root.read()
884
+
.get_at_path(&path_for_type)
885
+
.and_then(|d| match d {
886
+
Data::String(s) => Some(s.string_type()),
887
+
_ => None,
888
+
})
889
+
.unwrap_or(LexiconStringType::String)
890
+
});
891
+
892
+
// Local state for invalid input
893
+
let mut input_text = use_signal(|| current_text());
894
+
let mut parse_error = use_signal(|| None::<String>);
895
+
896
+
// Sync input when current changes
897
+
use_effect(move || {
898
+
input_text.set(current_text());
899
+
});
900
+
901
+
let path_for_mutation = path.clone();
902
+
let handle_input = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| {
903
+
let new_text = evt.value();
904
+
input_text.set(new_text.clone());
905
+
906
+
match try_parse_as_type(&new_text, string_type()) {
907
+
Ok(new_atproto_str) => {
908
+
parse_error.set(None);
909
+
let mut new_data = root.read().clone();
910
+
new_data.set_at_path(&path_for_mutation, Data::String(new_atproto_str));
911
+
root.set(new_data);
912
+
}
913
+
Err(e) => {
914
+
parse_error.set(Some(e));
915
+
}
916
+
}
917
+
};
918
+
919
+
let type_label = format!("{:?}", string_type()).to_lowercase();
920
+
921
+
rsx! {
922
+
div { class: "record-field",
923
+
div { class: "field-header",
924
+
PathLabel { path: path.clone() }
925
+
if type_label != "string" {
926
+
span { class: "string-type-tag", " [{type_label}]" }
927
+
}
928
+
{remove_button}
929
+
}
930
+
input {
931
+
r#type: "text",
932
+
value: "{input_text}",
933
+
oninput: handle_input,
934
+
class: if parse_error().is_some() { "invalid" } else { "" },
935
+
}
936
+
if let Some(err) = parse_error() {
937
+
span { class: "field-error", " ❌ {err}" }
938
+
}
939
+
}
940
+
}
941
+
}
942
+
943
+
/// Integer field with validation
944
+
#[component]
945
+
fn EditableIntegerField(
946
+
root: Signal<Data<'static>>,
947
+
path: String,
948
+
#[props(default)] remove_button: Option<Element>,
949
+
) -> Element {
950
+
let path_for_memo = path.clone();
951
+
let current_value = use_memo(move || {
952
+
root.read()
953
+
.get_at_path(&path_for_memo)
954
+
.and_then(|d| d.as_integer())
955
+
.unwrap_or(0)
956
+
});
957
+
958
+
let mut input_text = use_signal(|| current_value().to_string());
959
+
let mut parse_error = use_signal(|| None::<String>);
960
+
961
+
use_effect(move || {
962
+
input_text.set(current_value().to_string());
963
+
});
964
+
965
+
let path_for_mutation = path.clone();
966
+
967
+
rsx! {
968
+
div { class: "record-field",
969
+
div { class: "field-header",
970
+
PathLabel { path: path.clone() }
971
+
{remove_button}
972
+
}
973
+
input {
974
+
r#type: "number",
975
+
value: "{input_text}",
976
+
oninput: move |evt| {
977
+
let text = evt.value();
978
+
input_text.set(text.clone());
979
+
980
+
match text.parse::<i64>() {
981
+
Ok(num) => {
982
+
parse_error.set(None);
983
+
let mut data_edit = root.write_unchecked();
984
+
data_edit.set_at_path(&path_for_mutation, Data::Integer(num));
985
+
}
986
+
Err(_) => {
987
+
parse_error.set(Some("Must be a valid integer".to_string()));
988
+
}
989
+
}
990
+
}
991
+
}
992
+
if let Some(err) = parse_error() {
993
+
span { class: "field-error", " ❌ {err}" }
994
+
}
995
+
}
996
+
}
997
+
}
998
+
999
+
/// Boolean field (checkbox)
1000
+
#[component]
1001
+
fn EditableBooleanField(
1002
+
root: Signal<Data<'static>>,
1003
+
path: String,
1004
+
#[props(default)] remove_button: Option<Element>,
1005
+
) -> Element {
1006
+
let path_for_memo = path.clone();
1007
+
let current_value = use_memo(move || {
1008
+
root.read()
1009
+
.get_at_path(&path_for_memo)
1010
+
.and_then(|d| d.as_boolean())
1011
+
.unwrap_or(false)
1012
+
});
1013
+
1014
+
let path_for_mutation = path.clone();
1015
+
rsx! {
1016
+
div { class: "record-field",
1017
+
div { class: "field-header",
1018
+
PathLabel { path: path.clone() }
1019
+
{remove_button}
1020
+
}
1021
+
input {
1022
+
r#type: "checkbox",
1023
+
checked: current_value(),
1024
+
onchange: move |evt| {
1025
+
root.with_mut(|data| {
1026
+
if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) {
1027
+
*target = Data::Boolean(evt.checked());
1028
+
}
1029
+
});
1030
+
}
1031
+
}
1032
+
}
1033
+
}
1034
+
}
1035
+
1036
+
/// Null field with type inference
1037
+
#[component]
1038
+
fn EditableNullField(
1039
+
root: Signal<Data<'static>>,
1040
+
path: String,
1041
+
#[props(default)] remove_button: Option<Element>,
1042
+
) -> Element {
1043
+
let mut input_text = use_signal(|| String::new());
1044
+
let mut parse_error = use_signal(|| None::<String>);
1045
+
1046
+
let path_for_mutation = path.clone();
1047
+
rsx! {
1048
+
div { class: "record-field",
1049
+
div { class: "field-header",
1050
+
PathLabel { path: path.clone() }
1051
+
span { class: "field-value muted", "null" }
1052
+
{remove_button}
1053
+
}
1054
+
input {
1055
+
r#type: "text",
1056
+
placeholder: "Enter value (or {{}}, [], true, 123)...",
1057
+
value: "{input_text}",
1058
+
oninput: move |evt| {
1059
+
input_text.set(evt.value());
1060
+
},
1061
+
onkeydown: move |evt| {
1062
+
use dioxus::prelude::keyboard_types::Key;
1063
+
if evt.key() == Key::Enter {
1064
+
let text = input_text();
1065
+
match infer_data_from_text(&text) {
1066
+
Ok(new_value) => {
1067
+
root.with_mut(|data| {
1068
+
if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) {
1069
+
*target = new_value;
1070
+
}
1071
+
});
1072
+
input_text.set(String::new());
1073
+
parse_error.set(None);
1074
+
}
1075
+
Err(e) => {
1076
+
parse_error.set(Some(e));
1077
+
}
1078
+
}
1079
+
}
1080
+
}
1081
+
}
1082
+
if let Some(err) = parse_error() {
1083
+
span { class: "field-error", " ❌ {err}" }
1084
+
}
1085
+
}
1086
+
}
1087
+
}
1088
+
1089
+
// ============================================================================
1090
+
// Field with Remove Button Wrapper
1091
+
// ============================================================================
1092
+
1093
+
/// Wraps a field with an optional remove button in the header
1094
+
#[component]
1095
+
fn FieldWithRemove(
1096
+
root: Signal<Data<'static>>,
1097
+
path: String,
1098
+
did: String,
1099
+
is_removable: bool,
1100
+
parent_path: String,
1101
+
field_key: String,
1102
+
) -> Element {
1103
+
let remove_button = if is_removable {
1104
+
Some(rsx! {
1105
+
button {
1106
+
class: "field-remove-button",
1107
+
onclick: move |_| {
1108
+
let mut new_data = root.read().clone();
1109
+
if let Some(Data::Object(obj)) = new_data.get_at_path_mut(parent_path.as_str()) {
1110
+
obj.0.remove(field_key.as_str());
1111
+
}
1112
+
root.set(new_data);
1113
+
},
1114
+
"Remove"
1115
+
}
1116
+
})
1117
+
} else {
1118
+
None
1119
+
};
1120
+
1121
+
rsx! {
1122
+
EditableDataView {
1123
+
root: root,
1124
+
path: path.clone(),
1125
+
did: did.clone(),
1126
+
remove_button: remove_button,
1127
+
}
1128
+
}
1129
+
}
1130
+
1131
+
// ============================================================================
1132
+
// Object Field Editor (enables recursion)
1133
+
// ============================================================================
1134
+
1135
+
/// Object field - iterates fields and renders child EditableDataView for each
1136
+
#[component]
1137
+
fn EditableObjectField(root: Signal<Data<'static>>, path: String, did: String) -> Element {
1138
+
let path_for_memo = path.clone();
1139
+
let field_keys = use_memo(move || {
1140
+
root.read()
1141
+
.get_at_path(&path_for_memo)
1142
+
.and_then(|d| d.as_object())
1143
+
.map(|obj| obj.0.keys().cloned().collect::<Vec<_>>())
1144
+
.unwrap_or_default()
1145
+
});
1146
+
1147
+
let is_root = path.is_empty();
1148
+
1149
+
rsx! {
1150
+
if !is_root {
1151
+
div { class: "record-section object-section",
1152
+
div { class: "section-label",
1153
+
{
1154
+
let parts: Vec<&str> = path.split('.').collect();
1155
+
let final_part = parts.last().unwrap_or(&"");
1156
+
rsx! { "{final_part}" }
1157
+
}
1158
+
}
1159
+
div { class: "section-content",
1160
+
for key in field_keys() {
1161
+
{
1162
+
let field_path = if path.is_empty() {
1163
+
key.to_string()
1164
+
} else {
1165
+
format!("{}.{}", path, key)
1166
+
};
1167
+
let is_type_field = key == "$type";
1168
+
1169
+
rsx! {
1170
+
FieldWithRemove {
1171
+
key: "{field_path}",
1172
+
root: root,
1173
+
path: field_path.clone(),
1174
+
did: did.clone(),
1175
+
is_removable: !is_type_field,
1176
+
parent_path: path.clone(),
1177
+
field_key: key.clone(),
1178
+
}
1179
+
}
1180
+
}
1181
+
}
1182
+
1183
+
AddFieldWidget { root: root, path: path.clone() }
1184
+
}
1185
+
}
1186
+
} else {
1187
+
for key in field_keys() {
1188
+
{
1189
+
let field_path = key.to_string();
1190
+
let is_type_field = key == "$type";
1191
+
1192
+
rsx! {
1193
+
FieldWithRemove {
1194
+
key: "{field_path}",
1195
+
root: root,
1196
+
path: field_path.clone(),
1197
+
did: did.clone(),
1198
+
is_removable: !is_type_field,
1199
+
parent_path: path.clone(),
1200
+
field_key: key.clone(),
1201
+
}
1202
+
}
1203
+
}
1204
+
}
1205
+
1206
+
AddFieldWidget { root: root, path: path.clone() }
1207
+
}
1208
+
}
1209
+
}
1210
+
1211
+
/// Widget for adding new fields to objects
1212
+
#[component]
1213
+
fn AddFieldWidget(root: Signal<Data<'static>>, path: String) -> Element {
1214
+
let mut field_name = use_signal(|| String::new());
1215
+
let mut field_value = use_signal(|| String::new());
1216
+
let mut error = use_signal(|| None::<String>);
1217
+
let mut show_form = use_signal(|| false);
1218
+
1219
+
let path_for_enter = path.clone();
1220
+
let path_for_button = path.clone();
1221
+
1222
+
rsx! {
1223
+
div { class: "add-field-widget",
1224
+
if !show_form() {
1225
+
button {
1226
+
class: "add-button",
1227
+
onclick: move |_| show_form.set(true),
1228
+
"+ Add Field"
1229
+
}
1230
+
} else {
1231
+
div { class: "add-field-form",
1232
+
input {
1233
+
r#type: "text",
1234
+
placeholder: "Field name",
1235
+
value: "{field_name}",
1236
+
oninput: move |evt| field_name.set(evt.value()),
1237
+
}
1238
+
input {
1239
+
r#type: "text",
1240
+
placeholder: r#"Value: {{}}, [], true, 123, "text""#,
1241
+
value: "{field_value}",
1242
+
oninput: move |evt| field_value.set(evt.value()),
1243
+
onkeydown: move |evt| {
1244
+
use dioxus::prelude::keyboard_types::Key;
1245
+
if evt.key() == Key::Enter {
1246
+
let name = field_name();
1247
+
let value_text = field_value();
1248
+
1249
+
if name.is_empty() {
1250
+
error.set(Some("Field name required".to_string()));
1251
+
return;
1252
+
}
1253
+
1254
+
let new_value = match infer_data_from_text(&value_text) {
1255
+
Ok(data) => data,
1256
+
Err(e) => {
1257
+
error.set(Some(e));
1258
+
return;
1259
+
}
1260
+
};
1261
+
1262
+
let mut new_data = root.read().clone();
1263
+
if let Some(Data::Object(obj)) = new_data.get_at_path_mut(path_for_enter.as_str()) {
1264
+
obj.0.insert(name.into(), new_value);
1265
+
}
1266
+
root.set(new_data);
1267
+
1268
+
// Reset form
1269
+
field_name.set(String::new());
1270
+
field_value.set(String::new());
1271
+
show_form.set(false);
1272
+
error.set(None);
1273
+
}
1274
+
}
1275
+
}
1276
+
button {
1277
+
class: "add-field-widget-edit",
1278
+
onclick: move |_| {
1279
+
let name = field_name();
1280
+
let value_text = field_value();
1281
+
1282
+
if name.is_empty() {
1283
+
error.set(Some("Field name required".to_string()));
1284
+
return;
1285
+
}
1286
+
1287
+
let new_value = match infer_data_from_text(&value_text) {
1288
+
Ok(data) => data,
1289
+
Err(e) => {
1290
+
error.set(Some(e));
1291
+
return;
1292
+
}
1293
+
};
1294
+
1295
+
let mut new_data = root.read().clone();
1296
+
if let Some(Data::Object(obj)) = new_data.get_at_path_mut(path_for_button.as_str()) {
1297
+
obj.0.insert(name.into(), new_value);
1298
+
}
1299
+
root.set(new_data);
1300
+
1301
+
// Reset form
1302
+
field_name.set(String::new());
1303
+
field_value.set(String::new());
1304
+
show_form.set(false);
1305
+
error.set(None);
1306
+
},
1307
+
"Add"
1308
+
}
1309
+
button {
1310
+
class: "add-field-widget-edit",
1311
+
onclick: move |_| {
1312
+
show_form.set(false);
1313
+
field_name.set(String::new());
1314
+
field_value.set(String::new());
1315
+
error.set(None);
1316
+
},
1317
+
"Cancel"
1318
+
}
1319
+
if let Some(err) = error() {
1320
+
div { class: "field-error", "❌ {err}" }
1321
+
}
1322
+
}
1323
+
}
1324
+
}
1325
+
}
1326
+
}
1327
+
1328
+
/// Layout component for record view - handles header, metadata, and wraps children
1329
+
#[component]
1330
+
fn RecordViewLayout(uri: AtUri<'static>, cid: Option<Cid<'static>>, children: Element) -> Element {
1331
+
rsx! {
1332
+
document::Stylesheet { href: asset!("/assets/styling/record-view.css") }
1333
+
div {
1334
+
class: "record-view-container",
1335
+
div {
1336
+
class: "record-header",
1337
+
h1 { "Record" }
1338
+
div {
1339
+
class: "record-metadata",
1340
+
div { class: "metadata-row",
1341
+
span { class: "metadata-label", "URI" }
1342
+
span { class: "metadata-value",
1343
+
HighlightedUri { uri: uri.clone() }
1344
+
}
1345
+
}
1346
+
if let Some(cid) = cid {
1347
+
div { class: "metadata-row",
1348
+
span { class: "metadata-label", "CID" }
1349
+
code { class: "metadata-value", "{cid}" }
1350
+
}
1351
+
}
1352
+
}
1353
+
}
1354
+
{children}
1355
+
}
1356
+
}
1357
+
}
1358
+
1359
/// Render some text as markdown.
1360
+
#[component]
1361
+
fn EditableRecordContent(
1362
+
record_value: Signal<Data<'static>>,
1363
+
uri: ReadSignal<AtUri<'static>>,
1364
+
view_mode: Signal<ViewMode>,
1365
+
edit_mode: Signal<bool>,
1366
+
record_resource: Resource<Result<GetRecordOutput<'static>, AgentError>>,
1367
+
) -> Element {
1368
+
let mut edit_data = use_signal(|| record_value());
1369
+
let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string()));
1370
+
let navigator = use_navigator();
1371
+
let fetcher = use_context::<CachedFetcher>();
1372
+
1373
+
let update_fetcher = fetcher.clone();
1374
+
let create_fetcher = fetcher.clone();
1375
+
let replace_fetcher = fetcher.clone();
1376
+
let delete_fetcher = fetcher.clone();
1377
+
1378
+
rsx! {
1379
+
div {
1380
+
class: "tab-bar",
1381
+
button {
1382
+
class: if view_mode() == ViewMode::Pretty { "tab-button active" } else { "tab-button" },
1383
+
onclick: move |_| view_mode.set(ViewMode::Pretty),
1384
+
"View"
1385
+
}
1386
+
button {
1387
+
class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" },
1388
+
onclick: move |_| view_mode.set(ViewMode::Json),
1389
+
"JSON"
1390
+
}
1391
+
ActionButtons {
1392
+
on_update: move |_| {
1393
+
let fetcher = update_fetcher.clone();
1394
+
let uri = uri();
1395
+
let data = edit_data();
1396
+
spawn(async move {
1397
+
if let Some((did, _)) = fetcher.session_info().await {
1398
+
if let (Some(collection_str), Some(rkey)) = (uri.collection(), uri.rkey()) {
1399
+
let collection = Nsid::new(collection_str.as_str()).ok();
1400
+
if let Some(collection) = collection {
1401
+
let request = PutRecord::new()
1402
+
.repo(AtIdentifier::Did(did))
1403
+
.collection(collection)
1404
+
.rkey(rkey.clone())
1405
+
.record(data.clone())
1406
+
.build();
1407
+
1408
+
match fetcher.send(request).await {
1409
+
Ok(output) => {
1410
+
if output.status() == StatusCode::OK {
1411
+
dioxus_logger::tracing::info!("Record updated successfully");
1412
+
record_value.set(data);
1413
+
edit_mode.set(false);
1414
+
} else {
1415
+
dioxus_logger::tracing::error!("Unexpected status code: {:?}", output.status());
1416
+
}
1417
+
}
1418
+
Err(e) => {
1419
+
dioxus_logger::tracing::error!("Failed to update record: {:?}", e);
1420
+
}
1421
+
}
1422
+
}
1423
+
}
1424
+
}
1425
+
});
1426
+
},
1427
+
on_save_new: move |_| {
1428
+
let fetcher = create_fetcher.clone();
1429
+
let data = edit_data();
1430
+
let nav = navigator.clone();
1431
+
spawn(async move {
1432
+
if let Some((did, _)) = fetcher.session_info().await {
1433
+
if let Some(collection_str) = data.type_discriminator() {
1434
+
let collection = Nsid::new(collection_str).ok();
1435
+
if let Some(collection) = collection {
1436
+
let request = CreateRecord::new()
1437
+
.repo(AtIdentifier::Did(did))
1438
+
.collection(collection)
1439
+
.record(data.clone())
1440
+
.build();
1441
+
1442
+
match fetcher.send(request).await {
1443
+
Ok(response) => {
1444
+
if let Ok(output) = response.into_output() {
1445
+
dioxus_logger::tracing::info!("Record created: {}", output.uri);
1446
+
nav.push(Route::RecordView { uri: output.uri.to_smolstr() });
1447
+
}
1448
+
}
1449
+
Err(e) => {
1450
+
dioxus_logger::tracing::error!("Failed to create record: {:?}", e);
1451
+
}
1452
+
}
1453
+
}
1454
+
}
1455
+
}
1456
+
});
1457
+
},
1458
+
on_replace: move |_| {
1459
+
let fetcher = replace_fetcher.clone();
1460
+
let uri = uri();
1461
+
let data = edit_data();
1462
+
let nav = navigator.clone();
1463
+
spawn(async move {
1464
+
if let Some((did, _)) = fetcher.session_info().await {
1465
+
if let Some(new_collection_str) = data.type_discriminator() {
1466
+
let new_collection = Nsid::new(new_collection_str).ok();
1467
+
if let Some(new_collection) = new_collection {
1468
+
// Create new record
1469
+
let create_req = CreateRecord::new()
1470
+
.repo(AtIdentifier::Did(did.clone()))
1471
+
.collection(new_collection)
1472
+
.record(data.clone())
1473
+
.build();
1474
+
1475
+
match fetcher.send(create_req).await {
1476
+
Ok(response) => {
1477
+
if let Ok(create_output) = response.into_output() {
1478
+
// Delete old record
1479
+
if let (Some(old_collection_str), Some(old_rkey)) = (uri.collection(), uri.rkey()) {
1480
+
let old_collection = Nsid::new(old_collection_str.as_str()).ok();
1481
+
if let Some(old_collection) = old_collection {
1482
+
let delete_req = DeleteRecord::new()
1483
+
.repo(AtIdentifier::Did(did))
1484
+
.collection(old_collection)
1485
+
.rkey(old_rkey.clone())
1486
+
.build();
1487
+
1488
+
if let Err(e) = fetcher.send(delete_req).await {
1489
+
dioxus_logger::tracing::warn!("Created new record but failed to delete old: {:?}", e);
1490
+
}
1491
+
}
1492
+
}
1493
+
1494
+
dioxus_logger::tracing::info!("Record replaced: {}", create_output.uri);
1495
+
nav.push(Route::RecordView { uri: create_output.uri.to_smolstr() });
1496
+
}
1497
+
}
1498
+
Err(e) => {
1499
+
dioxus_logger::tracing::error!("Failed to replace record: {:?}", e);
1500
+
}
1501
+
}
1502
+
}
1503
+
}
1504
+
}
1505
+
});
1506
+
},
1507
+
on_delete: move |_| {
1508
+
let fetcher = delete_fetcher.clone();
1509
+
let uri = uri();
1510
+
let nav = navigator.clone();
1511
+
spawn(async move {
1512
+
if let Some((did, _)) = fetcher.session_info().await {
1513
+
if let (Some(collection_str), Some(rkey)) = (uri.collection(), uri.rkey()) {
1514
+
let collection = Nsid::new(collection_str.as_str()).ok();
1515
+
if let Some(collection) = collection {
1516
+
let request = DeleteRecord::new()
1517
+
.repo(AtIdentifier::Did(did))
1518
+
.collection(collection)
1519
+
.rkey(rkey.clone())
1520
+
.build();
1521
+
1522
+
match fetcher.send(request).await {
1523
+
Ok(_) => {
1524
+
dioxus_logger::tracing::info!("Record deleted");
1525
+
nav.push(Route::Home {});
1526
+
}
1527
+
Err(e) => {
1528
+
dioxus_logger::tracing::error!("Failed to delete record: {:?}", e);
1529
+
}
1530
+
}
1531
+
}
1532
+
}
1533
+
}
1534
+
});
1535
+
},
1536
+
on_cancel: move |_| {
1537
+
edit_data.set(record_value());
1538
+
edit_mode.set(false);
1539
+
},
1540
+
}
1541
+
}
1542
+
div {
1543
+
class: "tab-content",
1544
+
match view_mode() {
1545
+
ViewMode::Pretty => rsx! {
1546
+
div { class: "pretty-record",
1547
+
EditableDataView {
1548
+
root: edit_data,
1549
+
path: String::new(),
1550
+
did: uri().authority().to_string(),
1551
+
}
1552
+
}
1553
+
},
1554
+
ViewMode::Json => rsx! {
1555
+
JsonEditor { data: edit_data, nsid }
1556
+
},
1557
+
}
1558
+
}
1559
+
}
1560
+
}
1561
+
1562
#[component]
1563
pub fn CodeView(props: CodeViewProps) -> Element {
1564
let code = &*props.code.read();