Skip to content

Commit 34486ea

Browse files
authored
38 feat add support for co relrec relspec relsub domains (#284)
* feat: add support for co, relrec, relspec, and relsub domains with updated snapshots and export logic * feat: add support for RELREC, RELSPEC, and RELSUB domains with builder logic and UI integration * feat: refactor domain state to remove generated domains and enforce source-mapped structure * feat: refactor export data builder to unify domain handling and improve clarity
1 parent fbe01fa commit 34486ea

File tree

22 files changed

+1210
-322
lines changed

22 files changed

+1210
-322
lines changed

crates/tss-gui/src/app/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ impl App {
381381
if let Some(study) = &mut self.state.study
382382
&& let Some(domain_state) = study.domain_mut(&domain)
383383
{
384-
domain_state.validation_cache = Some(report);
384+
domain_state.set_validation_cache(report);
385385
}
386386
Task::none()
387387
}

crates/tss-gui/src/handler/domain_editor.rs

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,14 @@ fn trigger_preview_rebuild(state: &AppState, domain_code: &str) -> Task<Message>
6464
return Task::none();
6565
};
6666

67+
// Only source domains have mapping/normalization for preview
68+
let Some(src) = domain.as_source() else {
69+
return Task::none();
70+
};
71+
6772
let input = PreviewInput {
68-
source_df: domain.source.data.clone(),
69-
mapping: domain.mapping.clone(),
73+
source_df: src.source.data.clone(),
74+
mapping: src.mapping.clone(),
7075
ct_registry: state.terminology.clone(),
7176
};
7277

