diff --git a/src/fourc_webviewer/fourc_webserver.py b/src/fourc_webviewer/fourc_webserver.py index f72004e..0a8adf7 100644 --- a/src/fourc_webviewer/fourc_webserver.py +++ b/src/fourc_webviewer/fourc_webserver.py @@ -338,6 +338,7 @@ def init_general_sections_state_and_server_vars(self): # loop through input file sections self.state.general_sections = {} + self.state.add_section = "" self.state.add_key = "" # key for the add property row self.state.add_value = "" # value for the add property row for section_name, section_data in self._server_vars[ @@ -957,6 +958,97 @@ def change_export_fourc_yaml_path(self, export_fourc_yaml_path, **kwargs): ################################################# # SELECTION CHANGES ################################# ################################################ + @controller.set("click_delete_section_button") + def click_delete_section_button(self, **kwargs): + """Deletes the currently selected section; if it was the last + subsection, delete the main too.""" + if self.state.selected_section_name in self.state.json_schema.get( + "required", [] + ): + return + + cur_main = self.state.selected_main_section_name + cur_section = self.state.selected_section_name + if not cur_main or not cur_section: + return + + general_sections = copy.deepcopy(self.state.general_sections) or {} + sections_names = copy.deepcopy(self.state.section_names) or {} + + # delete the subsection's data + del general_sections[cur_main][cur_section] + self.state.general_sections = general_sections + + # rebuild subsections list (new list ref -> reactive) + subs_before = sections_names[cur_main]["subsections"] + new_subs = [s for s in subs_before if s != cur_section] + sections_names[cur_main] = {**sections_names[cur_main], "subsections": new_subs} + + if cur_section == cur_main: + # last one -> delete the main group immediately + sections_names.pop(cur_main, None) + general_sections.pop(cur_main, None) + self.state.section_names = sections_names + self.state.general_sections = general_sections + + # choose a new valid selection + new_main = next(iter(sections_names.keys()), "") + self.state.selected_main_section_name = new_main + self.state.selected_section_name = ( + sections_names[new_main]["subsections"][0] + if new_main and sections_names[new_main]["subsections"] + else "" + ) + return + + self.state.section_names = sections_names + self.state.selected_main_section_name = cur_main + self.state.selected_section_name = new_subs[0] if new_subs else "" + + @change("add_section") + def change_add_section(self, **kwargs): + """Reaction to section selection.""" + add_section = self.state.add_section + main_section_name = add_section.split("/")[0] or "" + + if add_section not in self.state.json_schema.get("properties", {}): + return + + general_sections = copy.deepcopy(self.state.general_sections) or {} + section_names = copy.deepcopy(self.state.section_names) or {} + + # Ensure main buckets exist + if main_section_name not in section_names: + section_names[main_section_name] = { + "subsections": [main_section_name], + "content_mode": self.state.all_content_modes["general_section"], + } + if main_section_name not in general_sections: + general_sections[main_section_name] = {main_section_name: {}} + + # Store data under main -> sub + if add_section not in general_sections[main_section_name]: + general_sections[main_section_name][add_section] = {} + + # Replace subsections list with a NEW list object + subs = section_names[main_section_name]["subsections"] + if add_section not in subs: + subs = subs + [add_section] # new list ref + section_names[main_section_name] = { + **section_names[main_section_name], # keep content_mode + "subsections": subs, # new list ref + } + + # Commit (new references -> reactive) + self.state.general_sections = general_sections + self.state.section_names = section_names + + # Set a valid selection so VSelect updates + self.state.selected_main_section_name = main_section_name + self.state.selected_section_name = add_section + + self.state.add_section = "" + @change("selected_main_section_name") def change_selected_main_section_name(self, selected_main_section_name, **kwargs): """Reaction to change of state.selected_main_section_name.""" diff --git a/src/fourc_webviewer/gui_utils.py b/src/fourc_webviewer/gui_utils.py index 41834bd..7654963 100644 --- a/src/fourc_webviewer/gui_utils.py +++ b/src/fourc_webviewer/gui_utils.py @@ -249,7 +249,9 @@ def _sections_dropdown(): items=("Object.keys(section_names)",), ) vuetify.VSelect( - v_if=("section_names[selected_main_section_name]['subsections'].length>1"), + v_if=( + "selected_main_section_name!=selected_section_name || section_names[selected_main_section_name]['subsections'].length>1", + ), v_model=("selected_section_name",), items=("section_names[selected_main_section_name]['subsections']",), ) @@ -576,6 +578,39 @@ def _functions_panel(server): ) +def _top_row(server): + """Top row layout (edit mode switch and add section).""" + # EDIT MODE switch + with html.Div(classes="d-flex align-center flex-nowrap w-100", style="gap: 12px;"): + vuetify.VSwitch( + v_model=("edit_mode", "all_edit_modes['view_mode']"), + label=("edit_mode", "VIEW"), + true_value=("all_edit_modes['edit_mode']",), + false_value=("all_edit_modes['view_mode']",), + color="primary", + inset=True, + classes="ma-0", + ) + # add sections on the right + with html.Div( + classes="d-inline-flex align-center ml-auto", + style="gap: 8px;", + v_if="edit_mode == all_edit_modes['edit_mode']", + ): + html.Span("Add Section:", classes="text-h6 font-weight-medium mr-3") + vuetify.VAutocomplete( + v_model=("add_section",), + items=( + "Object.keys(json_schema['properties']).filter(key => !new Set(['MATERIALS', 'TITLE', 'CLONING MATERIAL MAP', 'RESULT DESCRIPTION']).has(key) && !(['DESIGN', 'TOPOLOGY', 'ELEMENTS', 'NODE', 'FUNCT', 'GEOMETRY'].some(n => key.includes(n))))", + ), + dense=True, + solo=True, + filterable=True, + classes="ma-0 flex-grow-0", + style="width: 200px;", + ) + + def _prop_value_table(server): """Table (property - value) layout (for general sections).""" @@ -1414,24 +1449,24 @@ def create_gui(server, render_window): with layout.drawer as drawer: drawer.width = 800 with html.Div(v_if=("vtu_path != ''",)): - # EDIT MODE switch - vuetify.VSwitch( - v_model=("edit_mode", "all_edit_modes['view_mode']"), - label=("edit_mode", "VIEW"), - true_value=("all_edit_modes['edit_mode']",), - false_value=("all_edit_modes['view_mode']",), - color="primary", - inset=True, - classes="ml-5", - ) - # Further elements with conditional rendering (see above) + _top_row(server) _sections_dropdown() _prop_value_table(server) _materials_panel() _functions_panel(server) _design_conditions_panel() _result_description_panel() + vuetify.VBtn( + text="DELETE SECTION", + classes="mx-auto d-block mt-10", + outlined=True, + color="red", + v_if=( + "!json_schema['required'].includes(selected_section_name) && Object.keys(general_sections).includes(selected_main_section_name)", + ), + click=server.controller.click_delete_section_button, + ) with html.Div(classes="flex-column justify-start"): vuetify.VCard( title="No input file content available",