f
This commit is contained in:
@@ -11,6 +11,8 @@ import json
|
||||
from pathlib import Path
|
||||
from SYS.config import load_config, save_config
|
||||
from Store.registry import _discover_store_classes, _required_keys_for
|
||||
from ProviderCore.registry import list_providers
|
||||
from TUI.modalscreen.selection_modal import SelectionModal
|
||||
|
||||
class ConfigModal(ModalScreen):
|
||||
"""A modal for editing the configuration."""
|
||||
@@ -113,11 +115,13 @@ class ConfigModal(ModalScreen):
|
||||
with Horizontal(id="config-actions"):
|
||||
yield Button("Save", variant="success", id="save-btn")
|
||||
yield Button("Add Store", variant="primary", id="add-store-btn")
|
||||
yield Button("Add Provider", variant="primary", id="add-provider-btn")
|
||||
yield Button("Back", id="back-btn")
|
||||
yield Button("Close", variant="error", id="cancel-btn")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.query_one("#add-store-btn", Button).display = False
|
||||
self.query_one("#add-provider-btn", Button).display = False
|
||||
self.query_one("#back-btn", Button).display = False
|
||||
self.refresh_view()
|
||||
|
||||
@@ -131,7 +135,9 @@ class ConfigModal(ModalScreen):
|
||||
# Update visibility of buttons
|
||||
try:
|
||||
self.query_one("#add-store-btn", Button).display = (self.current_category == "stores" and self.editing_item_name is None)
|
||||
self.query_one("#add-provider-btn", Button).display = (self.current_category == "providers" and self.editing_item_name is None)
|
||||
self.query_one("#back-btn", Button).display = (self.editing_item_name is not None)
|
||||
self.query_one("#save-btn", Button).display = (self.editing_item_name is not None or self.current_category == "globals")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -184,6 +190,7 @@ class ConfigModal(ModalScreen):
|
||||
row = Horizontal(
|
||||
Static(name, classes="item-label"),
|
||||
Button("Edit", id=f"edit-provider-{name}"),
|
||||
Button("Delete", variant="error", id=f"del-provider-{name}"),
|
||||
classes="item-row"
|
||||
)
|
||||
container.mount(row)
|
||||
@@ -226,6 +233,20 @@ class ConfigModal(ModalScreen):
|
||||
if rk.upper() not in existing_keys_upper:
|
||||
container.mount(Label(rk))
|
||||
container.mount(Input(value="", id=f"item-{rk}", classes="config-input"))
|
||||
|
||||
# If it's a provider, we might have required keys
|
||||
if item_type == "provider":
|
||||
from ProviderCore.registry import get_provider_class
|
||||
try:
|
||||
pcls = get_provider_class(item_name)
|
||||
if pcls:
|
||||
required_keys = pcls.required_config_keys()
|
||||
for rk in required_keys:
|
||||
if rk.upper() not in existing_keys_upper:
|
||||
container.mount(Label(rk))
|
||||
container.mount(Input(value="", id=f"item-{rk}", classes="config-input"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def create_field(self, name: str, value: Any, id: str) -> Vertical:
|
||||
# This method is now unused - we mount labels and inputs directly
|
||||
@@ -256,6 +277,8 @@ class ConfigModal(ModalScreen):
|
||||
self.editing_item_type = None
|
||||
self.refresh_view()
|
||||
elif bid == "save-btn":
|
||||
if not self.validate_current_editor():
|
||||
return
|
||||
self.save_all()
|
||||
self.notify("Configuration saved!")
|
||||
# Return to the main list view within the current category
|
||||
@@ -274,16 +297,11 @@ class ConfigModal(ModalScreen):
|
||||
self.editing_item_type = "provider"
|
||||
self.refresh_view()
|
||||
elif bid == "add-store-btn":
|
||||
# Create a simple dialog or default folder store
|
||||
new_name = "new_folder"
|
||||
if "store" not in self.config_data:
|
||||
self.config_data["store"] = {}
|
||||
if "folder" not in self.config_data["store"]:
|
||||
self.config_data["store"]["folder"] = {}
|
||||
self.config_data["store"]["folder"][new_name] = {"NAME": new_name, "path": ""}
|
||||
self.editing_item_type = "store-folder"
|
||||
self.editing_item_name = new_name
|
||||
self.refresh_view()
|
||||
options = list(_discover_store_classes().keys())
|
||||
self.app.push_screen(SelectionModal("Select Store Type", options), callback=self.on_store_type_selected)
|
||||
elif bid == "add-provider-btn":
|
||||
options = list(list_providers().keys())
|
||||
self.app.push_screen(SelectionModal("Select Provider Type", options), callback=self.on_provider_type_selected)
|
||||
elif bid.startswith("del-store-"):
|
||||
parts = bid.replace("del-store-", "").split("-", 1)
|
||||
if len(parts) == 2:
|
||||
@@ -292,6 +310,57 @@ class ConfigModal(ModalScreen):
|
||||
if name in self.config_data["store"][stype]:
|
||||
del self.config_data["store"][stype][name]
|
||||
self.refresh_view()
|
||||
elif bid.startswith("del-provider-"):
|
||||
name = bid.replace("del-provider-", "")
|
||||
if "provider" in self.config_data and name in self.config_data["provider"]:
|
||||
del self.config_data["provider"][name]
|
||||
self.refresh_view()
|
||||
|
||||
def on_store_type_selected(self, stype: str) -> None:
|
||||
if not stype: return
|
||||
new_name = f"new_{stype}"
|
||||
if "store" not in self.config_data:
|
||||
self.config_data["store"] = {}
|
||||
if stype not in self.config_data["store"]:
|
||||
self.config_data["store"][stype] = {}
|
||||
|
||||
# Default config for the new store
|
||||
new_config = {"NAME": new_name}
|
||||
classes = _discover_store_classes()
|
||||
if stype in classes:
|
||||
required = _required_keys_for(classes[stype])
|
||||
for rk in required:
|
||||
if rk.upper() != "NAME":
|
||||
new_config[rk] = ""
|
||||
|
||||
self.config_data["store"][stype][new_name] = new_config
|
||||
self.editing_item_type = f"store-{stype}"
|
||||
self.editing_item_name = new_name
|
||||
self.refresh_view()
|
||||
|
||||
def on_provider_type_selected(self, ptype: str) -> None:
|
||||
if not ptype: return
|
||||
if "provider" not in self.config_data:
|
||||
self.config_data["provider"] = {}
|
||||
|
||||
# For providers, they are usually top-level entries in 'provider' dict
|
||||
if ptype not in self.config_data["provider"]:
|
||||
# Get required keys if possible
|
||||
from ProviderCore.registry import get_provider_class
|
||||
try:
|
||||
pcls = get_provider_class(ptype)
|
||||
new_config = {}
|
||||
if pcls:
|
||||
required = pcls.required_config_keys()
|
||||
for rk in required:
|
||||
new_config[rk] = ""
|
||||
self.config_data["provider"][ptype] = new_config
|
||||
except Exception:
|
||||
self.config_data["provider"][ptype] = {}
|
||||
|
||||
self.editing_item_type = "provider"
|
||||
self.editing_item_name = ptype
|
||||
self.refresh_view()
|
||||
|
||||
@on(Input.Changed)
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
@@ -325,3 +394,46 @@ class ConfigModal(ModalScreen):
|
||||
|
||||
def save_all(self) -> None:
|
||||
save_config(self.config_data, config_dir=self.workspace_root)
|
||||
|
||||
def validate_current_editor(self) -> bool:
|
||||
"""Ensure all required fields for the current item are filled."""
|
||||
if not self.editing_item_name:
|
||||
return True
|
||||
|
||||
item_type = str(self.editing_item_type or "")
|
||||
item_name = str(self.editing_item_name or "")
|
||||
|
||||
required_keys = []
|
||||
section = {}
|
||||
|
||||
if item_type.startswith("store-"):
|
||||
stype = item_type.replace("store-", "")
|
||||
classes = _discover_store_classes()
|
||||
if stype in classes:
|
||||
required_keys = _required_keys_for(classes[stype])
|
||||
section = self.config_data.get("store", {}).get(stype, {}).get(item_name, {})
|
||||
elif item_type == "provider":
|
||||
from ProviderCore.registry import get_provider_class
|
||||
try:
|
||||
pcls = get_provider_class(item_name)
|
||||
if pcls:
|
||||
required_keys = pcls.required_config_keys()
|
||||
except Exception:
|
||||
pass
|
||||
section = self.config_data.get("provider", {}).get(item_name, {})
|
||||
|
||||
# Check required keys
|
||||
for rk in required_keys:
|
||||
# Case-insensitive lookup for the required key in the current section
|
||||
val = None
|
||||
rk_upper = rk.upper()
|
||||
for k, v in section.items():
|
||||
if k.upper() == rk_upper:
|
||||
val = v
|
||||
break
|
||||
|
||||
if not val or not str(val).strip():
|
||||
self.notify(f"Required field '{rk}' cannot be blank", severity="error")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
65
TUI/modalscreen/selection_modal.py
Normal file
65
TUI/modalscreen/selection_modal.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from textual.app import ComposeResult
|
||||
from textual.screen import ModalScreen
|
||||
from textual.containers import Container, ScrollableContainer
|
||||
from textual.widgets import Static, Button, Label
|
||||
from typing import List, Callable
|
||||
|
||||
class SelectionModal(ModalScreen[str]):
|
||||
"""A modal for selecting a type from a list of strings."""
|
||||
|
||||
CSS = """
|
||||
SelectionModal {
|
||||
align: center middle;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#selection-container {
|
||||
width: 40%;
|
||||
height: 60%;
|
||||
background: $panel;
|
||||
border: thick $primary;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
.selection-title {
|
||||
background: $accent;
|
||||
color: $text;
|
||||
padding: 0 1;
|
||||
margin-bottom: 1;
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
height: 3;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
.selection-button {
|
||||
width: 100%;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#selection-cancel {
|
||||
width: 100%;
|
||||
margin-top: 1;
|
||||
background: $error;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, title: str, options: List[str]) -> None:
|
||||
super().__init__()
|
||||
self.selection_title = title
|
||||
self.options = sorted(options)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="selection-container"):
|
||||
yield Static(self.selection_title, classes="selection-title")
|
||||
with ScrollableContainer():
|
||||
for opt in self.options:
|
||||
yield Button(opt, id=f"opt-{opt}", classes="selection-button")
|
||||
yield Button("Cancel", id="selection-cancel")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "selection-cancel":
|
||||
self.dismiss("")
|
||||
elif event.button.id and event.button.id.startswith("opt-"):
|
||||
selection = event.button.id.replace("opt-", "")
|
||||
self.dismiss(selection)
|
||||
Reference in New Issue
Block a user