@@ -118,11 +123,12 @@ fn handle_mapping_message(state: &mut AppState, msg: MappingMessage) -> Task<Mes
118123
.study
119124
.as_mut()
120125
.and_then(|s| s.domain_mut(&domain_code))
126+
.and_then(|d| d.as_source_mut())
121127
{
122128
if let Err(e) = domain.mapping.accept_suggestion(&variable) {
123129
tracing::error!(variable = %variable, error = %e, "Failed to accept suggestion");
124130
}
125-
domain.invalidate_validation();
131+
domain.validation_cache = None;
126132
state.dirty_tracker.mark_dirty();
127133
}
128134
if let ViewState::DomainEditor(editor) = &mut state.view {
@@ -140,9 +146,10 @@ fn handle_mapping_message(state: &mut AppState, msg: MappingMessage) -> Task<Mes
140146
.study
141147
.as_mut()
142148
.and_then(|s| s.domain_mut(&domain_code))
149+
.and_then(|d| d.as_source_mut())
143150
{
144151
domain.mapping.clear_assignment(&variable);
145-
domain.invalidate_validation();
152+
domain.validation_cache = None;
146153
state.dirty_tracker.mark_dirty();
147154
}
148155
if let ViewState::DomainEditor(editor) = &mut state.view {
@@ -160,11 +167,12 @@ fn handle_mapping_message(state: &mut AppState, msg: MappingMessage) -> Task<Mes
160167
.study
161168
.as_mut()
162169
.and_then(|s| s.domain_mut(&domain_code))
170+
.and_then(|d| d.as_source_mut())
163171
{
164172
if let Err(e) = domain.mapping.accept_manual(&variable, &column) {
165173
tracing::error!(variable = %variable, column = %column, error = %e, "Failed to accept manual mapping");
166174
}
167-
domain.invalidate_validation();
175+
domain.validation_cache = None;
168176
state.dirty_tracker.mark_dirty();
169177
}
170178
if let ViewState::DomainEditor(editor) = &mut state.view {
@@ -204,9 +212,10 @@ fn handle_mapping_message(state: &mut AppState, msg: MappingMessage) -> Task<Mes
204212
.study
205213
.as_mut()
206214
.and_then(|s| s.domain_mut(&domain_code))
215+
.and_then(|d| d.as_source_mut())
207216
{
208217
let _ = domain.mapping.mark_not_collected(&variable, &reason);
209-
domain.invalidate_validation();
218+
domain.validation_cache = None;
210219
state.dirty_tracker.mark_dirty();
211220
}
212221
if let ViewState::DomainEditor(editor) = &mut state.view {
@@ -245,9 +254,10 @@ fn handle_mapping_message(state: &mut AppState, msg: MappingMessage) -> Task<Mes
245254
.study
246255
.as_mut()
247256
.and_then(|s| s.domain_mut(&domain_code))
257+
.and_then(|d| d.as_source_mut())
248258
{
249259
domain.mapping.clear_assignment(&variable);
250-
domain.invalidate_validation();
260+
domain.validation_cache = None;
251261
state.dirty_tracker.mark_dirty();
252262
}
253263
if let ViewState::DomainEditor(editor) = &mut state.view {
@@ -265,9 +275,10 @@ fn handle_mapping_message(state: &mut AppState, msg: MappingMessage) -> Task<Mes
265275
.study
266276
.as_mut()
267277
.and_then(|s| s.domain_mut(&domain_code))
278+
.and_then(|d| d.as_source_mut())
268279
{
269280
let _ = domain.mapping.mark_omit(&variable);
270-
domain.invalidate_validation();
281+
domain.validation_cache = None;
271282
state.dirty_tracker.mark_dirty();
272283
}
273284
if let ViewState::DomainEditor(editor) = &mut state.view {
@@ -285,9 +296,10 @@ fn handle_mapping_message(state: &mut AppState, msg: MappingMessage) -> Task<Mes
285296
.study
286297
.as_mut()
287298
.and_then(|s| s.domain_mut(&domain_code))
299+
.and_then(|d| d.as_source_mut())
288300
{
289301
domain.mapping.clear_assignment(&variable);
290-
domain.invalidate_validation();
302+
domain.validation_cache = None;
291303
state.dirty_tracker.mark_dirty();
292304
}
293305
if let ViewState::DomainEditor(editor) = &mut state.view {
@@ -348,20 +360,25 @@ fn handle_validation_message(state: &mut AppState, msg: ValidationMessage) -> Ta
348360
return Task::none();
349361
};
350362

363+
// Only source domains support refresh validation
364+
let Some(src) = domain.as_source() else {
365+
return Task::none();
366+
};
367+
351368
let df = match &state.view {
352369
ViewState::DomainEditor(editor) => {
353370
// Use preview cache if available, otherwise clone from Arc<DataFrame>
354371
editor
355372
.preview_cache
356373
.clone()
357-
.unwrap_or_else(|| (*domain.source.data).clone())
374+
.unwrap_or_else(|| (*src.source.data).clone())
358375
}
359-
_ => (*domain.source.data).clone(),
376+
_ => (*src.source.data).clone(),
360377
};
361378

362-
let sdtm_domain = domain.mapping.domain().clone();
379+
let sdtm_domain = src.mapping.domain().clone();
363380
let not_collected: std::collections::BTreeSet<String> =
364-
domain.mapping.all_not_collected().keys().cloned().collect();
381+
src.mapping.all_not_collected().keys().cloned().collect();
365382

366383
let input = ValidationInput {
367384
domain: sdtm_domain,
@@ -410,8 +427,10 @@ fn handle_validation_message(state: &mut AppState, msg: ValidationMessage) -> Ta
410427
ValidationMessage::GoToIssueSource { variable } => {
411428
if let ViewState::DomainEditor(editor) = &mut state.view {
412429
editor.tab = EditorTab::Mapping;
413-
if let Some(domain) = state.study.as_ref().and_then(|s| s.domain(&domain_code)) {
414-
let sdtm_domain = domain.mapping.domain();
430+
if let Some(domain) = state.study.as_ref().and_then(|s| s.domain(&domain_code))
431+
&& let Some(src) = domain.as_source()
432+
{
433+
let sdtm_domain = src.mapping.domain();
415434
if let Some(idx) = sdtm_domain
416435
.variables
417436
.iter()
@@ -471,7 +490,13 @@ fn handle_preview_message(state: &mut AppState, msg: PreviewMessage) -> Task<Mes
471490
}
472491

473492
PreviewMessage::RebuildPreview => {
474-
let Some(domain) = state.study.as_ref().and_then(|s| s.domain(&domain_code)) else {
493+
// Preview rebuild only applies to source domains
494+
let Some(source) = state
495+
.study
496+
.as_ref()
497+
.and_then(|s| s.domain(&domain_code))
498+
.and_then(|d| d.as_source())
499+
else {
475500
return Task::none();
476501
};
477502

@@ -481,8 +506,8 @@ fn handle_preview_message(state: &mut AppState, msg: PreviewMessage) -> Task<Mes
481506
}
482507

483508
let input = PreviewInput {
484-
source_df: domain.source.data.clone(),
485-
mapping: domain.mapping.clone(),
509+
source_df: source.source.data.clone(),
510+
mapping: source.mapping.clone(),
486511
ct_registry: state.terminology.clone(),
487512
};
488513

@@ -515,13 +540,14 @@ fn handle_supp_message(state: &mut AppState, msg: SuppMessage) -> Task<Message>
515540
editor.supp_ui.selected_column = Some(col_name.clone());
516541
editor.supp_ui.edit_draft = None;
517542
}
518-
// Initialize config if not exists
519-
if let Some(domain) = state
543+
// Initialize config if not exists (only for source domains)
544+
if let Some(source) = state
520545
.study
521546
.as_mut()
522547
.and_then(|s| s.domain_mut(&domain_code))
548+
.and_then(|d| d.as_source_mut())
523549
{
524-
domain
550+
source
525551
.supp_config
526552
.entry(col_name.clone())
527553
.or_insert_with(|| SuppColumnConfig::from_column(&col_name));
@@ -596,11 +622,12 @@ fn handle_supp_message(state: &mut AppState, msg: SuppMessage) -> Task<Message>
596622
};
597623

598624
if let Some(col_name) = col
599-
&& let Some(domain) = state
625+
&& let Some(source) = state
600626
.study
601627
.as_mut()
602628
.and_then(|s| s.domain_mut(&domain_code))
603-
&& let Some(config) = domain.supp_config.get_mut(&col_name)
629+
.and_then(|d| d.as_source_mut())
630+
&& let Some(config) = source.supp_config.get_mut(&col_name)
604631
{
605632
if config.qnam.trim().is_empty() || config.qlabel.trim().is_empty() {
606633
return Task::none();
@@ -621,11 +648,12 @@ fn handle_supp_message(state: &mut AppState, msg: SuppMessage) -> Task<Message>
621648
};
622649

623650
if let Some(col_name) = col
624-
&& let Some(domain) = state
651+
&& let Some(source) = state
625652
.study
626653
.as_mut()
627654
.and_then(|s| s.domain_mut(&domain_code))
628-
&& let Some(config) = domain.supp_config.get_mut(&col_name)
655+
.and_then(|d| d.as_source_mut())
656+
&& let Some(config) = source.supp_config.get_mut(&col_name)
629657
{
630658
config.action = SuppAction::Skip;
631659
state.dirty_tracker.mark_dirty();
@@ -643,11 +671,12 @@ fn handle_supp_message(state: &mut AppState, msg: SuppMessage) -> Task<Message>
643671
};
644672

645673
if let Some(col_name) = col
646-
&& let Some(domain) = state
674+
&& let Some(source) = state
647675
.study
648676
.as_mut()
649677
.and_then(|s| s.domain_mut(&domain_code))
650-
&& let Some(config) = domain.supp_config.get_mut(&col_name)
678+
.and_then(|d| d.as_source_mut())
679+
&& let Some(config) = source.supp_config.get_mut(&col_name)
651680
{
652681
config.action = SuppAction::Pending;
653682
state.dirty_tracker.mark_dirty();
@@ -665,8 +694,12 @@ fn handle_supp_message(state: &mut AppState, msg: SuppMessage) -> Task<Message>
665694
};
666695

667696
if let Some(col_name) = &col
668-
&& let Some(domain) = state.study.as_ref().and_then(|s| s.domain(&domain_code))
669-
&& let Some(config) = domain.supp_config.get(col_name)
697+
&& let Some(source) = state
698+
.study
699+
.as_ref()
700+
.and_then(|s| s.domain(&domain_code))
701+
.and_then(|d| d.as_source())
702+
&& let Some(config) = source.supp_config.get(col_name)
670703
{
671704
let draft = SuppEditDraft::from_config(config);
672705
if let ViewState::DomainEditor(editor) = &mut state.view {
@@ -690,11 +723,12 @@ fn handle_supp_message(state: &mut AppState, msg: SuppMessage) -> Task<Message>
690723
return Task::none();
691724
}
692725

693-
if let Some(domain) = state
726+
if let Some(source) = state
694727
.study
695728
.as_mut()
696729
.and_then(|s| s.domain_mut(&domain_code))
697-
&& let Some(config) = domain.supp_config.get_mut(&col_name)
730+
.and_then(|d| d.as_source_mut())
731+
&& let Some(config) = source.supp_config.get_mut(&col_name)
698732
{
699733
config.qnam = draft.qnam;
700734
config.qlabel = draft.qlabel;
@@ -746,8 +780,12 @@ where
746780
let mut dummy = SuppColumnConfig::from_column("");
747781
update(&mut dummy, Some(draft));
748782
}
749-
} else if let Some(domain) = state.study.as_mut().and_then(|s| s.domain_mut(domain_code))
750-
&& let Some(config) = domain.supp_config.get_mut(&col_name)
783+
} else if let Some(source) = state
784+
.study
785+
.as_mut()
786+
.and_then(|s| s.domain_mut(domain_code))
787+
.and_then(|d| d.as_source_mut())
788+
&& let Some(config) = source.supp_config.get_mut(&col_name)
751789
{
752790
update(config, None);
753791
}

crates/tss-gui/src/handler/export.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -183,15 +183,13 @@ fn start_export(state: &mut AppState) -> Task<Message> {
183183

184184
for code in &selected_domains {
185185
if let Some(gui_domain) = study.domain(code) {
186-
// Collect not_collected variables for validation
187-
let not_collected: std::collections::BTreeSet<String> = gui_domain
188-
.mapping
189-
.all_not_collected()
190-
.keys()
191-
.cloned()
192-
.collect();
193-
if !not_collected.is_empty() {
194-
not_collected_map.insert(code.clone(), not_collected);
186+
// Collect not_collected variables for validation (source domains only)
187+
if let Some(source) = gui_domain.as_source() {
188+
let not_collected: std::collections::BTreeSet<String> =
189+
source.mapping.all_not_collected().keys().cloned().collect();
190+
if !not_collected.is_empty() {
191+
not_collected_map.insert(code.clone(), not_collected);
192+
}
195193
}
196194

197195
match crate::service::export::build_domain_export_data(

0 commit comments

Comments
 (0)