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